diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e80276c..352f861 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,82 +1,34 @@ name: Continuous Integration Checks -on: - push: - branches: - - master - pull_request: +on: [push, pull_request] jobs: build: strategy: fail-fast: false matrix: - platform: [ ubuntu-latest, macos-latest, windows-latest ] + platform: [ ubuntu-latest ] toolchain: [ stable ] include: - - platform: windows-latest - arguments: --workspace --exclude watchtower-plugin + - toolchain: stable + - toolchain: stable + platform: macos-latest + - toolchain: stable + platform: windows-latest runs-on: ${{ matrix.platform }} steps: - name: Checkout source code - uses: actions/checkout@v4 + uses: actions/checkout@v2 - name: Install Rust ${{ matrix.toolchain }} toolchain - uses: dtolnay/rust-toolchain@master + uses: actions-rs/toolchain@v1 with: toolchain: ${{ matrix.toolchain }} - - name: Install Protoc - uses: arduino/setup-protoc@v3 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} + override: true + profile: minimal - name: Build on Rust ${{ matrix.toolchain }} run: | - cargo build ${{ matrix.arguments }} --verbose --color always + cargo build --verbose --color always - name: Test on Rust ${{ matrix.toolchain }} run: | - cargo test ${{ matrix.arguments }} --verbose --color always - - lint: - runs-on: ubuntu-latest - steps: - - name: Checkout source code - uses: actions/checkout@v4 - - name: Install Rust stable toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - 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 - - 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 - + cargo test --verbose --color always \ No newline at end of file diff --git a/.github/workflows/cln-plugin.yaml b/.github/workflows/cln-plugin.yaml deleted file mode 100644 index 9451239..0000000 --- a/.github/workflows/cln-plugin.yaml +++ /dev/null @@ -1,96 +0,0 @@ -name: CI tests for CLN watchtower-plugin - -on: - push: - branches: - - master - pull_request: - -env: - 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@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 - uses: actions/cache@v4 - with: - path: lightning - key: ${{ runner.os }}-build-cache-cln-dev-v${{ env.cln_version }} - - name: Compile CLN - env: - PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring - run: | - 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 && poetry run make -j 8 - - cln-plugin: - needs: cache-cln - if: ${{ always() }} - runs-on: ubuntu-latest - steps: - - 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 - tar -xzf bitcoin-${{ env.bitcoind_version }}-x86_64-linux-gnu.tar.gz - ln -s $(pwd)/bitcoin-${{ env.bitcoind_version }}/bin/bitcoin* /usr/local/bin - - name: Load CLN cache - id: cache-cln - uses: actions/cache@v4 - with: - path: lightning - key: ${{ runner.os }}-build-cache-cln-dev-v${{ env.cln_version }} - - name: Link CLN - run: | - cd lightning && sudo PATH=$PATH make install - - name: Install teos and the plugin - run: | - 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 --no-root - - name: Run tests - run: | - cd watchtower-plugin/tests - VALGRIND=0 SLOW_MACHINE=1 poetry run pytest test.py --log-cli-level=INFO -s diff --git a/.gitignore b/.gitignore index 599e08c..d58e334 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ target -__pycache__ +*/target +Cargo.lock .vscode -.idea -result diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b646bcb..1d1ad5b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ We use `rustfmt` as our base code formatter. Before submitting a PR make sure yo cargo fmt ``` -In addition, we use [clippy](https://github.com/rust-lang/rust-clippy/) to catch common mistakes and improve the code: +In addition, we use [clippy](https://github.com/rust-lang/rust-clippy/) to detect catch common mistakes and improve the code: ```bash cargo clippy @@ -70,37 +70,8 @@ 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. +Tests should be provided to cover both positive and negative conditions. Test should cover both the proper execution as well as all the covered error paths. PR with no proper test coverage will be rejected. -## Git conventions +## Signing Commits -### 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). +We require that all commits to be merge 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 deleted file mode 100644 index bbf6e71..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,4100 +0,0 @@ -# This file is automatically @generated by Cargo. -# 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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" -dependencies = [ - "generic-array", -] - -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.15", - "once_cell", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi 0.3.9", -] - -[[package]] -name = "anyhow" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "asn1-rs" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" -dependencies = [ - "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]] -name = "assert-json-diff" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite 0.2.16", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.96", -] - -[[package]] -name = "async-trait" -version = "0.1.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.96", -] - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi 0.3.9", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" -dependencies = [ - "futures-core", - "getrandom 0.2.15", - "instant", - "pin-project-lite 0.2.16", - "rand 0.8.5", - "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]] -name = "base32" -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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bech32" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" - -[[package]] -name = "bitcoin" -version = "0.32.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" -dependencies = [ - "base58ck", - "bech32", - "bitcoin-internals", - "bitcoin-io", - "bitcoin-units", - "bitcoin_hashes", - "hex-conservative", - "hex_lit", - "secp256k1", - "serde", -] - -[[package]] -name = "bitcoin-internals" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" -dependencies = [ - "serde", -] - -[[package]] -name = "bitcoin-io" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -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", - "log", - "serde", - "serde_json", -] - -[[package]] -name = "bitcoincore-rpc-json" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8909583c5fab98508e80ef73e5592a651c954993dc6b7739963257d19f0e71a" -dependencies = [ - "bitcoin", - "serde", - "serde_json", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "block-padding", - "generic-array", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-padding" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" - -[[package]] -name = "bstr" -version = "1.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "bumpalo" -version = "3.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" - -[[package]] -name = "bytes" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" - -[[package]] -name = "cc" -version = "1.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chacha20" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fee7ad89dc1128635074c268ee661f90c3f7e83d9fd12910608c36b47d6c3412" -dependencies = [ - "cfg-if 1.0.0", - "cipher", - "cpufeatures 0.1.5", - "zeroize", -] - -[[package]] -name = "chacha20poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1580317203210c517b6d44794abfbe600698276db18127e37ad3e69bf5e848e5" -dependencies = [ - "aead", - "chacha20", - "cipher", - "poly1305", - "zeroize", -] - -[[package]] -name = "chunked_transfer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" - -[[package]] -name = "cipher" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" -dependencies = [ - "generic-array", -] - -[[package]] -name = "clap" -version = "2.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags 1.3.2", - "strsim", - "textwrap", - "unicode-width", - "vec_map", -] - -[[package]] -name = "cln-plugin" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55eefc811f7d5280586dec7342824a84ab81f1d7e0cdb4cd579c1470e3e236cc" -dependencies = [ - "anyhow", - "bytes 1.9.0", - "futures", - "log", - "serde", - "serde_json", - "tokio 1.43.0", - "tokio-stream", - "tokio-util 0.7.13", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "colored" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" -dependencies = [ - "lazy_static", - "windows-sys 0.59.0", -] - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" -dependencies = [ - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "crypto-mac" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" -dependencies = [ - "generic-array", - "subtle", -] - -[[package]] -name = "curve25519-dalek" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.5.1", - "subtle", - "zeroize", -] - -[[package]] -name = "data-encoding" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" - -[[package]] -name = "der-parser" -version = "9.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" -dependencies = [ - "asn1-rs", - "displaydoc", - "nom", - "num-bigint", - "num-traits", - "rusticata-macros", -] - -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -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 2.0.96", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "crypto-common", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.96", -] - -[[package]] -name = "dnssec-prover" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96487aad690d45a83f2b9876828ba856c5430bbb143cb5730d8a5d04a4805179" - -[[package]] -name = "ed25519" -version = "1.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" -dependencies = [ - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" -dependencies = [ - "curve25519-dalek", - "ed25519", - "rand 0.7.3", - "serde", - "sha2", - "zeroize", -] - -[[package]] -name = "either" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if 1.0.0", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "errno" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fuchsia-cprng" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" - -[[package]] -name = "fuchsia-zircon" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" -dependencies = [ - "bitflags 1.3.2", - "fuchsia-zircon-sys", -] - -[[package]] -name = "fuchsia-zircon-sys" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.96", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite 0.2.16", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "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.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" -dependencies = [ - "aho-corasick", - "bstr", - "log", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "h2" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535" -dependencies = [ - "bytes 0.5.6", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 1.9.3", - "slab", - "tokio 0.2.25", - "tokio-util 0.3.1", - "tracing", - "tracing-futures", -] - -[[package]] -name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes 1.9.0", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 2.7.1", - "slab", - "tokio 1.43.0", - "tokio-util 0.7.13", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -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 0.11.2", -] - -[[package]] -name = "headers" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" -dependencies = [ - "base64 0.21.7", - "bytes 1.9.0", - "headers-core", - "http 0.2.12", - "httpdate 1.0.3", - "mime", - "sha1", -] - -[[package]] -name = "headers-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" -dependencies = [ - "http 0.2.12", -] - -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" -dependencies = [ - "crypto-mac", - "digest 0.9.0", -] - -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes 1.9.0", - "fnv", - "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]] -name = "http-body" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" -dependencies = [ - "bytes 0.5.6", - "http 0.2.12", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes 1.9.0", - "http 0.2.12", - "pin-project-lite 0.2.16", -] - -[[package]] -name = "httparse" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" - -[[package]] -name = "httpdate" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "0.13.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a6f157065790a3ed2f88679250419b5cdd96e714a0d65f7797fd337186e96bb" -dependencies = [ - "bytes 0.5.6", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.2.7", - "http 0.2.12", - "http-body 0.3.1", - "httparse", - "httpdate 0.3.2", - "itoa 0.4.8", - "pin-project", - "socket2 0.3.19", - "tokio 0.2.25", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes 1.9.0", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "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", -] - -[[package]] -name = "hyper-timeout" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" -dependencies = [ - "hyper 0.14.32", - "pin-project-lite 0.2.16", - "tokio 1.43.0", - "tokio-io-timeout", -] - -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes 1.9.0", - "hyper 0.14.32", - "native-tls", - "tokio 1.43.0", - "tokio-native-tls", -] - -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "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.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "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.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if 1.0.0", -] - -[[package]] -name = "iovec" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" -dependencies = [ - "libc", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - -[[package]] -name = "itoa" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" - -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "jsonrpc" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3662a38d341d77efecb73caf01420cfa5aa63c0253fd7bc05289ef9f6616e1bf" -dependencies = [ - "base64 0.13.1", - "minreq", - "serde", - "serde_json", -] - -[[package]] -name = "jsonrpc-core" -version = "17.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4467ab6dfa369b69e52bd0692e480c4d117410538526a57a304a0f2250fd95e" -dependencies = [ - "futures", - "futures-executor", - "futures-util", - "log", - "serde", - "serde_derive", - "serde_json", -] - -[[package]] -name = "jsonrpc-http-server" -version = "17.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "522a047cac0958097ee71d047dd71cb84979fd2fa21c7a68fbe12736bef870a2" -dependencies = [ - "futures", - "hyper 0.13.10", - "jsonrpc-core", - "jsonrpc-server-utils", - "log", - "net2", - "parking_lot 0.11.2", - "unicase", -] - -[[package]] -name = "jsonrpc-server-utils" -version = "17.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bce68fa279a2822b3619369cd024f8a4f8e5ce485468834f8679a3c7919aae2d" -dependencies = [ - "bytes 0.5.6", - "futures", - "globset", - "jsonrpc-core", - "lazy_static", - "log", - "tokio 0.2.25", - "tokio-util 0.3.1", - "unicase", -] - -[[package]] -name = "keccak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" -dependencies = [ - "cpufeatures 0.2.17", -] - -[[package]] -name = "kernel32-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" -dependencies = [ - "winapi 0.2.8", - "winapi-build", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.169" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" - -[[package]] -name = "libm" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" - -[[package]] -name = "libsqlite3-sys" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cafc7c74096c336d9d27145f7ebd4f4b6f95ba16aa5a282387267e6925cb58" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "lightning" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3224b577def19c2bb3dcf2c35a95d94909183204c061746d1245ecc6e889e8e" -dependencies = [ - "bech32", - "bitcoin", - "dnssec-prover", - "hashbrown 0.13.2", - "libm", - "lightning-invoice", - "lightning-types", - "possiblyrandom", -] - -[[package]] -name = "lightning-block-sync" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baab5bdee174a2047d939a4ca0dc2e1c23caa0f8cab0b4380aed77a20e116f1e" -dependencies = [ - "bitcoin", - "chunked_transfer", - "lightning", - "serde_json", -] - -[[package]] -name = "lightning-invoice" -version = "0.33.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -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.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.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -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.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata 0.1.10", -] - -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" -dependencies = [ - "cfg-if 0.1.10", - "fuchsia-zircon", - "fuchsia-zircon-sys", - "iovec", - "kernel32-sys", - "libc", - "log", - "miow", - "net2", - "slab", - "winapi 0.2.8", -] - -[[package]] -name = "mio" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" -dependencies = [ - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", -] - -[[package]] -name = "miow" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" -dependencies = [ - "kernel32-sys", - "net2", - "winapi 0.2.8", - "ws2_32-sys", -] - -[[package]] -name = "mockito" -version = "0.32.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "406f43768da5a859ce19bb0978fd8dc2167a7d9a52f3935c6a187242e1a4ff9f" -dependencies = [ - "assert-json-diff", - "colored", - "futures", - "hyper 0.14.32", - "lazy_static", - "log", - "rand 0.8.5", - "regex", - "serde_json", - "serde_urlencoded", - "similar", - "tokio 1.43.0", -] - -[[package]] -name = "multer" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -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 = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "net2" -version = "0.2.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b13b648036a2339d06de780866fbdfda0dde886de7b3af2ddeba8b14f4ee34ac" -dependencies = [ - "cfg-if 0.1.10", - "libc", - "winapi 0.3.9", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -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 = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.9", - "libc", -] - -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "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.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "openssl" -version = "0.10.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e" -dependencies = [ - "bitflags 2.8.0", - "cfg-if 1.0.0", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.96", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-sys" -version = "0.9.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 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.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if 1.0.0", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi 0.3.9", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "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.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "petgraph" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" -dependencies = [ - "fixedbitset", - "indexmap 2.7.1", -] - -[[package]] -name = "pin-project" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.96", -] - -[[package]] -name = "pin-project-lite" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" - -[[package]] -name = "poly1305" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" -dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "possiblyrandom" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b122a615d72104fb3d8b26523fdf9232cd8ee06949fb37e4ce3ff964d15dffd" -dependencies = [ - "getrandom 0.2.15", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.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" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "prost" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" -dependencies = [ - "bytes 1.9.0", - "prost-derive", -] - -[[package]] -name = "prost-build" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" -dependencies = [ - "bytes 1.9.0", - "heck 0.5.0", - "itertools", - "log", - "multimap", - "once_cell", - "petgraph", - "prettyplease", - "prost", - "prost-types", - "regex", - "syn 2.0.96", - "tempfile", -] - -[[package]] -name = "prost-derive" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" -dependencies = [ - "anyhow", - "itertools", - "proc-macro2", - "quote", - "syn 2.0.96", -] - -[[package]] -name = "prost-types" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" -dependencies = [ - "prost", -] - -[[package]] -name = "quote" -version = "1.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi 0.3.9", -] - -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" -dependencies = [ - "rand_core 0.4.2", -] - -[[package]] -name = "rand_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.15", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rcgen" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" -dependencies = [ - "pem", - "ring", - "rustls-pki-types", - "time", - "x509-parser", - "yasna", -] - -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" -dependencies = [ - "bitflags 2.8.0", -] - -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "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.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi 0.3.9", -] - -[[package]] -name = "reqwest" -version = "0.11.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" -dependencies = [ - "base64 0.21.7", - "bytes 1.9.0", - "encoding_rs", - "futures-core", - "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper-tls", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite 0.2.16", - "rustls-pemfile 1.0.4", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "system-configuration", - "tokio 1.43.0", - "tokio-native-tls", - "tokio-socks", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg", -] - -[[package]] -name = "ring" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" -dependencies = [ - "cc", - "cfg-if 1.0.0", - "getrandom 0.2.15", - "libc", - "spin", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rusqlite" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba4d3462c8b2e4d7f4fcfcf2b296dc6b65404fbbc7b63daa37fd485c149daf7" -dependencies = [ - "bitflags 1.3.2", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "memchr", - "smallvec", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rusticata-macros" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" -dependencies = [ - "nom", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.8.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustls" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" -dependencies = [ - "log", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" - -[[package]] -name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -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.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" -dependencies = [ - "cc", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.8.0", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" - -[[package]] -name = "serde" -version = "1.0.217" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.217" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.96", -] - -[[package]] -name = "serde_json" -version = "1.0.138" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" -dependencies = [ - "indexmap 2.7.1", - "itoa 1.0.14", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa 1.0.14", - "ryu", - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if 1.0.0", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if 1.0.0", - "cpufeatures 0.2.17", - "digest 0.9.0", - "opaque-debug", -] - -[[package]] -name = "sha3" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" -dependencies = [ - "block-buffer 0.9.0", - "digest 0.9.0", - "keccak", - "opaque-debug", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -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.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" - -[[package]] -name = "similar" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" - -[[package]] -name = "simple_logger" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48047e77b528151aaf841a10a9025f9459da80ba820e425ff7eb005708a76dc7" -dependencies = [ - "atty", - "colored", - "log", - "time", - "winapi 0.3.9", -] - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - -[[package]] -name = "socket2" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "winapi 0.3.9", -] - -[[package]] -name = "socket2" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - -[[package]] -name = "structopt" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" -dependencies = [ - "clap", - "lazy_static", - "structopt-derive", -] - -[[package]] -name = "structopt-derive" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" -dependencies = [ - "heck 0.3.3", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.96" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" -dependencies = [ - "proc-macro2", - "quote", - "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]] -name = "tempdir" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" -dependencies = [ - "rand 0.4.6", - "remove_dir_all", -] - -[[package]] -name = "tempfile" -version = "3.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" -dependencies = [ - "cfg-if 1.0.0", - "fastrand", - "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", - "home", - "jsonrpc-http-server", - "lightning", - "lightning-block-sync", - "lightning-net-tokio", - "log", - "prost", - "rand 0.8.5", - "rcgen", - "rusqlite", - "serde", - "serde_json", - "simple_logger", - "structopt", - "tempdir", - "teos-common", - "tokio 1.43.0", - "tokio-stream", - "toml", - "tonic", - "tonic-build", - "torut", - "triggered", - "warp", -] - -[[package]] -name = "teos-common" -version = "0.2.0" -dependencies = [ - "bitcoin", - "chacha20poly1305", - "hex", - "lightning", - "prost", - "rand 0.8.5", - "rusqlite", - "serde", - "serde_json", - "tonic", - "tonic-build", -] - -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "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.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" -dependencies = [ - "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.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tokio" -version = "0.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" -dependencies = [ - "bytes 0.5.6", - "fnv", - "futures-core", - "iovec", - "lazy_static", - "memchr", - "mio 0.6.23", - "num_cpus", - "pin-project-lite 0.1.12", - "slab", -] - -[[package]] -name = "tokio" -version = "1.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" -dependencies = [ - "backtrace", - "bytes 1.9.0", - "libc", - "mio 1.0.3", - "parking_lot 0.12.3", - "pin-project-lite 0.2.16", - "signal-hook-registry", - "socket2 0.5.8", - "tokio-macros", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-io-timeout" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" -dependencies = [ - "pin-project-lite 0.2.16", - "tokio 1.43.0", -] - -[[package]] -name = "tokio-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.96", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio 1.43.0", -] - -[[package]] -name = "tokio-rustls" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" -dependencies = [ - "rustls", - "rustls-pki-types", - "tokio 1.43.0", -] - -[[package]] -name = "tokio-socks" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" -dependencies = [ - "either", - "futures-util", - "thiserror", - "tokio 1.43.0", -] - -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite 0.2.16", - "tokio 1.43.0", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" -dependencies = [ - "futures-util", - "log", - "tokio 1.43.0", - "tungstenite", -] - -[[package]] -name = "tokio-util" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" -dependencies = [ - "bytes 0.5.6", - "futures-core", - "futures-sink", - "log", - "pin-project-lite 0.1.12", - "tokio 0.2.25", -] - -[[package]] -name = "tokio-util" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" -dependencies = [ - "bytes 1.9.0", - "futures-core", - "futures-sink", - "pin-project-lite 0.2.16", - "tokio 1.43.0", -] - -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - -[[package]] -name = "tonic" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" -dependencies = [ - "async-stream", - "async-trait", - "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", - "rustls-pemfile 2.2.0", - "rustls-pki-types", - "tokio 1.43.0", - "tokio-rustls", - "tokio-stream", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tonic-build" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4ef6dd70a610078cb4e338a0f79d06bc759ff1b22d2120c2ff02ae264ba9c2" -dependencies = [ - "prettyplease", - "proc-macro2", - "prost-build", - "quote", - "syn 2.0.96", -] - -[[package]] -name = "torut" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99febc413f26cf855b3a309c5872edff5c31e0ffe9c2fce5681868761df36f69" -dependencies = [ - "base32", - "base64 0.13.1", - "derive_more", - "ed25519-dalek", - "hex", - "hmac", - "rand 0.7.3", - "serde", - "serde_derive", - "sha2", - "sha3", - "tokio 1.43.0", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "indexmap 1.9.3", - "pin-project", - "pin-project-lite 0.2.16", - "rand 0.8.5", - "slab", - "tokio 1.43.0", - "tokio-util 0.7.13", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "log", - "pin-project-lite 0.2.16", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.96", -] - -[[package]] -name = "tracing-core" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "pin-project", - "tracing", -] - -[[package]] -name = "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce148eae0d1a376c1b94ae651fc3261d9cb8294788b962b7382066376503a2d1" - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" -dependencies = [ - "byteorder", - "bytes 1.9.0", - "data-encoding", - "http 1.2.0", - "httparse", - "log", - "rand 0.8.5", - "sha1", - "thiserror", - "url", - "utf-8", -] - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "unicase" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" - -[[package]] -name = "unicode-ident" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "universal-hash" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" -dependencies = [ - "generic-array", - "subtle", -] - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "warp" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" -dependencies = [ - "bytes 1.9.0", - "futures-channel", - "futures-util", - "headers", - "http 0.2.12", - "hyper 0.14.32", - "log", - "mime", - "mime_guess", - "multer", - "percent-encoding", - "pin-project", - "scoped-tls", - "serde", - "serde_json", - "serde_urlencoded", - "tokio 1.43.0", - "tokio-tungstenite", - "tokio-util 0.7.13", - "tower-service", - "tracing", -] - -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasi" -version = "0.13.3+wasi-0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -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.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.96", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" -dependencies = [ - "cfg-if 1.0.0", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.96", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "watchtower-plugin" -version = "0.2.0" -dependencies = [ - "backoff", - "bitcoin", - "cln-plugin", - "hex", - "home", - "log", - "mockito", - "reqwest", - "rusqlite", - "serde", - "serde_json", - "tempdir", - "teos-common", - "tokio 1.43.0", - "tonic", -] - -[[package]] -name = "web-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.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.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -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.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -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.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" -dependencies = [ - "winapi 0.2.8", - "winapi-build", -] - -[[package]] -name = "x509-parser" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" -dependencies = [ - "asn1-rs", - "data-encoding", - "der-parser", - "lazy_static", - "nom", - "oid-registry", - "ring", - "rusticata-macros", - "thiserror", - "time", -] - -[[package]] -name = "yasna" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" -dependencies = [ - "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.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "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 a070916..b4aad4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,6 @@ [workspace] -resolver = "2" members = [ "teos", "teos-common", - "watchtower-plugin" -] +] \ No newline at end of file diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 3ea5e11..1c91d88 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -1,24 +1,25 @@ # Dependencies -`rust-teos` has the following system-wide dependencies: +`rusty-teos` has the following system-wide dependencies: - `rust` - `bitcoind` ### Minimum Supported Rust Version (MSRV) -Refer to [toolchain](./rust-toolchain.toml) +FIXME: Define MSRV ### Installing Rust Refer to [rust-lang.org](https://www.rust-lang.org/tools/install). ### Installing bitcoind -`rust-teos` runs on top of a Bitcoin Core node. Other underlying Bitcoin nodes are not supported at the moment. +`rusty-teos` runs on top of a Bitcoin Core node. Other underlying Bitcoin nodes are not supported at the moment. You can get Bitcoin Core from [bitcoincore.org](https://bitcoincore.org/en/download/). Bitcoin needs to be running with the following options enabled: +- `txindex` to be able to look for non-wallet transactions - `server` to run rpc commands Here's an example of a `bitcoin.conf` you can use for mainnet. **DO NOT USE THE PROVIDED RPC USER AND PASSWORD.** @@ -30,6 +31,9 @@ rpcuser=user rpcpassword=passwd rpcservertimeout=600 +# [blockchain] +txindex=1 + # [others] daemon=1 debug=1 diff --git a/INSTALL.md b/INSTALL.md index bb32bda..f7654a9 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -3,9 +3,9 @@ The tower can be installed and tested using cargo: ``` -git clone https://github.com/talaia-labs/rust-teos.git -cd rust-teos -cargo install --locked --path teos +git https://github.com/sr-gi/rusty-teos.git +cd rusty-teos +cargo install --path teos ``` You can run tests with: @@ -14,8 +14,4 @@ You can run tests with: cargo test ``` -Please refer to the cargo documentation for more detailed instructions. - -# Systemd setup for backend - -Refer to [contrib](contrib/init/README.md) for a detailed explanation of how to set up your systemd service for `teosd`. \ No newline at end of file +Please refer to the cargo documentation for more detailed instructions. \ No newline at end of file diff --git a/README.md b/README.md index 0336b48..fb2a936 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,13 @@ +**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 in two main crates: - -`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`. -- `teos-common`: including shared functionality between server and client-side (useful to build a client). +- `teos`: including the tower's main functionality (server-side) and a CLI. +- `teos-common`: including shared functionality between server and client side (useful to build a client). ## Dependencies @@ -21,9 +18,8 @@ Refer to [INSTALL.md](INSTALL.md) ## Running TEOS -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. +Make sure bitcoind is running before running `rust-teos` (it will fail at startup if it cannot connect to bitcoind). You can find +[here](DEPENDENCIES.md#installing-bitcoind) a sample config file. ### Starting the tower daemon â™– @@ -35,31 +31,31 @@ teosd ### Configuration file and command line parameters -`teosd` comes with a default configuration that can be found at [teos/src/config.rs](teos/src/config.rs). +`rust-teos` comes with a default configuration that can be found at [teos/src/config.rs](teos/src/config.rs). The configuration includes, amongst others, where your data folder is placed, what network it connects to, etc. To change the configuration defaults you can: -- Define a configuration file named `teos.toml` following the template (check [conf_template.toml](teos/src/conf_template.toml)) and place it in the `data_dir` (that defaults to `~/.teos/`). +- Define a configuration file named `teos.toml` following the template (check [teos/src/conf_template.toml](teos/src/conf_template.toml)) and place it in the `data_dir` (that defaults to `~/.teos/`) -and/or +and / or - Add some global options when running the daemon (run `teosd -h` for more info). -### Passing command-line options to `teosd` +### Passing command line options to `teosd` -Some configuration options can also be specified when running `teosd`. We can, for instance, change the tower data directory as follows: +Some configuration options can also be passed as options when running `teosd`. We can, for instance, change the tower data directory as follows: ``` teosd --datadir= ``` -### Running `teosd` in another network +### Running TEOS in another network -By default, `teosd` runs on `mainnet`. In order to run it on another network, you need to change the network parameter in the configuration file or pass the network parameter as a command-line option. Notice that if `teosd` does not find a `bitcoind` node running in the same network that it is set to run, it will refuse to run. +By default, `rust-teos` runs on `mainnet`. In order to run it on another network you need to change the network parameter in the configuration file or pass the network parameter as a command line option. Notice that if teos does not find a `bitcoind` node running in the same network that it is set to run, it will refuse to run. -The configuration file option to change the network where `teosd` will run is `btc_network`: +The configuration file option to change the network where `teos` will run is `btc_network`: ``` btc_network = mainnet @@ -71,38 +67,17 @@ For regtest, it should look like: btc_network = regtest ``` -### Running `teosd` with Tor - -This requires a Tor daemon running on the same machine as `teosd` and a control port open on that daemon. - -Download Tor from the [torproject site](https://www.torproject.org/download/). - -To open Tor's control port, you add the following to the Tor config file ([source](https://2019.www.torproject.org/docs/faq.html.en#torrc)): - -``` -## The port on which Tor will listen for local connections from Tor -## controller applications, as documented in control-spec.txt. -ControlPort 9051 - -## If you enable the controlport, be sure to enable one of these -## authentication methods, to prevent attackers from accessing it. -CookieAuthentication 1 -CookieAuthFileGroupReadable 1 -``` - -Once the Tor daemon is running, and the control port is open, make sure to enable `--torsupport` when running `teosd`. - ### Tower id and signing key -`teosd` needs a pair of keys that will serve as tower id and signing key. The former can be used by users to identify the tower, whereas the latter is used by the tower to sign responses. These keys are automatically generated on the first run and can be refreshed by running `teosd` with the `--overwritekey` flag. Notice that once a key is overwritten you won't be able to use the previous key again*. +`teos` needs a pair of keys that will serve as tower id and signing key. The former can be used by users to identify the tower, whereas the latter is used by the tower to sign responses. These keys are automatically generated on the first run, and can be refreshed by running `teos` with the `--overwritekey` flag. Notice that once a key is overwritten you won't be able to use the previous key again*. -\* Old keys are actually kept in the tower's database as a fail-safe in case you overwrite them by mistake. However, there is no automated way of switching back to an old key. Feel free to open an issue if you overwrote your key by mistake and need support to recover it. +\* Old keys are actually kept in the tower's database as a fail safe in case you overwrite them by mistake. However, there is no automated way of switching back to and old key. Feel free to open an issue if you overwrote your key by mistake and need support to recover it. -## Interacting with a TEOS instance +## Interacting with a TEOS Instance -You can interact with a `teosd` instance (either run by yourself or someone else) by using `teos-cli`. This is an admin tool that has privileged access to the watchtower, and it should therefore only be used within a trusted environment (for example, the same machine). +You can interact with a `teos` instance (either run by yourself or someone else) by using `teos-cli`. This is an admin tool that has privileged access to the watchtower, and it should therefore only be used within a trusted environment (for example, the same machine). -While `teos-cli` works independently of `teosd`, it shares the same configuration file by default, of which it only uses a subset of its settings. The folder can be changed using the `--datadir` command-line argument if desired. +While `teos-cli` works independently of `teos`, it shares the same configuration file by default, of which it only uses a subset of its settings. The folder can be changed using the `--datadir` command line argument, if desired. For help on the available arguments and commands, you can run: @@ -110,18 +85,9 @@ For help on the available arguments and commands, you can run: teos-cli -h ``` -### Running teos-cli remotely - -To run `teos-cli` remotely, you'll need to take one extra step. When `teosd` is started up, self-signed certificates are automatically generated for a user to make a secure connection to the remote TEOS watchtower. When the CLI is run locally, it knows where to find these files. But if run remotely, these files need to be copied over to the machine where the CLI is being run. - -The files are generated to the data directory (by default stored at `~/.teos/`). To run remotely, users need to copy the `client.pem`, `client-key.pem`, and `ca.pem` files to the corresponding watchtower data directory on the machine where the CLI is being run. That is, by default, to `~/.teos/` on the remote machine. - ## Interacting with TEOS as a client -### TEOS clients -Here is a list of the available clients for `teos`: - -- [watchtower-client for CLN](watchtower-plugin/) +FIXME: Add client and docs ## Contributing Refer to [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/coffee.yml b/coffee.yml deleted file mode 100644 index 143466e..0000000 --- a/coffee.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -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/contrib/init/README.md b/contrib/init/README.md deleted file mode 100644 index e627707..0000000 --- a/contrib/init/README.md +++ /dev/null @@ -1,42 +0,0 @@ -**This document guides you into how to set-up a systemd service to run `teosd`.** - -Since the teos service requires bitcoin to run, it is strongly recommended to also create a [system service for bitcoin](https://github.com/bitcoin/bitcoin/blob/master/contrib/init/bitcoind.service). - -Once you have set the bitcoin service, proceed to copy [teosd.service](teosd.service) to the systemd folder, that is, if running from this folder: - -``` -cp teosd.service /etc/systemd/system -``` - -You can also create a file called `teosd.service` in the systemd folder and copy the content of [teosd.service](teosd.service) to it: - -``` -sudo vim /etc/systemd/system/teosd.service -``` - -Notice the provided service file is using `teos` both as user and group for the service, so you may want to update that if that is not the configuration you are intending to use. Here are the lines to be updated: - -``` -[Service] -ExecStart=/home//.cargo/bin/teosd -SyslogIdentifier= - -# Directory creation and permissions -#################################### -User= -Group= -``` - -The next step is enabling the service. You can do so by running: - -``` -sudo systemctl enable teosd.service -``` - -Finally, you can start the service by running: - -``` -sudo systemctl start teosd.service -``` - -From that point on, the tower will be run every time your system is turned on, and restarted if needed. diff --git a/contrib/init/teosd.service b/contrib/init/teosd.service deleted file mode 100644 index 2a37ccd..0000000 --- a/contrib/init/teosd.service +++ /dev/null @@ -1,46 +0,0 @@ -[Unit] -Description=The Eye of Satoshi daemon -Requires=bitcoind.service -After=bitcoind.service -Wants=network-online.target -After=network-online.target - -[Service] -ExecStart=/home/teos/.cargo/bin/teosd -StandardOutput=journal -StandardError=journal -SyslogIdentifier=teos - -# Process management -#################### -Type=simple -Restart=on-failure -TimeoutSec=300 -RestartSec=60 - -# Directory creation and permissions -#################################### -User=teos -Group=teos - -# Hardening measures -#################### -# Provide a private /tmp and /var/tmp. -PrivateTmp=true - -# Mount /usr, /boot/ and /etc read-only for the process. -ProtectSystem=full - -# Disallow the process and all of its children to gain -# new privileges through execve(). -NoNewPrivileges=true - -# Use a new /dev namespace only populated with API pseudo devices -# such as /dev/null, /dev/zero and /dev/random. -PrivateDevices=true - -# Deny the creation of writable and executable memory mappings. -MemoryDenyWriteExecute=true - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index 1333ea8..0000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,49 +0,0 @@ -# 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 deleted file mode 100644 index 6c4289a..0000000 --- a/docker/README.md +++ /dev/null @@ -1,113 +0,0 @@ -## 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 deleted file mode 100755 index f8975af..0000000 --- a/docker/entrypoint.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/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 deleted file mode 100644 index ca135b2..0000000 --- a/flake.lock +++ /dev/null @@ -1,116 +0,0 @@ -{ - "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 deleted file mode 100644 index a272a74..0000000 --- a/flake.nix +++ /dev/null @@ -1,114 +0,0 @@ -{ - 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 deleted file mode 100644 index 8a95a07..0000000 --- a/rust-toolchain.toml +++ /dev/null @@ -1,6 +0,0 @@ -[toolchain] -channel = "1.81.0" -components = [ - "rustfmt", - "clippy", -] diff --git a/teos-common/Cargo.toml b/teos-common/Cargo.toml index 14a45d4..ea5ca28 100644 --- a/teos-common/Cargo.toml +++ b/teos-common/Cargo.toml @@ -1,27 +1,19 @@ [package] name = "teos-common" -version = "0.2.0" +version = "0.0.1" authors = ["Sergi Delgado Segura "] -edition = "2021" +edition = "2018" # 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.12" -rusqlite = { version = "0.26.0", features = [ "bundled", "limits" ] } -serde = "1.0.130" -serde_json = "1.0" -tonic = "0.11" +hex = "0.4.3" # Crypto rand = "0.8.4" chacha20poly1305 = "0.8.0" # Bitcoin and Lightning -bitcoin = { version = "0.32.0", features = [ "serde" ] } -lightning = "0.1.0" - -[build-dependencies] -tonic-build = "0.11" +bitcoin = "0.27" +lightning = "0.0.99" diff --git a/teos-common/build.rs b/teos-common/build.rs deleted file mode 100644 index a66e76f..0000000 --- a/teos-common/build.rs +++ /dev/null @@ -1,30 +0,0 @@ -fn main() -> Result<(), Box> { - tonic_build::configure() - .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") - .type_attribute("AppointmentData.appointment_data", "#[serde(untagged)]") - .field_attribute("AppointmentData.appointment_data", "#[serde(flatten)]") - .field_attribute("appointment_data", "#[serde(rename = \"appointment\")]") - .field_attribute("user_id", "#[serde(with = \"hex::serde\")]") - .field_attribute("locator", "#[serde(with = \"hex::serde\")]") - .field_attribute( - "locators", - "#[serde(with = \"crate::ser::serde_vec_bytes\")]", - ) - .field_attribute("encrypted_blob", "#[serde(with = \"hex::serde\")]") - .field_attribute("dispute_txid", "#[serde(with = \"crate::ser::serde_be\")]") - .field_attribute("penalty_txid", "#[serde(with = \"crate::ser::serde_be\")]") - .field_attribute("penalty_rawtx", "#[serde(with = \"hex::serde\")]") - .field_attribute( - "GetAppointmentResponse.status", - "#[serde(with = \"crate::ser::serde_status\")]", - ) - .compile( - &[ - "proto/common/teos/v2/appointment.proto", - "proto/common/teos/v2/user.proto", - ], - &["proto/common/teos/v2"], - )?; - - Ok(()) -} diff --git a/teos-common/proto/common/teos/v2/appointment.proto b/teos-common/proto/common/teos/v2/appointment.proto deleted file mode 100644 index 3eb9d1e..0000000 --- a/teos-common/proto/common/teos/v2/appointment.proto +++ /dev/null @@ -1,75 +0,0 @@ -syntax = "proto3"; -package common.teos.v2; - -message Appointment { - /* - Contains the basic information about an appointment (Watcher) and it's used for messages like - AddAppointmentRequest or encapsulated inside AppointmentData for GetAppointmentResponse - */ - - bytes locator = 1; - bytes encrypted_blob = 2; - uint32 to_self_delay = 3; - - } - - message Tracker { - // It's the equivalent of an appointment message from data held by the Responder. - - bytes dispute_txid = 1; - bytes penalty_txid = 2; - bytes penalty_rawtx = 3; - } - - message AppointmentData { - /* - Encapsulates the data for a GetAppointmentResponse, given it can be an appointment (data is on the Watcher) or a - tracker (data is on the Responder). - */ - - oneof appointment_data { - Appointment appointment = 1; - Tracker tracker = 2; - } - } - - message AddAppointmentRequest { - // Request to add an appointment to the backend, contains the appointment data and the user signature. - - Appointment appointment = 1; - string signature = 2; - } - - message AddAppointmentResponse { - /* - Response to an AddAppointmentRequest, contains the locator to identify the added appointment, the tower signature, - the block at which the tower has started (or will start) watching for the appointment, and the updated subscription - information. - */ - - bytes locator = 1; - uint32 start_block = 2; - string signature = 3; - uint32 available_slots = 4; - uint32 subscription_expiry = 5; - } - - message GetAppointmentRequest { - // Request to get information about an appointment. Contains the appointment locator and a signature by the user. - - bytes locator = 1; - string signature = 2; - } - - message GetAppointmentResponse { - // Response to a GetAppointmentRequest. Contains the appointment data encapsulated in an AppointmentData message. - - AppointmentData appointment_data = 1; - enum AppointmentStatus { - NOT_FOUND = 0; - BEING_WATCHED = 1; - DISPUTE_RESPONDED = 2; - - } - AppointmentStatus status = 2; - } \ No newline at end of file diff --git a/teos-common/proto/common/teos/v2/user.proto b/teos-common/proto/common/teos/v2/user.proto deleted file mode 100644 index 84e54d7..0000000 --- a/teos-common/proto/common/teos/v2/user.proto +++ /dev/null @@ -1,32 +0,0 @@ -syntax = "proto3"; -package common.teos.v2; - -message RegisterRequest { - // Requests a user registration with the tower. Contains the user id in the form of a compressed ECDSA public key. - - bytes user_id = 1; - } - - message RegisterResponse { - // Response to a RegisterRequest, contains the registration information alongside the tower signature of the agreement. - - bytes user_id = 1; - uint32 available_slots = 2; - uint32 subscription_start = 3; - uint32 subscription_expiry = 4; - string subscription_signature = 5; - } - - message GetSubscriptionInfoRequest { - // Request to get a specific user's subscription info. - - string signature = 1; -} - -message GetSubscriptionInfoResponse { - // Response with the information the tower has about a specific user - - uint32 available_slots = 1; - uint32 subscription_expiry = 2; - repeated bytes locators = 3; -} \ No newline at end of file diff --git a/teos-common/src/appointment.rs b/teos-common/src/appointment.rs index 7ac1e0f..b22f925 100644 --- a/teos-common/src/appointment.rs +++ b/teos-common/src/appointment.rs @@ -1,17 +1,15 @@ //! Logic related to appointments shared between users and the towers. -use serde::{Deserialize, Serialize}; +use hex; use std::array::TryFromSliceError; use std::{convert::TryInto, fmt}; use bitcoin::Txid; -use crate::protos as msgs; - pub const LOCATOR_LEN: usize = 16; /// User identifier for appointments. -#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Serialize, Deserialize)] +#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] pub struct Locator([u8; LOCATOR_LEN]); impl Locator { @@ -21,42 +19,36 @@ impl Locator { } /// Encodes a locator into its byte representation. - pub fn to_vec(&self) -> Vec { + pub fn serialize(&self) -> Vec { self.0.to_vec() } /// Builds a locator from its byte representation. - pub fn from_slice(data: &[u8]) -> Result { + pub fn deserialize(data: &[u8]) -> Result { data.try_into().map(Self) } } +impl std::str::FromStr for Locator { + type Err = String; + + fn from_str(s: &str) -> Result { + let raw_locator = hex::decode(s).map_err(|_| "Locator is not hex encoded")?; + Locator::deserialize(&raw_locator) + .map_err(|_| "Locator cannot be built from the given data".into()) + } +} + impl fmt::Display for Locator { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", hex::encode(self.to_vec())) - } -} - -impl AsRef<[u8]> for Locator { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -impl hex::FromHex for Locator { - type Error = String; - - fn from_hex>(hex: T) -> Result { - let raw_locator = hex::decode(hex).map_err(|_| "Locator is not hex encoded")?; - Locator::from_slice(&raw_locator) - .map_err(|_| "Locator cannot be built from the given data".into()) + write!(f, "{}", hex::encode(self.serialize())) } } /// Contains data regarding an appointment between a client and the tower. /// /// An appointment is requested for every new channel update. -#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, Eq, PartialEq, Clone)] pub struct Appointment { /// The user identifier for the appointment. pub locator: Locator, @@ -70,7 +62,6 @@ pub struct Appointment { } /// Represents all the possible states of an appointment in the tower, or in a response to a client request. -#[derive(Serialize, Deserialize, Debug)] pub enum AppointmentStatus { NotFound = 0, BeingWatched = 1, @@ -95,7 +86,7 @@ impl std::str::FromStr for AppointmentStatus { "being_watched" => Ok(AppointmentStatus::BeingWatched), "dispute_responded" => Ok(AppointmentStatus::DisputeResponded), "not_found" => Ok(AppointmentStatus::NotFound), - _ => Err(format!("Unknown status: {s}")), + _ => Err(format!("Unknown status: {}", s)), } } } @@ -107,7 +98,7 @@ impl fmt::Display for AppointmentStatus { AppointmentStatus::DisputeResponded => "dispute_responded", AppointmentStatus::NotFound => "not_found", }; - write!(f, "{s}") + write!(f, "{}", s) } } @@ -127,27 +118,10 @@ impl Appointment { /// `locator || encrypted_blob || to_self_delay` /// /// All values are big endian. - pub fn to_vec(&self) -> Vec { - let mut result = self.locator.to_vec(); + pub fn serialize(&self) -> Vec { + let mut result = self.locator.serialize(); result.extend(&self.encrypted_blob); result.extend(self.to_self_delay.to_be_bytes().to_vec()); result } } - -impl From for msgs::Appointment { - fn from(a: Appointment) -> Self { - Self { - locator: a.locator.to_vec(), - encrypted_blob: a.encrypted_blob.clone(), - to_self_delay: a.to_self_delay, - } - } -} - -/// Computes the number of slots an appointment takes from a user subscription. -/// -/// This is based on the [encrypted_blob](Appointment::encrypted_blob) size and the slot size that was defined by the [Gatekeeper](crate::gatekeeper::Gatekeeper). -pub fn compute_appointment_slots(blob_size: usize, blob_max_size: usize) -> u32 { - (blob_size as f32 / blob_max_size as f32).ceil() as u32 -} diff --git a/teos-common/src/cryptography.rs b/teos-common/src/cryptography.rs index 5dee477..690edd6 100644 --- a/teos-common/src/cryptography.rs +++ b/teos-common/src/cryptography.rs @@ -1,13 +1,14 @@ //! Cryptography module, used in the interaction between users and towers. -use chacha20poly1305::aead::{Aead, NewAead}; -use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; use rand::distributions::Uniform; use rand::Rng; -use bitcoin::consensus; +use chacha20poly1305::aead::{Aead, NewAead}; +use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; + use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::{Error, PublicKey, Secp256k1, SecretKey}; +use bitcoin::util::psbt::serialize::{Deserialize, Serialize}; use bitcoin::{Transaction, Txid}; use lightning::util::message_signing; @@ -19,7 +20,7 @@ pub enum DecryptingError { } /// Shadows [message_signing::sign]. -pub fn sign(msg: &[u8], sk: &SecretKey) -> String { +pub fn sign(msg: &[u8], sk: &SecretKey) -> Result { message_signing::sign(msg, sk) } @@ -46,11 +47,11 @@ pub fn encrypt( ) -> Result, chacha20poly1305::aead::Error> { // Defaults is [0; 12] let nonce = Nonce::default(); - let k = sha256::Hash::hash(secret.as_byte_array()); - let key = Key::from_slice(k.as_byte_array()); + let _k = sha256::Hash::hash(secret); + let key = Key::from_slice(&_k); let cypher = ChaCha20Poly1305::new(key); - cypher.encrypt(&nonce, consensus::serialize(message).as_ref()) + cypher.encrypt(&nonce, message.serialize().as_ref()) } /// Decrypts an encrypted blob of data using `chacha20poly1305` and a given secret. @@ -63,13 +64,13 @@ 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.as_byte_array()); - let key = Key::from_slice(k.as_byte_array()); + let _k = sha256::Hash::hash(secret); + let key = Key::from_slice(&_k); let cypher = ChaCha20Poly1305::new(key); match cypher.decrypt(&nonce, encrypted_blob.as_ref()) { - Ok(tx_bytes) => consensus::deserialize(&tx_bytes).map_err(DecryptingError::Encode), + Ok(tx_bytes) => Transaction::deserialize(&tx_bytes).map_err(DecryptingError::Encode), Err(e) => Err(DecryptingError::AED(e)), } } @@ -80,13 +81,17 @@ 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); - (&mut rng).sample_iter(uniform_u8).take(size).collect() + let v: Vec = (&mut rng).sample_iter(uniform_u8).take(size).collect(); + + v } /// 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(&get_random_bytes(32)) { + if let Ok(sk) = SecretKey::from_slice(&raw_sk) { return (sk, PublicKey::from_secret_key(&Secp256k1::new(), &sk)); } } @@ -94,11 +99,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; + use bitcoin::{hashes::hex::FromHex, util::psbt::serialize::Deserialize}; const HEX_TX: &str = "010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff54038e830a1b4d696e656420627920416e74506f6f6c373432c2005b005e7a0ae3fabe6d6d7841cd582ead8ea5dd8e3de1173cae6fcd2a53c7362ebb7fb6f815604fe07cbe0200000000000000ac0e060005f90000ffffffff04d9476026000000001976a91411dbe48cc6b617f9c6adaf4d9ed5f625b1c7cb5988ac0000000000000000266a24aa21a9ed7248c6efddd8d99bfddd7f499f0b915bffa8253003cc934df1ff14a81301e2340000000000000000266a24b9e11b6d7054937e13f39529d6ad7e685e9dd4efa426f247d5f5a5bed58cdddb2d0fa60100000000000000002b6a2952534b424c4f434b3a054a68aa5368740e8b3e3c67bce45619c2cfd07d4d4f0936a5612d2d0034fa0a0120000000000000000000000000000000000000000000000000000000000000000000000000"; const HEX_TXID: &str = "d6ac4a5e61657c4c604dcde855a1db74ec6b3e54f32695d72c5e11c7761ea1b4"; @@ -109,17 +111,17 @@ mod tests { let expected_enc_blob = Vec::from_hex(ENC_BLOB).unwrap(); let tx_bytes = Vec::from_hex(HEX_TX).unwrap(); - let tx: Transaction = consensus::deserialize(&tx_bytes).unwrap(); - let txid = bitcoin::Txid::from_str(HEX_TXID).unwrap(); + let tx = Transaction::deserialize(&tx_bytes).unwrap(); + let txid = Txid::from_hex(HEX_TXID).unwrap(); assert_eq!(encrypt(&tx, &txid).unwrap(), expected_enc_blob); } #[test] fn test_decrypt() { - let expected_tx = consensus::deserialize(&Vec::from_hex(HEX_TX).unwrap()).unwrap(); + let expected_tx = Transaction::deserialize(&Vec::from_hex(HEX_TX).unwrap()).unwrap(); let encrypted_blob = Vec::from_hex(ENC_BLOB).unwrap(); - let txid = bitcoin::Txid::from_str(HEX_TXID).unwrap(); + let txid = Txid::from_hex(HEX_TXID).unwrap(); assert_eq!(decrypt(&encrypted_blob, &txid).unwrap(), expected_tx); } } diff --git a/teos-common/src/dbm.rs b/teos-common/src/dbm.rs deleted file mode 100644 index d94369f..0000000 --- a/teos-common/src/dbm.rs +++ /dev/null @@ -1,72 +0,0 @@ -//! Logic related to a common database manager, component in charge of persisting data on disk. This is the base of more complex managers -//! that can be used by both clients and towers. -//! - -use rusqlite::ffi::{SQLITE_CONSTRAINT_FOREIGNKEY, SQLITE_CONSTRAINT_PRIMARYKEY}; -use rusqlite::{Connection, Error as SqliteError, ErrorCode, Params}; - -/// Packs the errors than can raise when interacting with the underlying database. -#[derive(Debug)] -pub enum Error { - AlreadyExists, - MissingForeignKey, - MissingField, - NotFound, - Unknown(SqliteError), -} - -pub trait DatabaseConnection { - fn get_connection(&self) -> &Connection; - fn get_mut_connection(&mut self) -> &mut Connection; -} - -pub trait DatabaseManager: Sized { - fn create_tables(&mut self, tables: Vec<&str>) -> Result<(), SqliteError>; - fn store_data(&self, query: &str, params: P) -> Result<(), Error>; - fn remove_data(&self, query: &str, params: P) -> Result<(), Error>; - fn update_data(&self, query: &str, params: P) -> Result<(), Error>; -} - -impl DatabaseManager for T { - /// Creates the database tables if not present. - fn create_tables(&mut self, tables: Vec<&str>) -> Result<(), SqliteError> { - let tx = self.get_mut_connection().transaction().unwrap(); - for table in tables.iter() { - tx.execute(table, [])?; - } - tx.commit() - } - - /// Generic method to store data into the database. - fn store_data(&self, query: &str, params: P) -> Result<(), Error> { - match self.get_connection().execute(query, params) { - Ok(_) => Ok(()), - Err(e) => match e { - SqliteError::SqliteFailure(ie, _) => match ie.code { - ErrorCode::ConstraintViolation => match ie.extended_code { - SQLITE_CONSTRAINT_FOREIGNKEY => Err(Error::MissingForeignKey), - SQLITE_CONSTRAINT_PRIMARYKEY => Err(Error::AlreadyExists), - _ => Err(Error::Unknown(e)), - }, - _ => Err(Error::Unknown(e)), - }, - _ => Err(Error::Unknown(e)), - }, - } - } - - /// Generic method to remove data from the database. - fn remove_data(&self, query: &str, params: P) -> Result<(), Error> { - match self.get_connection().execute(query, params).unwrap() { - 0 => Err(Error::NotFound), - _ => Ok(()), - } - } - - /// Generic method to update data from the database. - fn update_data(&self, query: &str, params: P) -> Result<(), Error> { - // Updating data is fundamentally the same as deleting it in terms of interface. - // A query is sent and either no row is modified or some rows are - self.remove_data(query, params) - } -} diff --git a/teos-common/src/lib.rs b/teos-common/src/lib.rs index 12a7df0..b0e58d7 100644 --- a/teos-common/src/lib.rs +++ b/teos-common/src/lib.rs @@ -2,45 +2,30 @@ //! //! Functionality shared between users and towers. -// FIXME: This is a temporary fix. See https://github.com/tokio-rs/prost/issues/661 -#[allow(clippy::derive_partial_eq_without_eq)] -pub mod protos { - tonic::include_proto!("common.teos.v2"); -} - pub mod appointment; pub mod constants; pub mod cryptography; -pub mod dbm; pub mod errors; -pub mod net; pub mod receipts; -pub mod ser; -pub mod test_utils; use std::fmt; -use std::{convert::TryFrom, str::FromStr}; - -use serde::{Deserialize, Serialize}; -use serde_json::json; use bitcoin::secp256k1::{Error, PublicKey}; pub const USER_ID_LEN: usize = 33; -pub use UserId as TowerId; /// User identifier. A wrapper around a [PublicKey]. -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] pub struct UserId(pub PublicKey); impl UserId { /// Encodes the user id in its byte representation. - pub fn to_vec(&self) -> Vec { + pub fn serialize(&self) -> Vec { self.0.serialize().to_vec() } /// Builds a user id from its byte representation. - pub fn from_slice(data: &[u8]) -> Result { + pub fn deserialize(data: &[u8]) -> Result { Ok(UserId(PublicKey::from_slice(data)?)) } } @@ -62,126 +47,3 @@ impl fmt::Display for UserId { write!(f, "{}", self.0) } } - -impl TryFrom for UserId { - type Error = String; - - fn try_from(value: serde_json::Value) -> Result { - match value { - serde_json::Value::String(s) => UserId::from_str(&s), - serde_json::Value::Array(mut a) => { - let param_count = a.len(); - if param_count == 1 { - UserId::try_from(a.pop().unwrap()) - } else { - Err(format!( - "Unexpected json format. Expected a single parameter. Received: {param_count}" - )) - } - } - serde_json::Value::Object(mut m) => { - let param_count = m.len(); - if param_count > 1 { - Err(format!( - "Unexpected json format. Expected a single parameter. Received: {param_count}" - )) - } else { - UserId::try_from(json!(m - .remove("user_id") - .or_else(|| m.remove("tower_id")) - .ok_or("user_id or tower_id not found")?)) - } - } - _ => Err(format!( - "Unexpected request format. Expected: user_id/tower_id. Received: '{value}'" - )), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - use std::collections::HashMap; - - use crate::test_utils::get_random_user_id; - - #[test] - fn try_from_json_string() { - let user_id = get_random_user_id(); - assert_eq!(UserId::try_from(json!(user_id.to_string())), Ok(user_id)); - } - - #[test] - fn try_from_json_wrong_string() { - let user_id = "not_a_user_id"; - assert!(matches!( - UserId::try_from(json!(user_id.to_string())), - Err(..) - )); - } - - #[test] - fn try_from_json_array() { - let user_id = get_random_user_id(); - assert_eq!(UserId::try_from(json!([user_id.to_string()])), Ok(user_id)); - } - - #[test] - fn try_from_json_array_empty() { - assert!(matches!(UserId::try_from(json!([])), Err(..))); - } - - #[test] - fn try_from_json_array_too_many_elements() { - let user_id = get_random_user_id(); - assert!(matches!( - UserId::try_from(json!([user_id.to_string(), user_id.to_string()])), - Err(..) - )); - } - - #[test] - fn try_from_json_dict() { - let user_id = get_random_user_id(); - assert_eq!( - UserId::try_from(json!(HashMap::from([("tower_id", user_id.to_string())]))), - Ok(user_id) - ); - assert_eq!( - UserId::try_from(json!(HashMap::from([("user_id", user_id.to_string())]))), - Ok(user_id) - ); - } - - #[test] - fn try_from_json_empty_dict() { - assert!(matches!( - UserId::try_from(json!(HashMap::::new())), - Err(..) - )); - } - - #[test] - fn try_from_json_wrong_dict() { - let user_id = get_random_user_id(); - assert!(matches!( - UserId::try_from(json!(HashMap::from([("random_key", user_id.to_string())]))), - Err(..) - )); - } - - #[test] - fn try_from_json_dict_too_many_keys() { - let user_id = get_random_user_id(); - - assert!(matches!( - UserId::try_from(json!(HashMap::from([ - ("tower_id", user_id.to_string()), - ("user_id", user_id.to_string()) - ]))), - Err(..) - )); - } -} diff --git a/teos-common/src/net/http.rs b/teos-common/src/net/http.rs deleted file mode 100644 index f982bd9..0000000 --- a/teos-common/src/net/http.rs +++ /dev/null @@ -1,29 +0,0 @@ -pub enum Endpoint { - Register, - AddAppointment, - GetAppointment, - GetSubscriptionInfo, - Ping, -} - -impl std::fmt::Display for Endpoint { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Endpoint::Register => "register", - Endpoint::AddAppointment => "add_appointment", - Endpoint::GetAppointment => "get_appointment", - Endpoint::GetSubscriptionInfo => "get_subscription_info", - Endpoint::Ping => "ping", - } - ) - } -} - -impl Endpoint { - pub fn path(&self) -> String { - format!("/{self}") - } -} diff --git a/teos-common/src/net/mod.rs b/teos-common/src/net/mod.rs deleted file mode 100644 index f9fd9fd..0000000 --- a/teos-common/src/net/mod.rs +++ /dev/null @@ -1,116 +0,0 @@ -pub mod http; - -use serde::Serialize; -use std::fmt; - -/// Represents all types of teos network addresses -#[derive(Clone, Serialize, Debug, PartialEq, Eq)] -pub enum AddressType { - IpV4 = 0, - TorV3 = 1, -} - -impl From for AddressType { - fn from(x: i32) -> Self { - match x { - 0 => AddressType::IpV4, - 1 => AddressType::TorV3, - x => panic!("Unknown address type {}", x), - } - } -} - -impl std::str::FromStr for AddressType { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "ipv4" => Ok(AddressType::IpV4), - "torv3" => Ok(AddressType::TorV3), - _ => Err(format!("Unknown type: {s}")), - } - } -} - -impl fmt::Display for AddressType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let s = match self { - AddressType::IpV4 => "ipv4", - AddressType::TorV3 => "torv3", - }; - write!(f, "{s}") - } -} - -impl AddressType { - pub fn get_type(net_addr: &str) -> AddressType { - if net_addr.contains(".onion:") { - AddressType::TorV3 - } else { - AddressType::IpV4 - } - } - - pub fn is_tor(&self) -> bool { - self == &AddressType::TorV3 - } - - pub fn is_clearnet(&self) -> bool { - self == &AddressType::IpV4 - } -} - -#[derive(Clone, Serialize, Debug, PartialEq, Eq)] -pub struct NetAddr { - net_addr: String, - #[serde(skip)] - addr_type: AddressType, -} - -impl NetAddr { - pub fn new(net_addr: String) -> Self { - NetAddr { - addr_type: AddressType::get_type(&net_addr), - net_addr, - } - } - - pub fn net_addr(&self) -> &str { - &self.net_addr - } - - pub fn addr_type(&self) -> &AddressType { - &self.addr_type - } - - pub fn is_onion(&self) -> bool { - self.addr_type().is_tor() - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - - pub const TORV3_ADDR: &str = - "recnedb7xfhzjdrcgxongzli3a6qyrv5jwgowoho3v5g3rwk7kkglrid.onion:9814"; - pub const IPV4_ADDR: &str = "teos.talaia.watch:9814"; - - #[test] - fn test_get_type() { - assert_eq!(AddressType::get_type(TORV3_ADDR), AddressType::TorV3); - assert_eq!(AddressType::get_type(IPV4_ADDR), AddressType::IpV4); - } - - #[test] - fn test_is_tor() { - assert!(NetAddr::new(TORV3_ADDR.to_owned()).addr_type.is_tor()); - assert!(!NetAddr::new(IPV4_ADDR.to_owned()).addr_type.is_tor()); - } - - #[test] - fn test_is_clearnet() { - assert!(!NetAddr::new(TORV3_ADDR.to_owned()).addr_type.is_clearnet()); - assert!(NetAddr::new(IPV4_ADDR.to_owned()).addr_type.is_clearnet()); - } -} diff --git a/teos-common/src/receipts.rs b/teos-common/src/receipts.rs index 5dc0e00..9ebf185 100644 --- a/teos-common/src/receipts.rs +++ b/teos-common/src/receipts.rs @@ -1,66 +1,27 @@ //! Receipts issued by towers and handed to users as commitment proof. -use serde::Serialize; - use bitcoin::secp256k1::SecretKey; 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. -/// - 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. -/// -/// 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 -/// before the tower wipes their data, then the tower can create a new receipt with (S, E') for E' > E instead of a second receipt (E, E'). -// Notice this only applies as long as there is no gap between the two subscriptions. -#[derive(Serialize, Debug, Eq, PartialEq, Clone)] +#[derive(Debug)] pub struct RegistrationReceipt { user_id: UserId, available_slots: u32, - subscription_start: u32, subscription_expiry: u32, - #[serde(rename = "subscription_signature")] signature: Option, } impl RegistrationReceipt { - pub fn new( - user_id: UserId, - available_slots: u32, - subscription_start: u32, - subscription_expiry: u32, - ) -> Self { + pub fn new(user_id: UserId, available_slots: u32, subscription_expiry: u32) -> Self { RegistrationReceipt { user_id, available_slots, - subscription_start, subscription_expiry, signature: None, } } - pub fn with_signature( - user_id: UserId, - available_slots: u32, - subscription_start: u32, - subscription_expiry: u32, - signature: String, - ) -> Self { - RegistrationReceipt { - user_id, - available_slots, - subscription_start, - subscription_expiry, - signature: Some(signature), - } - } - pub fn user_id(&self) -> UserId { self.user_id } @@ -69,10 +30,6 @@ impl RegistrationReceipt { self.available_slots } - pub fn subscription_start(&self) -> u32 { - self.subscription_start - } - pub fn subscription_expiry(&self) -> u32 { self.subscription_expiry } @@ -81,33 +38,21 @@ impl RegistrationReceipt { self.signature.clone() } - pub fn to_vec(&self) -> Vec { + pub fn serialize(&self) -> Vec { let mut ser = Vec::new(); - ser.extend_from_slice(&self.user_id.to_vec()); + ser.extend_from_slice(&self.user_id.serialize()); ser.extend_from_slice(&self.available_slots.to_be_bytes()); - ser.extend_from_slice(&self.subscription_start.to_be_bytes()); ser.extend_from_slice(&self.subscription_expiry.to_be_bytes()); ser } pub fn sign(&mut self, sk: &SecretKey) { - self.signature = Some(cryptography::sign(&self.to_vec(), sk)); - } - - pub fn verify(&self, id: &UserId) -> bool { - if let Some(signature) = self.signature() { - cryptography::verify(&self.to_vec(), &signature, &id.0) - } else { - false - } + // TODO: Check if there's any case where this can actually fail. Don't unwrap if so. + self.signature = Some(cryptography::sign(&self.serialize(), sk).unwrap()); } } - -/// Proof that a certain state was backed up with the tower. -/// -/// Appointment receipts can be used alongside a registration receipt that covers it, and on chain data (a breach not being reacted with a penalty), to prove a tower has not reacted to a channel breach. -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug)] pub struct AppointmentReceipt { user_signature: String, start_block: u32, @@ -123,14 +68,6 @@ impl AppointmentReceipt { } } - pub fn with_signature(user_signature: String, start_block: u32, signature: String) -> Self { - AppointmentReceipt { - user_signature, - start_block, - signature: Some(signature), - } - } - pub fn user_signature(&self) -> &str { &self.user_signature } @@ -143,7 +80,7 @@ impl AppointmentReceipt { self.signature.clone() } - pub fn to_vec(&self) -> Vec { + pub fn serialize(&self) -> Vec { let mut ser = Vec::new(); ser.extend_from_slice(self.user_signature.as_bytes()); ser.extend_from_slice(&self.start_block.to_be_bytes()); @@ -152,14 +89,7 @@ impl AppointmentReceipt { } pub fn sign(&mut self, sk: &SecretKey) { - self.signature = Some(cryptography::sign(&self.to_vec(), sk)); - } - - pub fn verify(&self, id: &UserId) -> bool { - if let Some(signature) = self.signature() { - cryptography::verify(&self.to_vec(), &signature, &id.0) - } else { - false - } + // TODO: Check if there's any case where this can actually fail. Don't unwrap if so. + self.signature = Some(cryptography::sign(&self.serialize(), sk).unwrap()); } } diff --git a/teos-common/src/ser.rs b/teos-common/src/ser.rs deleted file mode 100644 index 4946ba3..0000000 --- a/teos-common/src/ser.rs +++ /dev/null @@ -1,146 +0,0 @@ -use std::collections::HashSet; - -use serde::{ser::SerializeSeq, Serializer}; - -use crate::appointment::Locator; - -pub fn serialize_locators(hs: &HashSet, s: S) -> Result -where - S: Serializer, -{ - let mut seq = s.serialize_seq(Some(hs.len()))?; - for element in hs.iter() { - seq.serialize_element(&hex::encode(element))?; - } - seq.end() -} - -pub mod serde_be { - use super::*; - use serde::de::{self, Deserializer}; - - pub fn serialize(v: &[u8], s: S) -> Result - where - S: Serializer, - { - let mut v = v.to_owned(); - v.reverse(); - hex::serialize(v, s) - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - struct BEVisitor; - - impl<'de> de::Visitor<'de> for BEVisitor { - type Value = Vec; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a hex encoded string") - } - - fn visit_str(self, v: &str) -> Result - where - E: de::Error, - { - let mut v = - hex::decode(v).map_err(|_| E::custom("cannot deserialize the given value"))?; - v.reverse(); - Ok(v) - } - } - - deserializer.deserialize_any(BEVisitor) - } -} - -pub mod serde_vec_bytes { - use super::*; - use serde::de::{self, Deserializer, SeqAccess}; - - pub fn serialize(v: &[Vec], s: S) -> Result - where - S: Serializer, - { - let mut seq = s.serialize_seq(Some(v.len()))?; - for element in v.iter() { - seq.serialize_element(&hex::encode(element))?; - } - seq.end() - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> - where - D: Deserializer<'de>, - { - struct VecVisitor; - - impl<'de> de::Visitor<'de> for VecVisitor { - type Value = Vec>; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a hex encoded string") - } - - fn visit_seq(self, mut seq: A) -> Result - where - A: SeqAccess<'de>, - { - let mut result = Vec::new(); - while let Some(v) = seq.next_element::()? { - result - .push(hex::decode(v).map_err(|_| { - de::Error::custom("cannot deserialize the given value") - })?); - } - - Ok(result) - } - } - - deserializer.deserialize_any(VecVisitor) - } -} - -pub mod serde_status { - use super::*; - use serde::de::{self, Deserializer}; - use std::str::FromStr; - - use crate::appointment::AppointmentStatus; - - pub fn serialize(status: &i32, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&AppointmentStatus::from(*status).to_string()) - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct StatusVisitor; - - impl<'de> de::Visitor<'de> for StatusVisitor { - type Value = i32; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a string containing the status") - } - - fn visit_str(self, v: &str) -> Result - where - E: de::Error, - { - let status = AppointmentStatus::from_str(v) - .map_err(|_| E::custom("given status is unknown"))?; - Ok(status as i32) - } - } - - deserializer.deserialize_any(StatusVisitor) - } -} diff --git a/teos-common/src/test_utils.rs b/teos-common/src/test_utils.rs deleted file mode 100644 index 4bd5cb9..0000000 --- a/teos-common/src/test_utils.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::convert::TryInto; - -use bitcoin::script::PushBytesBuf; -use hex::FromHex; -use rand::distributions::Standard; -use rand::prelude::Distribution; -use rand::Rng; - -use bitcoin::hashes::Hash; -use bitcoin::secp256k1::SecretKey; -use bitcoin::{consensus, Amount, ScriptBuf, Transaction, TxOut, Txid}; - -use crate::appointment::{Appointment, Locator}; -use crate::cryptography; -use crate::receipts::{AppointmentReceipt, RegistrationReceipt}; -use crate::UserId; - -pub static TXID_HEX: &str = "338bda693c4a26e0d41a01f7f2887aaf48bf0bdf93e6415c9110b29349349d3e"; -pub static TX_HEX: &str = "010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff54038e830a1b4d696e656420627920416e74506f6f6c373432c2005b005e7a0ae3fabe6d6d7841cd582ead8ea5dd8e3de1173cae6fcd2a53c7362ebb7fb6f815604fe07cbe0200000000000000ac0e060005f90000ffffffff04d9476026000000001976a91411dbe48cc6b617f9c6adaf4d9ed5f625b1c7cb5988ac0000000000000000266a24aa21a9ed7248c6efddd8d99bfddd7f499f0b915bffa8253003cc934df1ff14a81301e2340000000000000000266a24b9e11b6d7054937e13f39529d6ad7e685e9dd4efa426f247d5f5a5bed58cdddb2d0fa60100000000000000002b6a2952534b424c4f434b3a054a68aa5368740e8b3e3c67bce45619c2cfd07d4d4f0936a5612d2d0034fa0a0120000000000000000000000000000000000000000000000000000000000000000000000000"; - -pub fn get_random_int() -> T -where - Standard: Distribution, -{ - let mut rng = rand::thread_rng(); - rng.gen() -} - -pub fn get_random_user_id() -> UserId { - let (_, pk) = cryptography::get_random_keypair(); - - 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, - None => { - let prev_txid_bytes = cryptography::get_random_bytes(32); - Txid::from_slice(&prev_txid_bytes).unwrap() - } - }; - - let tx_bytes = Vec::from_hex(TX_HEX).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]); - let locator = Locator::from_slice(&raw_locator).unwrap(); - - let encrypted_blob = cryptography::encrypt(&penalty_tx, &dispute_txid).unwrap(); - Appointment::new(locator, encrypted_blob, get_random_int()) -} - -pub fn get_random_registration_receipt() -> RegistrationReceipt { - let (sk, _) = cryptography::get_random_keypair(); - let start = get_random_int(); - let mut receipt = - RegistrationReceipt::new(get_random_user_id(), get_random_int(), start, start + 420); - receipt.sign(&sk); - - receipt -} - -pub fn get_registration_receipt_from_previous(r: &RegistrationReceipt) -> RegistrationReceipt { - let (sk, _) = cryptography::get_random_keypair(); - let mut receipt = RegistrationReceipt::new( - r.user_id(), - r.available_slots() + 1 + get_random_int::() as u32, - r.subscription_start(), - r.subscription_expiry() + 1 + get_random_int::() as u32, - ); - receipt.sign(&sk); - - receipt -} - -pub fn get_random_appointment_receipt(tower_sk: SecretKey) -> AppointmentReceipt { - let mut receipt = AppointmentReceipt::new("user_sig".into(), 42); - receipt.sign(&tower_sk); - - receipt -} diff --git a/teos/Cargo.toml b/teos/Cargo.toml index 287b0aa..68c2170 100644 --- a/teos/Cargo.toml +++ b/teos/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "teos" -version = "0.2.0" +version = "0.0.1" authors = ["Sergi Delgado Segura "] license = "MIT" -edition = "2021" +edition = "2018" default-run="teosd" [[bin]] @@ -19,36 +19,33 @@ path = "src/main.rs" hex = { version = "0.4.3", features = [ "serde" ] } home = "0.5.3" log = "0.4" -prost = "0.12" -rcgen = { version = "0.13.1", features = ["pem", "x509-parser"] } +prost = "0.9" 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.11", features = [ "tls", "transport" ] } +tonic = "0.6" tokio = { version = "1.5", features = [ "rt-multi-thread" ] } triggered = "0.1.2" -warp = "0.3.5" -torut = "0.2.1" -base64 = "0.22.1" +warp = "0.3.2" # Bitcoin and Lightning -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" ] } +bitcoin = { version = "0.27", features = [ "base64" ] } +bitcoincore-rpc = "0.14.0" +lightning = "0.0.104" +lightning-net-tokio = "0.0.104" +lightning-block-sync = { version = "0.0.104", features = [ "rpc-client" ] } # Local teos-common = { path = "../teos-common" } [build-dependencies] -tonic-build = "0.11" +tonic-build = "0.6" [dev-dependencies] -jsonrpc-http-server = "17.1.0" +chunked_transfer = "1.4" rand = "0.8.4" -tempdir = "0.3.7" -tokio-stream = { version = "0.1.5", features = [ "net" ] } +jsonrpc-http-server = "17.1.0" +tokio-stream = { version = "0.1.5", features = [ "net" ] } \ No newline at end of file diff --git a/teos/build.rs b/teos/build.rs index aa9031f..0e5cc8f 100644 --- a/teos/build.rs +++ b/teos/build.rs @@ -1,28 +1,37 @@ fn main() -> Result<(), Box> { tonic_build::configure() - .extern_path(".common.teos.v2", "::teos-common::protos") .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") + .type_attribute("AppointmentData.appointment_data", "#[serde(untagged)]") + .field_attribute("AppointmentData.appointment_data", "#[serde(flatten)]") + .field_attribute("appointment_data", "#[serde(rename = \"appointment\")]") .field_attribute("user_id", "#[serde(with = \"hex::serde\")]") .field_attribute("tower_id", "#[serde(with = \"hex::serde\")]") + .field_attribute("locator", "#[serde(with = \"hex::serde\")]") + .field_attribute("encrypted_blob", "#[serde(with = \"hex::serde\")]") + .field_attribute("tx", "#[serde(with = \"hex::serde\")]") + .field_attribute( + "locators", + "#[serde(serialize_with = \"crate::api::http::serialize_vec_bytes\")]", + ) .field_attribute( "user_ids", - "#[serde(serialize_with = \"teos_common::ser::serde_vec_bytes::serialize\")]", + "#[serde(serialize_with = \"crate::api::http::serialize_vec_bytes\")]", ) .field_attribute( "GetUserResponse.appointments", - "#[serde(serialize_with = \"teos_common::ser::serde_vec_bytes::serialize\")]", + "#[serde(serialize_with = \"crate::api::http::serialize_vec_bytes\")]", ) .field_attribute( - "NetworkAddress.address_type", - "#[serde(rename = \"type\", with = \"crate::api::serde::serde_address_type\")]", + "GetAppointmentResponse.status", + "#[serde(with = \"crate::api::serde_status\")]", ) .compile( &[ - "proto/teos/v2/appointment.proto", - "proto/teos/v2/tower_services.proto", - "proto/teos/v2/user.proto", + "proto/teos/appointment.proto", + "proto/teos/tower_services.proto", + "proto/teos/user.proto", ], - &["proto/teos/v2", "../teos-common/proto/"], + &["proto/teos"], )?; Ok(()) diff --git a/teos/proto/teos/appointment.proto b/teos/proto/teos/appointment.proto new file mode 100644 index 0000000..a0b1f24 --- /dev/null +++ b/teos/proto/teos/appointment.proto @@ -0,0 +1,81 @@ +syntax = "proto3"; +package teos.v2; + +message Appointment { + /* + Contains the basic information about an appointment (Watcher) and it's used for messages like + AddAppointmentRequest or encapsulated inside AppointmentData for GetAppointmentResponse + */ + + bytes locator = 1; + bytes encrypted_blob = 2; + uint32 to_self_delay = 3; + +} + +message Tracker { + // It's the equivalent of an appointment message from data held by the Responder. + + bytes dispute_txid = 1; + bytes penalty_txid = 2; + bytes penalty_rawtx = 3; +} + +message AppointmentData { + /* + Encapsulates the data for a GetAppointmentResponse, given it can be an appointment (data is on the Watcher) or a + tracker (data is on the Responder). + */ + + oneof appointment_data { + Appointment appointment = 1; + Tracker tracker = 2; + } +} + +message AddAppointmentRequest { + // Request to add an appointment to the backend, contains the appointment data and the user signature. + + Appointment appointment = 1; + string signature = 2; +} + +message AddAppointmentResponse { + /* + Response to an AddAppointmentRequest, contains the locator to identify the added appointment, the tower signature, + the block at which the tower has started (or will start) watching for the appointment, and the updated subscription + information. + */ + + bytes locator = 1; + uint32 start_block = 2; + string signature = 3; + uint32 available_slots = 4; + uint32 subscription_expiry = 5; +} + +message GetAppointmentRequest { + // Request to get information about an appointment. Contains the appointment locator and a signature by the user. + + bytes locator = 1; + string signature = 2; +} + +message GetAppointmentResponse { + // Response to a GetAppointmentRequest. Contains the appointment data encapsulated in an AppointmentData message. + + AppointmentData appointment_data = 1; + enum AppointmentStatus { + NOT_FOUND = 0; + BEING_WATCHED = 1; + DISPUTE_RESPONDED = 2; + + } + AppointmentStatus status = 2; +} + +message GetAllAppointmentsResponse { + // Response with data about all the appointments in the tower. + + repeated AppointmentData appointments = 1; +} \ No newline at end of file diff --git a/teos/proto/teos/v2/tower_services.proto b/teos/proto/teos/tower_services.proto similarity index 52% rename from teos/proto/teos/v2/tower_services.proto rename to teos/proto/teos/tower_services.proto index 92fa881..4fc4c28 100644 --- a/teos/proto/teos/v2/tower_services.proto +++ b/teos/proto/teos/tower_services.proto @@ -3,46 +3,31 @@ package teos.v2; import "appointment.proto"; import "user.proto"; -import "common/teos/v2/appointment.proto"; -import "common/teos/v2/user.proto"; import "google/protobuf/empty.proto"; -message NetworkAddress { - // Tower public API endpoint. - enum AddressType { - IpV4 = 0; - TorV3 = 1; - } - AddressType address_type = 1; - string address = 2; - uint32 port = 3; - -} - message GetTowerInfoResponse { // Response with information about the tower. + bytes tower_id = 1; uint32 n_registered_users = 2; uint32 n_watcher_appointments = 3; uint32 n_responder_trackers = 4; bool bitcoind_reachable = 5; - repeated NetworkAddress addresses = 6; } service PublicTowerServices { // Public tower services, only reachable from the public API. - rpc register(common.teos.v2.RegisterRequest) returns (common.teos.v2.RegisterResponse) {} - rpc add_appointment(common.teos.v2.AddAppointmentRequest) returns (common.teos.v2.AddAppointmentResponse) {} - rpc get_appointment(common.teos.v2.GetAppointmentRequest) returns (common.teos.v2.GetAppointmentResponse) {} - rpc get_subscription_info(common.teos.v2.GetSubscriptionInfoRequest) returns (common.teos.v2.GetSubscriptionInfoResponse) {} + rpc register(RegisterRequest) returns (RegisterResponse) {} + rpc add_appointment(AddAppointmentRequest) returns (AddAppointmentResponse) {} + rpc get_appointment(GetAppointmentRequest) returns (GetAppointmentResponse) {} + rpc get_subscription_info(GetSubscriptionInfoRequest) returns (GetSubscriptionInfoResponse) {} } service PrivateTowerServices { // Private tower services, only reachable from the private API. rpc get_all_appointments(google.protobuf.Empty) returns (GetAllAppointmentsResponse) {} - rpc get_appointments(GetAppointmentsRequest) returns (GetAppointmentsResponse) {} rpc get_tower_info(google.protobuf.Empty) returns (GetTowerInfoResponse) {} rpc get_users(google.protobuf.Empty) returns (GetUsersResponse) {} rpc get_user(GetUserRequest) returns (GetUserResponse) {} diff --git a/teos/proto/teos/user.proto b/teos/proto/teos/user.proto new file mode 100644 index 0000000..5cef3e2 --- /dev/null +++ b/teos/proto/teos/user.proto @@ -0,0 +1,51 @@ +syntax = "proto3"; +package teos.v2; + +message RegisterRequest { + // Requests a user registration with the tower. Contains the user id in the form of a compressed ECDSA public key. + + bytes user_id = 1; +} + +message RegisterResponse { + // Response to a RegisterRequest, contains the registration information alongside the tower signature of the agreement. + + bytes user_id = 1; + uint32 available_slots = 2; + uint32 subscription_expiry = 3; + string subscription_signature = 4; +} + +message GetUserRequest { + // Request to get information about a specific user. Contains the user id. + + bytes user_id = 1; +} + +message GetUserResponse { + // Response with the information the tower has about a specific user + + uint32 available_slots = 1; + uint32 subscription_expiry = 2; + repeated bytes appointments = 3; +} + +message GetUsersResponse { + // Response with information about all the users registered with the tower. Contains a list of user ids. + + repeated bytes user_ids = 1; +} + +message GetSubscriptionInfoRequest { + // Request to get a specific user's subscription info. + + string signature = 1; +} + +message GetSubscriptionInfoResponse { + // Response with the information the tower has about a specific user + + uint32 available_slots = 1; + uint32 subscription_expiry = 2; + repeated bytes locators = 3; +} \ No newline at end of file diff --git a/teos/proto/teos/v2/appointment.proto b/teos/proto/teos/v2/appointment.proto deleted file mode 100644 index 67c6679..0000000 --- a/teos/proto/teos/v2/appointment.proto +++ /dev/null @@ -1,22 +0,0 @@ -syntax = "proto3"; -package teos.v2; - -import "common/teos/v2/appointment.proto"; - -message GetAppointmentsRequest { - // Request the information of appointments with specific locator. - - bytes locator = 1; -} - -message GetAppointmentsResponse { - // Response with the information of all appointments with a specific locator. - - repeated common.teos.v2.AppointmentData appointments = 1; -} - -message GetAllAppointmentsResponse { - // Response with data about all the appointments in the tower. - - repeated common.teos.v2.AppointmentData appointments = 1; -} \ No newline at end of file diff --git a/teos/proto/teos/v2/user.proto b/teos/proto/teos/v2/user.proto deleted file mode 100644 index 36e2a3f..0000000 --- a/teos/proto/teos/v2/user.proto +++ /dev/null @@ -1,22 +0,0 @@ -syntax = "proto3"; -package teos.v2; - -message GetUserRequest { - // Request to get information about a specific user. Contains the user id. - - bytes user_id = 1; -} - -message GetUserResponse { - // Response with the information the tower has about a specific user - - uint32 available_slots = 1; - uint32 subscription_expiry = 2; - repeated bytes appointments = 3; -} - -message GetUsersResponse { - // Response with information about all the users registered with the tower. Contains a list of user ids. - - repeated bytes user_ids = 1; -} \ No newline at end of file diff --git a/teos/src/api/http.rs b/teos/src/api/http.rs index 949c08b..60ec315 100644 --- a/teos/src/api/http.rs +++ b/teos/src/api/http.rs @@ -1,17 +1,15 @@ -use serde::{Deserialize, Serialize}; +use serde::{ser::SerializeSeq, Deserialize, Serialize, Serializer}; use std::convert::Infallible; use std::error::Error; use std::net::SocketAddr; -use tokio::time::Duration; use tonic::transport::Channel; -use triggered::{Listener, Trigger}; +use triggered::Listener; use warp::{http::StatusCode, reject, reply, Filter, Rejection, Reply}; use teos_common::appointment::LOCATOR_LEN; -use teos_common::net::http::Endpoint; -use teos_common::protos as common_msgs; use teos_common::{errors, USER_ID_LEN}; +use crate::protos as msgs; use crate::protos::public_tower_services_client::PublicTowerServicesClient; // TODO: Limit the body length for /add_appointment should not be needed, since slots are consumed proportionally to it. @@ -36,14 +34,14 @@ impl ApiError { fn missing_field(field_name: &str) -> Rejection { reject::custom(Self::new( - format!("missing field `{field_name}`"), + format!("missing field `{}`", field_name), errors::MISSING_FIELD, )) } fn empty_field(field_name: &str) -> Rejection { reject::custom(Self::new( - format!("`{field_name}` field is empty"), + format!("`{}` field is empty", field_name), errors::EMPTY_FIELD, )) } @@ -51,13 +49,25 @@ impl ApiError { fn wrong_field_length(field_name: &str, field_size: usize, expected_size: usize) -> Rejection { reject::custom(Self::new( format!( - "Wrong `{field_name}` field size. Expected {expected_size}, received {field_size}" + "Wrong `{}` field size. Expected {}, received {}", + field_name, expected_size, field_size ), errors::WRONG_FIELD_SIZE, )) } } +pub fn serialize_vec_bytes(v: &[Vec], s: S) -> Result +where + S: Serializer, +{ + let mut seq = s.serialize_seq(Some(v.len()))?; + for element in v.iter() { + seq.serialize_element(&hex::encode(element))?; + } + seq.end() +} + fn with_grpc( grpc_endpoint: PublicTowerServicesClient, ) -> impl Filter,), Error = Infallible> + Clone { @@ -97,13 +107,13 @@ fn parse_grpc_response( match result { Ok(r) => { let inner = r.into_inner(); - log::debug!("Request succeeded"); + log::info!("Request succeeded"); log::debug!("Response: {}", serde_json::json!(inner)); (reply::json(&inner), StatusCode::OK) } Err(s) => { let (status_code, error_code) = match_status(&s); - log::debug!("Request failed, error_code={error_code}"); + log::info!("Request failed, error_code={}", error_code); log::debug!("Response: {}", serde_json::json!(s.message())); ( reply::json(&ApiError::new(s.message().into(), error_code)), @@ -114,14 +124,14 @@ fn parse_grpc_response( } async fn register( - req: common_msgs::RegisterRequest, + req: msgs::RegisterRequest, addr: Option, mut grpc_conn: PublicTowerServicesClient, ) -> std::result::Result { - log::debug!( - "Received a register request from {}", - addr.map_or("an unknown address".to_owned(), |a| a.to_string()) - ); + match addr { + Some(a) => log::info!("Received register request from {}", a), + None => log::info!("Received register request from unknown address"), + } let user_id = req.user_id.clone(); if user_id.is_empty() { @@ -140,14 +150,14 @@ async fn register( } async fn add_appointment( - req: common_msgs::AddAppointmentRequest, + req: msgs::AddAppointmentRequest, addr: Option, mut grpc_conn: PublicTowerServicesClient, ) -> std::result::Result { - log::debug!( - "Received an add_appointment request from {}", - addr.map_or("an unknown address".to_owned(), |a| a.to_string()) - ); + match addr { + Some(a) => log::info!("Received add_appointment request from {}", a), + None => log::info!("Received add_appointment request from unknown address"), + } if let Some(a) = &req.appointment { if a.locator.is_empty() { @@ -172,14 +182,14 @@ async fn add_appointment( } async fn get_appointment( - req: common_msgs::GetAppointmentRequest, + req: msgs::GetAppointmentRequest, addr: Option, mut grpc_conn: PublicTowerServicesClient, ) -> std::result::Result { - log::debug!( - "Received an get_appointment request from {}", - addr.map_or("an unknown address".to_owned(), |a| a.to_string()) - ); + match addr { + Some(a) => log::info!("Received get_appointment request from {}", a), + None => log::info!("Received get_appointment request from unknown address"), + } if req.locator.is_empty() { return Err(ApiError::empty_field("locator")); @@ -200,14 +210,14 @@ async fn get_appointment( } async fn get_subscription_info( - req: common_msgs::GetSubscriptionInfoRequest, + req: msgs::GetSubscriptionInfoRequest, addr: Option, mut grpc_conn: PublicTowerServicesClient, ) -> std::result::Result { - log::debug!( - "Received an get_subscription_info request from {}", - addr.map_or("an unknown address".to_owned(), |a| a.to_string()) - ); + match addr { + Some(a) => log::info!("Received get_subscription_info request from {}", a), + None => log::info!("Received get_subscription_info request from unknown address"), + } if req.signature.is_empty() { return Err(ApiError::empty_field("signature")); @@ -217,40 +227,32 @@ 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 { +) -> impl Filter + Clone { let register = warp::post() - .and(warp::path(Endpoint::Register.to_string())) + .and(warp::path("register")) .and(warp::body::content_length_limit(REGISTER_BODY_LEN).and(warp::body::json())) .and(warp::addr::remote()) .and(with_grpc(grpc_conn.clone())) .and_then(register); let add_appointment = warp::post() - .and(warp::path(Endpoint::AddAppointment.to_string())) + .and(warp::path("add_appointment")) .and(warp::body::content_length_limit(ADD_APPOINTMENT_BODY_LEN).and(warp::body::json())) .and(warp::addr::remote()) .and(with_grpc(grpc_conn.clone())) .and_then(add_appointment); let get_appointment = warp::post() - .and(warp::path(Endpoint::GetAppointment.to_string())) + .and(warp::path("get_appointment")) .and(warp::body::content_length_limit(GET_APPOINTMENT_BODY_LEN).and(warp::body::json())) .and(warp::addr::remote()) .and(with_grpc(grpc_conn.clone())) .and_then(get_appointment); let get_subscription_info = warp::post() - .and(warp::path(Endpoint::GetSubscriptionInfo.to_string())) + .and(warp::path("get_subscription_info")) .and( warp::body::content_length_limit(GET_SUBSCRIPTION_INFO_BODY_LEN) .and(warp::body::json()), @@ -259,16 +261,10 @@ 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) } @@ -302,24 +298,10 @@ async fn handle_rejection(err: Rejection) -> Result { } } -pub async fn serve( - http_bind: SocketAddr, - grpc_bind: SocketAddr, - service_ready: Trigger, - shutdown_signal: Listener, -) { - let grpc_conn = loop { - match PublicTowerServicesClient::connect(format!("http://{grpc_bind}")).await { - Ok(conn) => break conn, - Err(_) => { - log::error!("Cannot connect to the gRPC server. Retrying shortly"); - tokio::time::sleep(Duration::from_secs(1)).await; - } - } - }; - let (_, server) = - warp::serve(router(grpc_conn)).bind_with_graceful_shutdown(http_bind, shutdown_signal); - service_ready.trigger(); +pub async fn serve(http_bind: SocketAddr, grpc_bind: String, shutdown_signal: Listener) { + let grpc_conn = PublicTowerServicesClient::connect(grpc_bind).await.unwrap(); + let (_, server) = warp::serve(router(grpc_conn)) + .bind_with_graceful_shutdown(http_bind, async { shutdown_signal.await }); server.await } @@ -335,7 +317,7 @@ mod test_helpers { use crate::api::internal::InternalAPI; use crate::protos::public_tower_services_server::PublicTowerServicesServer; - use crate::test_utils::{create_api_with_config, ApiConfig, BitcoindStopper}; + use crate::test_utils::{create_api_with_config, ApiConfig}; pub(crate) enum RequestBody<'a> { Jsonify(&'a str), @@ -346,8 +328,8 @@ mod test_helpers { pub(crate) async fn run_tower_in_background_with_config( api_config: ApiConfig, - ) -> (SocketAddr, Arc, BitcoindStopper) { - let (internal_rpc_api, bitcoind_stopper) = create_api_with_config(api_config).await; + ) -> (SocketAddr, Arc) { + let internal_rpc_api = create_api_with_config(api_config).await; let cloned = internal_rpc_api.clone(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); @@ -361,19 +343,18 @@ mod test_helpers { .unwrap(); }); - (addr, cloned, bitcoind_stopper) + (addr, cloned) } - pub(crate) async fn run_tower_in_background() -> (SocketAddr, BitcoindStopper) { - let (sock_addr, _, bitcoind_stopper) = - run_tower_in_background_with_config(ApiConfig::default()).await; - - (sock_addr, bitcoind_stopper) + pub(crate) async fn run_tower_in_background() -> SocketAddr { + run_tower_in_background_with_config(ApiConfig::default()) + .await + .0 } - pub(crate) async fn check_api_error( - endpoint: Endpoint, - body: RequestBody<'_>, + pub(crate) async fn check_api_error<'a>( + endpoint: &str, + body: RequestBody<'a>, server_addr: SocketAddr, ) -> (ApiError, StatusCode) { let grpc_conn = PublicTowerServicesClient::connect(format!( @@ -385,22 +366,15 @@ mod test_helpers { .unwrap(); let req = match body { - RequestBody::Json(j) => warp::test::request() - .method("POST") - .path(&endpoint.path()) - .json(&j), - RequestBody::DoNotJsonify(j) => warp::test::request() - .method("POST") - .path(&endpoint.path()) - .json(&j), + RequestBody::Json(j) => warp::test::request().method("POST").path(endpoint).json(&j), + RequestBody::DoNotJsonify(j) => { + warp::test::request().method("POST").path(endpoint).json(&j) + } RequestBody::Jsonify(j) => warp::test::request() .method("POST") - .path(&endpoint.path()) + .path(endpoint) .json(&serde_json::from_str::(j).unwrap()), - RequestBody::Body(b) => warp::test::request() - .method("POST") - .path(&endpoint.path()) - .body(b), + RequestBody::Body(b) => warp::test::request().method("POST").path(endpoint).body(b), }; let res = req.reply(&router(grpc_conn)).await; @@ -411,7 +385,7 @@ mod test_helpers { } pub(crate) async fn request_to_api( - endpoint: Endpoint, + endpoint: &str, body: B, server_addr: SocketAddr, ) -> Result @@ -429,7 +403,7 @@ mod test_helpers { let res = warp::test::request() .method("POST") - .path(&endpoint.path()) + .path(endpoint) .json(&serde_json::json!(body)) .reply(&router(grpc_conn)) .await; @@ -440,16 +414,16 @@ mod test_helpers { #[cfg(test)] mod tests_failures { - use super::test_helpers::{check_api_error, run_tower_in_background, RequestBody}; use super::*; - use teos_common::test_utils::get_random_user_id; + use super::test_helpers::{check_api_error, run_tower_in_background, RequestBody}; + use crate::test_utils::get_random_user_id; #[tokio::test] async fn test_no_json_request_body() { - let (server_addr, _s) = run_tower_in_background().await; + let server_addr = run_tower_in_background().await; let (api_error, status) = - check_api_error(Endpoint::Register, RequestBody::Body(""), server_addr).await; + check_api_error("/register", RequestBody::Body(""), server_addr).await; assert!(api_error.error.contains("EOF while parsing")); assert_eq!(api_error.error_code, errors::INVALID_REQUEST_FORMAT); assert_eq!(status, StatusCode::BAD_REQUEST); @@ -457,13 +431,9 @@ mod tests_failures { #[tokio::test] async fn test_wrong_json_request_body() { - let (server_addr, _s) = run_tower_in_background().await; - let (api_error, status) = check_api_error( - Endpoint::Register, - RequestBody::DoNotJsonify(""), - server_addr, - ) - .await; + let server_addr = run_tower_in_background().await; + let (api_error, status) = + check_api_error("/register", RequestBody::DoNotJsonify(""), server_addr).await; assert!(api_error.error.contains("expected struct")); assert_eq!(api_error.error_code, errors::WRONG_FIELD_TYPE); assert_eq!(status, StatusCode::BAD_REQUEST); @@ -471,13 +441,9 @@ mod tests_failures { #[tokio::test] async fn test_empty_json_request_body() { - let (server_addr, _s) = run_tower_in_background().await; - let (api_error, status) = check_api_error( - Endpoint::Register, - RequestBody::Jsonify(r#"{}"#), - server_addr, - ) - .await; + let server_addr = run_tower_in_background().await; + let (api_error, status) = + check_api_error("/register", RequestBody::Jsonify(r#"{}"#), server_addr).await; assert!(api_error.error.contains("missing field")); assert_eq!(api_error.error_code, errors::MISSING_FIELD); assert_eq!(status, StatusCode::BAD_REQUEST); @@ -485,9 +451,9 @@ mod tests_failures { #[tokio::test] async fn test_empty_field() { - let (server_addr, _s) = run_tower_in_background().await; + let server_addr = run_tower_in_background().await; let (api_error, status) = check_api_error( - Endpoint::Register, + "/register", RequestBody::Jsonify(r#"{"user_id": ""}"#), server_addr, ) @@ -499,9 +465,9 @@ mod tests_failures { #[tokio::test] async fn test_wrong_field_hex_encoding_odd() { - let (server_addr, _s) = run_tower_in_background().await; + let server_addr = run_tower_in_background().await; let (api_error, status) = check_api_error( - Endpoint::Register, + "/register", RequestBody::Jsonify(r#"{"user_id": "a"}"#), server_addr, ) @@ -513,9 +479,9 @@ mod tests_failures { #[tokio::test] async fn test_wrong_hex_encoding_character() { - let (server_addr, _s) = run_tower_in_background().await; + let server_addr = run_tower_in_background().await; let (api_error, status) = - check_api_error(Endpoint::Register, + check_api_error("/register", RequestBody::Jsonify(r#"{"user_id": "022fa2900ed7fc07b4e8ca3ea081e846245b0497944644aa78ea0b994ac22074dZ"}"#), server_addr ).await; @@ -527,9 +493,9 @@ mod tests_failures { #[tokio::test] async fn test_wrong_field_size() { - let (server_addr, _s) = run_tower_in_background().await; + let server_addr = run_tower_in_background().await; let (api_error, status) = check_api_error( - Endpoint::Register, + "/register", RequestBody::Jsonify(r#"{"user_id": "aa"}"#), server_addr, ) @@ -542,9 +508,9 @@ mod tests_failures { #[tokio::test] async fn test_wrong_field_type() { - let (server_addr, _s) = run_tower_in_background().await; + let server_addr = run_tower_in_background().await; let (api_error, status) = check_api_error( - Endpoint::Register, + "/register", RequestBody::DoNotJsonify(r#"{"user_id": 1}"#), server_addr, ) @@ -557,9 +523,9 @@ mod tests_failures { #[tokio::test] async fn test_request_missing_field() { // We'll use a different endpoint here since we need a json object with more than one field - let (server_addr, _s) = run_tower_in_background().await; + let server_addr = run_tower_in_background().await; let (api_error, status) = check_api_error( - Endpoint::AddAppointment, + "/add_appointment", RequestBody::Jsonify(r#"{"signature": "aa"}"#), server_addr, ) @@ -573,7 +539,7 @@ mod tests_failures { #[tokio::test] async fn test_empty_request_body() { - let (server_addr, _s) = run_tower_in_background().await; + let server_addr = run_tower_in_background().await; let grpc_conn = PublicTowerServicesClient::connect(format!( "http://{}:{}", server_addr.ip(), @@ -584,16 +550,16 @@ mod tests_failures { let res = warp::test::request() .method("POST") - .path(&Endpoint::Register.path()) + .path("/register") .reply(&router(grpc_conn)) .await; - assert_eq!(res.status(), StatusCode::LENGTH_REQUIRED); + assert_eq!(res.status(), StatusCode::LENGTH_REQUIRED) } #[tokio::test] async fn test_payload_too_large() { - let (server_addr, _s) = run_tower_in_background().await; + let server_addr = run_tower_in_background().await; let grpc_conn = PublicTowerServicesClient::connect(format!( "http://{}:{}", server_addr.ip(), @@ -604,17 +570,17 @@ mod tests_failures { let res = warp::test::request() .method("POST") - .path(&Endpoint::Register.path()) + .path("/register") .json(&format!("{}{}", get_random_user_id(), get_random_user_id())) .reply(&router(grpc_conn)) .await; - assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE) } #[tokio::test] async fn test_wrong_endpoint() { - let (server_addr, _s) = run_tower_in_background().await; + let server_addr = run_tower_in_background().await; let grpc_conn = PublicTowerServicesClient::connect(format!( "http://{}:{}", server_addr.ip(), @@ -625,16 +591,17 @@ mod tests_failures { let res = warp::test::request() .method("POST") + .path("/") .json(&"") .reply(&router(grpc_conn)) .await; - assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED); + assert_eq!(res.status(), StatusCode::NOT_FOUND) } #[tokio::test] async fn test_wrong_method() { - let (server_addr, _s) = run_tower_in_background().await; + let server_addr = run_tower_in_background().await; let grpc_conn = PublicTowerServicesClient::connect(format!( "http://{}:{}", server_addr.ip(), @@ -644,57 +611,54 @@ mod tests_failures { .unwrap(); let res = warp::test::request() + .path("/") .json(&"") .reply(&router(grpc_conn)) .await; - assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED); + assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED) } } #[cfg(test)] mod tests_methods { + use super::*; + use super::test_helpers::{ check_api_error, request_to_api, run_tower_in_background, run_tower_in_background_with_config, RequestBody, }; - use super::*; - - use crate::responder::{ConfirmationStatus, TransactionTracker}; + use crate::extended_appointment::UUID; use crate::test_utils::{ - generate_dummy_appointment, get_random_tx, ApiConfig, DURATION, SLOTS, + generate_dummy_appointment, get_random_user_id, ApiConfig, DURATION, SLOTS, }; - use crate::watcher::Breach; - - use teos_common::test_utils::get_random_user_id; use teos_common::{cryptography, UserId}; #[tokio::test] async fn test_register() { - let (server_addr, _s) = run_tower_in_background().await; - let response = - request_to_api::( - Endpoint::Register, - common_msgs::RegisterRequest { - user_id: get_random_user_id().to_vec(), - }, - server_addr, - ) - .await; - assert!(matches!(response, Ok(common_msgs::RegisterResponse { .. }))); + let server_addr = run_tower_in_background().await; + let response = request_to_api::( + "/register", + msgs::RegisterRequest { + user_id: get_random_user_id().serialize(), + }, + server_addr, + ) + .await; + assert!(matches!(response, Ok(msgs::RegisterResponse { .. }))); } #[tokio::test] async fn test_register_max_slots() { - let (server_addr, _, _s) = + let (server_addr, _) = run_tower_in_background_with_config(ApiConfig::new(u32::MAX, DURATION)).await; let user_id = get_random_user_id(); // Register once, this should go trough and set slots to the limit - request_to_api::( - Endpoint::Register, - common_msgs::RegisterRequest { - user_id: user_id.to_vec(), + request_to_api::( + "/register", + msgs::RegisterRequest { + user_id: user_id.serialize(), }, server_addr, ) @@ -704,9 +668,9 @@ mod tests_methods { // Register again to get additional slots, this should fail assert_eq!( check_api_error( - Endpoint::Register, - RequestBody::Json(serde_json::json!(common_msgs::RegisterRequest { - user_id: user_id.to_vec(), + "/register", + RequestBody::Json(serde_json::json!(msgs::RegisterRequest { + user_id: user_id.serialize(), })), server_addr, ) @@ -723,7 +687,7 @@ mod tests_methods { #[tokio::test] async fn test_register_service_unavailable() { - let (server_addr, _, _s) = run_tower_in_background_with_config( + let (server_addr, _) = run_tower_in_background_with_config( ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable(), ) .await; @@ -732,9 +696,9 @@ mod tests_methods { // Register with bitcoind down assert_eq!( check_api_error( - Endpoint::Register, - RequestBody::Json(serde_json::json!(common_msgs::RegisterRequest { - user_id: user_id.to_vec(), + "/register", + RequestBody::Json(serde_json::json!(msgs::RegisterRequest { + user_id: user_id.serialize(), })), server_addr, ) @@ -751,13 +715,13 @@ mod tests_methods { #[tokio::test] async fn test_add_appointment() { - let (server_addr, _s) = run_tower_in_background().await; + let server_addr = run_tower_in_background().await; // Register first let (user_sk, user_pk) = cryptography::get_random_keypair(); - request_to_api::( - Endpoint::Register, - common_msgs::RegisterRequest { + request_to_api::( + "/register", + msgs::RegisterRequest { user_id: user_pk.serialize().to_vec(), }, server_addr, @@ -767,14 +731,11 @@ 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); + let signature = cryptography::sign(&appointment.serialize(), &user_sk).unwrap(); - let response = request_to_api::< - common_msgs::AddAppointmentRequest, - common_msgs::AddAppointmentResponse, - >( - Endpoint::AddAppointment, - common_msgs::AddAppointmentRequest { + let response = request_to_api::( + "/add_appointment", + msgs::AddAppointmentRequest { appointment: Some(appointment.into()), signature, }, @@ -782,23 +743,20 @@ mod tests_methods { ) .await; - assert!(matches!( - response, - Ok(common_msgs::AddAppointmentResponse { .. }) - )); + assert!(matches!(response, Ok(msgs::AddAppointmentResponse { .. }))); } #[tokio::test] async fn test_add_appointment_non_registered() { - let (server_addr, _s) = run_tower_in_background().await; - let (user_sk, _s) = cryptography::get_random_keypair(); + let server_addr = run_tower_in_background().await; + let (user_sk, _) = cryptography::get_random_keypair(); let appointment = generate_dummy_appointment(None).inner; - let signature = cryptography::sign(&appointment.to_vec(), &user_sk); + let signature = cryptography::sign(&appointment.serialize(), &user_sk).unwrap(); assert_eq!( check_api_error( - Endpoint::AddAppointment, - RequestBody::Json(serde_json::json!(common_msgs::AddAppointmentRequest { + "/add_appointment", + RequestBody::Json(serde_json::json!(msgs::AddAppointmentRequest { appointment: Some(appointment.into()), signature, })), @@ -818,14 +776,14 @@ mod tests_methods { #[tokio::test] async fn test_add_appointment_already_triggered() { // Get the InternalAPI so we can mess with the inner state - let (server_addr, internal_api, _s) = + let (server_addr, internal_api) = run_tower_in_background_with_config(ApiConfig::new(u32::MAX, DURATION)).await; // Register let (user_sk, user_pk) = cryptography::get_random_keypair(); - request_to_api::( - Endpoint::Register, - common_msgs::RegisterRequest { + request_to_api::( + "/register", + msgs::RegisterRequest { user_id: user_pk.serialize().to_vec(), }, server_addr, @@ -833,24 +791,18 @@ mod tests_methods { .await .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), - ); + // Add the appointment to the Responder so it counts as triggered + let appointment = generate_dummy_appointment(None).inner; + let signature = cryptography::sign(&appointment.serialize(), &user_sk).unwrap(); internal_api .get_watcher() - .add_dummy_tracker_to_responder(&tracker); + .add_random_tracker_to_responder(UUID::new(appointment.locator, UserId(user_pk))); // 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, - RequestBody::Json(serde_json::json!(common_msgs::AddAppointmentRequest { + "/add_appointment", + RequestBody::Json(serde_json::json!(msgs::AddAppointmentRequest { appointment: Some(appointment.into()), signature, })), @@ -869,18 +821,18 @@ mod tests_methods { #[tokio::test] async fn test_add_appointment_service_unavailable() { - let (server_addr, _, _s) = run_tower_in_background_with_config( + let (server_addr, _) = run_tower_in_background_with_config( ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable(), ) .await; let (user_sk, _) = cryptography::get_random_keypair(); let appointment = generate_dummy_appointment(None).inner; - let signature = cryptography::sign(&appointment.to_vec(), &user_sk); + let signature = cryptography::sign(&appointment.serialize(), &user_sk).unwrap(); assert_eq!( check_api_error( - Endpoint::AddAppointment, - RequestBody::Json(serde_json::json!(common_msgs::AddAppointmentRequest { + "/add_appointment", + RequestBody::Json(serde_json::json!(msgs::AddAppointmentRequest { appointment: Some(appointment.into()), signature, })), @@ -899,13 +851,13 @@ mod tests_methods { #[tokio::test] async fn test_get_appointment() { - let (server_addr, _s) = run_tower_in_background().await; + let server_addr = run_tower_in_background().await; // Register first let (user_sk, user_pk) = cryptography::get_random_keypair(); - request_to_api::( - Endpoint::Register, - common_msgs::RegisterRequest { + request_to_api::( + "/register", + msgs::RegisterRequest { user_id: user_pk.serialize().to_vec(), }, server_addr, @@ -915,11 +867,11 @@ mod tests_methods { // Add an appointment let appointment = generate_dummy_appointment(None).inner; - let signature = cryptography::sign(&appointment.to_vec(), &user_sk); + let signature = cryptography::sign(&appointment.serialize(), &user_sk).unwrap(); - request_to_api::( - Endpoint::AddAppointment, - common_msgs::AddAppointmentRequest { + request_to_api::( + "/add_appointment", + msgs::AddAppointmentRequest { appointment: Some(appointment.clone().into()), signature, }, @@ -929,31 +881,26 @@ mod tests_methods { .unwrap(); // Get it back - let response = request_to_api::< - common_msgs::GetAppointmentRequest, - common_msgs::GetAppointmentResponse, - >( - Endpoint::GetAppointment, - common_msgs::GetAppointmentRequest { - locator: appointment.locator.to_vec(), + let response = request_to_api::( + "/get_appointment", + msgs::GetAppointmentRequest { + locator: appointment.locator.serialize(), signature: cryptography::sign( format!("get appointment {}", appointment.locator).as_bytes(), &user_sk, - ), + ) + .unwrap(), }, server_addr, ) .await; - assert!(matches!( - response, - Ok(common_msgs::GetAppointmentResponse { .. }) - )); + assert!(matches!(response, Ok(msgs::GetAppointmentResponse { .. }))); } #[tokio::test] async fn test_get_appointment_non_registered() { - let (server_addr, _s) = run_tower_in_background().await; + let server_addr = run_tower_in_background().await; // User is not registered let (user_sk, _) = cryptography::get_random_keypair(); @@ -962,13 +909,14 @@ mod tests_methods { assert_eq!( check_api_error( - Endpoint::GetAppointment, - RequestBody::Json(serde_json::json!(common_msgs::GetAppointmentRequest { - locator: appointment.locator.to_vec(), + "/get_appointment", + RequestBody::Json(serde_json::json!(msgs::GetAppointmentRequest { + locator: appointment.locator.serialize(), signature: cryptography::sign( format!("get appointment {}", appointment.locator).as_bytes(), &user_sk, ) + .unwrap() })), server_addr, ) @@ -985,13 +933,13 @@ mod tests_methods { #[tokio::test] async fn test_get_appointment_not_found() { - let (server_addr, _s) = run_tower_in_background().await; + let server_addr = run_tower_in_background().await; // Register first let (user_sk, user_pk) = cryptography::get_random_keypair(); - request_to_api::( - Endpoint::Register, - common_msgs::RegisterRequest { + request_to_api::( + "/register", + msgs::RegisterRequest { user_id: user_pk.serialize().to_vec(), }, server_addr, @@ -1004,13 +952,14 @@ mod tests_methods { assert_eq!( check_api_error( - Endpoint::GetAppointment, - RequestBody::Json(serde_json::json!(common_msgs::GetAppointmentRequest { - locator: appointment.locator.to_vec(), + "/get_appointment", + RequestBody::Json(serde_json::json!(msgs::GetAppointmentRequest { + locator: appointment.locator.serialize(), signature: cryptography::sign( format!("get appointment {}", appointment.locator).as_bytes(), &user_sk, ) + .unwrap() })), server_addr, ) @@ -1027,7 +976,7 @@ mod tests_methods { #[tokio::test] async fn test_get_appointment_service_unavailable() { - let (server_addr, _, _s) = run_tower_in_background_with_config( + let (server_addr, _) = run_tower_in_background_with_config( ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable(), ) .await; @@ -1038,13 +987,14 @@ mod tests_methods { assert_eq!( check_api_error( - Endpoint::GetAppointment, - RequestBody::Json(serde_json::json!(common_msgs::GetAppointmentRequest { - locator: appointment.locator.to_vec(), + "/get_appointment", + RequestBody::Json(serde_json::json!(msgs::GetAppointmentRequest { + locator: appointment.locator.serialize(), signature: cryptography::sign( format!("get appointment {}", appointment.locator).as_bytes(), &user_sk, ) + .unwrap() })), server_addr, ) @@ -1061,13 +1011,13 @@ mod tests_methods { #[tokio::test] async fn test_get_subscription_info() { - let (server_addr, _s) = run_tower_in_background().await; + let server_addr = run_tower_in_background().await; // Register first let (user_sk, user_pk) = cryptography::get_random_keypair(); - request_to_api::( - Endpoint::Register, - common_msgs::RegisterRequest { + request_to_api::( + "/register", + msgs::RegisterRequest { user_id: user_pk.serialize().to_vec(), }, server_addr, @@ -1076,36 +1026,36 @@ mod tests_methods { .unwrap(); // Get the subscription info - let response = request_to_api::< - common_msgs::GetSubscriptionInfoRequest, - common_msgs::GetSubscriptionInfoResponse, - >( - Endpoint::GetSubscriptionInfo, - common_msgs::GetSubscriptionInfoRequest { - signature: cryptography::sign("get subscription info".as_bytes(), &user_sk), - }, - server_addr, - ) - .await; + let response = + request_to_api::( + "/get_subscription_info", + msgs::GetSubscriptionInfoRequest { + signature: cryptography::sign("get subscription info".as_bytes(), &user_sk) + .unwrap(), + }, + server_addr, + ) + .await; assert!(matches!( response, - Ok(common_msgs::GetSubscriptionInfoResponse { .. }) + Ok(msgs::GetSubscriptionInfoResponse { .. }) )); } #[tokio::test] async fn test_get_subscription_info_non_registered() { - let (server_addr, _s) = run_tower_in_background().await; + let server_addr = run_tower_in_background().await; // User is not registered let (user_sk, _) = cryptography::get_random_keypair(); assert_eq!( check_api_error( - Endpoint::GetSubscriptionInfo, - RequestBody::Json(serde_json::json!(common_msgs::GetSubscriptionInfoRequest { + "/get_subscription_info", + RequestBody::Json(serde_json::json!(msgs::GetSubscriptionInfoRequest { signature: cryptography::sign("get subscription info".as_bytes(), &user_sk) + .unwrap(), })), server_addr, ) @@ -1123,16 +1073,17 @@ mod tests_methods { #[tokio::test] async fn test_get_subscription_info_service_unavailable() { let (user_sk, _) = cryptography::get_random_keypair(); - let (server_addr, _, _s) = run_tower_in_background_with_config( + let (server_addr, _) = run_tower_in_background_with_config( ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable(), ) .await; assert_eq!( check_api_error( - Endpoint::GetSubscriptionInfo, - RequestBody::Json(serde_json::json!(common_msgs::GetSubscriptionInfoRequest { + "/get_subscription_info", + RequestBody::Json(serde_json::json!(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 8a1e1ea..4d8f895 100644 --- a/teos/src/api/internal.rs +++ b/teos/src/api/internal.rs @@ -2,17 +2,16 @@ 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; + use crate::watcher::{ AddAppointmentFailure, AppointmentInfo, GetAppointmentFailure, GetSubscriptionInfoFailure, Watcher, }; use teos_common::appointment::{Appointment, AppointmentStatus, Locator}; -use teos_common::protos as common_msgs; use teos_common::UserId; /// Internal API of the tower. @@ -22,8 +21,6 @@ use teos_common::UserId; pub struct InternalAPI { /// A [Watcher] instance. watcher: Arc, - /// A list of public API endpoints. - addresses: Vec, /// A flag that indicates wether bitcoind is reachable or not. bitcoind_reachable: Arc<(Mutex, Condvar)>, /// A signal indicating the tower is shuting down. @@ -34,22 +31,16 @@ impl InternalAPI { /// Creates a new [InternalAPI] instance. pub fn new( watcher: Arc, - addresses: Vec, bitcoind_reachable: Arc<(Mutex, Condvar)>, shutdown_trigger: Trigger, ) -> Self { Self { watcher, - addresses, bitcoind_reachable, shutdown_trigger, } } - pub fn get_addresses(&self) -> &Vec { - &self.addresses - } - /// Checks whether bitcoind is reachable. fn check_service_unavailable(&self) -> Result<(), Status> { if *self.bitcoind_reachable.0.lock().unwrap() { @@ -70,12 +61,12 @@ impl PublicTowerServices for Arc { /// Register endpoint. Part of the public API. Internally calls [Watcher::register]. async fn register( &self, - request: Request, - ) -> Result, Status> { + request: Request, + ) -> Result, Status> { self.check_service_unavailable()?; let req_data = request.into_inner(); - let user_id = UserId::from_slice(&req_data.user_id).map_err(|_| { + let user_id = UserId::deserialize(&req_data.user_id).map_err(|_| { Status::new( Code::InvalidArgument, "Provided public key does not match expected format (33-byte compressed key)", @@ -83,10 +74,9 @@ impl PublicTowerServices for Arc { })?; match self.watcher.register(user_id) { - Ok(receipt) => Ok(Response::new(common_msgs::RegisterResponse { + Ok(receipt) => Ok(Response::new(msgs::RegisterResponse { user_id: req_data.user_id, available_slots: receipt.available_slots(), - subscription_start: receipt.subscription_start(), subscription_expiry: receipt.subscription_expiry(), subscription_signature: receipt.signature().unwrap(), })), @@ -100,14 +90,14 @@ impl PublicTowerServices for Arc { /// Add appointment endpoint. Part of the public API. Internally calls [Watcher::add_appointment]. async fn add_appointment( &self, - request: Request, - ) -> Result, Status> { + request: Request, + ) -> Result, Status> { self.check_service_unavailable()?; let req_data = request.into_inner(); let app_data = req_data.appointment.unwrap(); let appointment = Appointment::new( - Locator::from_slice(&app_data.locator).unwrap(), + Locator::deserialize(&app_data.locator).unwrap(), app_data.encrypted_blob, app_data.to_self_delay, ); @@ -118,8 +108,8 @@ impl PublicTowerServices for Arc { .add_appointment(appointment, req_data.signature) { Ok((receipt, available_slots, subscription_expiry)) => { - Ok(Response::new(common_msgs::AddAppointmentResponse { - locator: locator.to_vec(), + Ok(Response::new(msgs::AddAppointmentResponse { + locator: locator.serialize(), start_block: receipt.start_block(), signature: receipt.signature().unwrap(), available_slots, @@ -134,7 +124,7 @@ impl PublicTowerServices for Arc { )), AddAppointmentFailure::SubscriptionExpired(x) => Err(Status::new( Code::Unauthenticated, - format!("Your subscription expired at {x}"), + format!("Your subscription expired at {}", x), )), AddAppointmentFailure::AlreadyTriggered => Err(Status::new( Code::AlreadyExists, @@ -147,19 +137,19 @@ impl PublicTowerServices for Arc { /// Get appointment endpoint. Part of the public API. Internally calls [Watcher::get_appointment]. async fn get_appointment( &self, - request: Request, - ) -> Result, Status> { + request: Request, + ) -> Result, Status> { self.check_service_unavailable()?; let req_data = request.into_inner(); - let locator = Locator::from_slice(&req_data.locator).unwrap(); + let locator = Locator::deserialize(&req_data.locator).unwrap(); match self.watcher.get_appointment(locator, &req_data.signature) { Ok(info) => { let (appointment_data, status) = match info { AppointmentInfo::Appointment(appointment) => ( - common_msgs::AppointmentData { + msgs::AppointmentData { appointment_data: Some( - common_msgs::appointment_data::AppointmentData::Appointment( + msgs::appointment_data::AppointmentData::Appointment( appointment.into(), ), ), @@ -167,17 +157,15 @@ impl PublicTowerServices for Arc { AppointmentStatus::BeingWatched, ), AppointmentInfo::Tracker(tracker) => ( - common_msgs::AppointmentData { + msgs::AppointmentData { appointment_data: Some( - common_msgs::appointment_data::AppointmentData::Tracker( - tracker.into(), - ), + msgs::appointment_data::AppointmentData::Tracker(tracker.into()), ), }, AppointmentStatus::DisputeResponded, ), }; - Ok(Response::new(common_msgs::GetAppointmentResponse { + Ok(Response::new(msgs::GetAppointmentResponse { appointment_data: Some(appointment_data), status: status as i32, })) @@ -192,7 +180,7 @@ impl PublicTowerServices for Arc { )), GetAppointmentFailure::SubscriptionExpired(x) => Err(Status::new( Code::Unauthenticated, - format!("Your subscription expired at {x}"), + format!("Your subscription expired at {}", x), )), }, } @@ -201,8 +189,8 @@ impl PublicTowerServices for Arc { /// Get subscription info endpoint. Part of the public API. Internally calls [Watcher::get_subscription_info]. async fn get_subscription_info( &self, - request: Request, - ) -> Result, Status> { + request: Request, + ) -> Result, Status> { self.check_service_unavailable()?; let (subscription_info, locators) = self .watcher @@ -214,14 +202,14 @@ impl PublicTowerServices for Arc { ), GetSubscriptionInfoFailure::SubscriptionExpired(x) => Status::new( Code::Unauthenticated, - format!("Your subscription expired at {x}"), + format!("Your subscription expired at {}", x), ), })?; - Ok(Response::new(common_msgs::GetSubscriptionInfoResponse { + Ok(Response::new(msgs::GetSubscriptionInfoResponse { available_slots: subscription_info.available_slots, subscription_expiry: subscription_info.subscription_expiry, - locators: locators.iter().map(|x| x.to_vec()).collect(), + locators: locators.iter().map(|x| x.serialize()).collect(), })) } } @@ -233,30 +221,21 @@ impl PrivateTowerServices for Arc { /// Internally calls [Watcher::get_all_watcher_appointments] and [Watcher::get_all_responder_trackers]. async fn get_all_appointments( &self, - request: Request<()>, + _: Request<()>, ) -> Result, Status> { - log::debug!( - "Received a get_all_appointments request from {}", - request - .remote_addr() - .map_or("an unknown address".to_owned(), |a| a.to_string()) - ); - let mut all_appointments = Vec::new(); for (_, appointment) in self.watcher.get_all_watcher_appointments().into_iter() { - all_appointments.push(common_msgs::AppointmentData { - appointment_data: Some( - common_msgs::appointment_data::AppointmentData::Appointment( - appointment.inner.into(), - ), - ), + all_appointments.push(msgs::AppointmentData { + appointment_data: Some(msgs::appointment_data::AppointmentData::Appointment( + appointment.inner.into(), + )), }) } for (_, tracker) in self.watcher.get_all_responder_trackers().into_iter() { - all_appointments.push(common_msgs::AppointmentData { - appointment_data: Some(common_msgs::appointment_data::AppointmentData::Tracker( + all_appointments.push(msgs::AppointmentData { + appointment_data: Some(msgs::appointment_data::AppointmentData::Tracker( tracker.into(), )), }) @@ -267,75 +246,15 @@ impl PrivateTowerServices for Arc { })) } - /// Get appointments endpoint. Gets the appointments with a specific locator. Part of the private API. - /// Internally calls [Watcher::get_watcher_appointments_using_locator] and [Watcher::get_responder_trackers_using_locator]. - async fn get_appointments( - &self, - request: tonic::Request, - ) -> Result, Status> { - log::debug!( - "Received a get_appointments requests from {}", - request - .remote_addr() - .map_or("an unknown address".to_owned(), |a| a.to_string()) - ); - - let mut matching_appointments = vec![]; - let locator = Locator::from_slice(&request.into_inner().locator).map_err(|_| { - Status::new( - Code::InvalidArgument, - "The provided locator does not match the expected format (16-byte hexadecimal string)", - ) - })?; - - for (_, appointment) in self - .watcher - .get_watcher_appointments_with_locator(locator) - .into_iter() - { - matching_appointments.push(common_msgs::AppointmentData { - appointment_data: Some( - common_msgs::appointment_data::AppointmentData::Appointment( - appointment.inner.into(), - ), - ), - }) - } - - for (_, tracker) in self - .watcher - .get_responder_trackers_with_locator(locator) - .into_iter() - { - matching_appointments.push(common_msgs::AppointmentData { - appointment_data: Some(common_msgs::appointment_data::AppointmentData::Tracker( - tracker.into(), - )), - }) - } - - Ok(Response::new(msgs::GetAppointmentsResponse { - appointments: matching_appointments, - })) - } - /// Get tower info endpoint. Gets information about the tower state. Part of the private API. /// Internally calls [Watcher::get_registered_users_count], [Watcher::get_appointments_count] /// and [Watcher::get_trackers_count]. async fn get_tower_info( &self, - request: Request<()>, + _: Request<()>, ) -> Result, Status> { - log::debug!( - "Received a get_tower_info request from {}", - request - .remote_addr() - .map_or("an unknown address".to_owned(), |a| a.to_string()) - ); - Ok(Response::new(msgs::GetTowerInfoResponse { - tower_id: self.watcher.tower_id.to_vec(), - addresses: self.get_addresses().clone(), + tower_id: self.watcher.tower_id.serialize(), n_registered_users: self.watcher.get_registered_users_count() as u32, n_watcher_appointments: self.watcher.get_appointments_count() as u32, n_responder_trackers: self.watcher.get_trackers_count() as u32, @@ -345,22 +264,12 @@ impl PrivateTowerServices for Arc { /// Get user endpoint. Gets all users in the tower. Part of the private API. /// Internally calls [Watcher::get_user_ids]. - async fn get_users( - &self, - request: Request<()>, - ) -> Result, Status> { - log::debug!( - "Received a get_users requests from {}", - request - .remote_addr() - .map_or("an unknown address".to_owned(), |a| a.to_string()) - ); - + async fn get_users(&self, _: Request<()>) -> Result, Status> { let user_ids = self .watcher .get_user_ids() .iter() - .map(|x| x.to_vec()) + .map(|x| x.serialize()) .collect(); Ok(Response::new(msgs::GetUsersResponse { user_ids })) @@ -372,14 +281,7 @@ impl PrivateTowerServices for Arc { &self, request: Request, ) -> Result, Status> { - log::debug!( - "Received a get_user request from {}", - request - .remote_addr() - .map_or("an unknown address".to_owned(), |a| a.to_string()) - ); - - let user_id = UserId::from_slice(&request.into_inner().user_id).map_err(|_| { + let user_id = UserId::deserialize(&request.into_inner().user_id).map_err(|_| { Status::new( Code::InvalidArgument, "Provided public key does not match expected format (33-byte compressed key)", @@ -387,13 +289,13 @@ impl PrivateTowerServices for Arc { })?; match self.watcher.get_user_info(user_id) { - Some((info, locators)) => Ok(Response::new(msgs::GetUserResponse { + Some(info) => Ok(Response::new(msgs::GetUserResponse { available_slots: info.available_slots, subscription_expiry: info.subscription_expiry, - // 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()) + appointments: info + .appointments + .iter() + .map(|(uuid, _)| uuid.serialize()) .collect(), })), None => Err(Status::new(Code::NotFound, "User not found")), @@ -401,15 +303,10 @@ impl PrivateTowerServices for Arc { } /// Stop endpoint. Stops the tower daemon. Part of the private API. - async fn stop(&self, request: Request<()>) -> Result, Status> { + async fn stop(&self, _: Request<()>) -> Result, Status> { self.shutdown_trigger.trigger(); - log::debug!( - "Received a shutting down request from {}, notifying components", - request - .remote_addr() - .map_or("an unknown address".to_owned(), |a| a.to_string()) - ); + log::debug!("Received shutting down signal, notifying components"); Ok(Response::new(())) } } @@ -431,22 +328,15 @@ mod tests_private_api { use std::collections::HashSet; use std::iter::FromIterator; - use bitcoin::hashes::Hash; - use bitcoin::Txid; - - use crate::responder::{ConfirmationStatus, TransactionTracker}; + use crate::extended_appointment::UUID; use crate::test_utils::{ - create_api, generate_dummy_appointment, generate_dummy_appointment_with_user, - get_random_tx, DURATION, SLOTS, START_HEIGHT, + create_api, generate_dummy_appointment, DURATION, SLOTS, START_HEIGHT, }; - use crate::watcher::Breach; - use teos_common::cryptography::{self, get_random_keypair}; - use teos_common::test_utils::get_random_user_id; #[tokio::test] async fn test_get_all_appointments() { - let (internal_api, _s) = create_api().await; + let internal_api = create_api().await; let response = internal_api .get_all_appointments(Request::new(())) @@ -454,19 +344,19 @@ mod tests_private_api { .unwrap() .into_inner(); - assert!(matches!(response, msgs::GetAllAppointmentsResponse { .. })); + assert!(matches!(response, msgs::GetAllAppointmentsResponse { .. })) } #[tokio::test] async fn test_get_all_appointments_watcher() { - let (internal_api, _s) = create_api().await; + let internal_api = create_api().await; // Add data to the Watcher so we can retrieve it later on let (user_sk, user_pk) = get_random_keypair(); 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); + let user_signature = cryptography::sign(&appointment.serialize(), &user_sk).unwrap(); internal_api .watcher .add_appointment(appointment.clone(), user_signature) @@ -481,16 +371,23 @@ mod tests_private_api { assert_eq!(response.appointments.len(), 1); assert!(matches!( response.appointments[0].appointment_data, - Some(common_msgs::appointment_data::AppointmentData::Appointment { .. }) + Some(msgs::appointment_data::AppointmentData::Appointment { .. }) )); } #[tokio::test] async fn test_get_all_appointments_responder() { - let (internal_api, _s) = create_api().await; + let internal_api = create_api().await; // Add data to the Responser so we can retrieve it later on - internal_api.watcher.add_random_tracker_to_responder(); + let (_, user_pk) = get_random_keypair(); + let user_id = UserId(user_pk); + internal_api.watcher.register(user_id).unwrap(); + + let appointment = generate_dummy_appointment(None).inner; + internal_api + .watcher + .add_random_tracker_to_responder(UUID::new(appointment.locator, user_id)); let response = internal_api .get_all_appointments(Request::new(())) @@ -501,128 +398,13 @@ mod tests_private_api { assert_eq!(response.appointments.len(), 1); assert!(matches!( response.appointments[0].appointment_data, - Some(common_msgs::appointment_data::AppointmentData::Tracker { .. }) + Some(msgs::appointment_data::AppointmentData::Tracker { .. }) )); } - #[tokio::test] - async fn test_get_appointments() { - let (internal_api, _s) = create_api().await; - - let locator = Locator::new(get_random_tx().compute_txid()).to_vec(); - let response = internal_api - .get_appointments(Request::new(msgs::GetAppointmentsRequest { locator })) - .await - .unwrap() - .into_inner(); - - assert!(matches!(response, msgs::GetAppointmentsResponse { .. })); - } - - #[tokio::test] - async fn test_get_appointments_watcher() { - let (internal_api, _s) = create_api().await; - - for i in 0..3 { - // Create a dispute tx to be used for creating different dummy appointments with the same locator. - 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; - - // Add that many appointments to the watcher. - for _ in 0..appointments_to_create { - 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); - internal_api - .watcher - .add_appointment(appointment, signature) - .unwrap(); - } - - let locator = Locator::new(dispute_txid); - - // Query for the current locator and assert it retrieves correct appointments. - let response = internal_api - .get_appointments(Request::new(msgs::GetAppointmentsRequest { - locator: locator.to_vec(), - })) - .await - .unwrap() - .into_inner(); - - // The response should contain `appointments_to_create` appointments, all having the locator of the current iteration. - assert_eq!(response.appointments.len(), appointments_to_create); - for app_data in response.appointments { - assert!(matches!( - app_data.appointment_data, - Some(common_msgs::appointment_data::AppointmentData::Appointment( - common_msgs::Appointment { - locator: ref app_loc, - .. - } - )) if Locator::from_slice(app_loc).unwrap() == locator - )); - } - } - } - - #[tokio::test] - async fn test_get_appointments_responder() { - let (internal_api, _s) = create_api().await; - - for i in 0..3 { - // Create a dispute tx to be used for creating different trackers. - let dispute_tx = get_random_tx(); - let breach = Breach::new(dispute_tx.clone(), get_random_tx()); - - // The number of different trackers to create for this dispute tx. - let trackers_to_create = 4 * i + 7; - - // Add that many trackers to the responder. - for _ in 0..trackers_to_create { - let tracker = TransactionTracker::new( - breach.clone(), - get_random_user_id(), - ConfirmationStatus::ConfirmedIn(100), - ); - internal_api - .watcher - .add_dummy_tracker_to_responder(&tracker); - } - - let locator = Locator::new(dispute_tx.compute_txid()); - - // Query for the current locator and assert it retrieves correct trackers. - let response = internal_api - .get_appointments(Request::new(msgs::GetAppointmentsRequest { - locator: locator.to_vec(), - })) - .await - .unwrap() - .into_inner(); - - // The response should contain `trackers_to_create` trackers, all with dispute txid that matches with the locator of the current iteration. - assert_eq!(response.appointments.len(), trackers_to_create); - for app_data in response.appointments { - assert!(matches!( - app_data.appointment_data, - Some(common_msgs::appointment_data::AppointmentData::Tracker( - common_msgs::Tracker { - ref dispute_txid, - .. - } - )) if Locator::new(Txid::from_slice(dispute_txid).unwrap()) == locator - )); - } - } - } - #[tokio::test] async fn test_get_tower_info_empty() { - let (internal_api, _s) = create_api().await; + let internal_api = create_api().await; let response = internal_api .get_tower_info(Request::new(())) @@ -630,7 +412,7 @@ mod tests_private_api { .unwrap() .into_inner(); - assert_eq!(response.tower_id, internal_api.watcher.tower_id.to_vec()); + assert_eq!(response.tower_id, internal_api.watcher.tower_id.serialize()); assert_eq!(response.n_registered_users, 0); assert_eq!(response.n_watcher_appointments, 0); assert_eq!(response.n_responder_trackers, 0); @@ -638,7 +420,7 @@ mod tests_private_api { #[tokio::test] async fn test_get_tower_info() { - let (internal_api, _s) = create_api().await; + let internal_api = create_api().await; // Register a user let (user_sk, user_pk) = get_random_keypair(); @@ -648,7 +430,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); + let user_signature = cryptography::sign(&appointment.serialize(), &user_sk).unwrap(); internal_api .watcher .add_appointment(appointment.clone(), user_signature) @@ -657,7 +439,10 @@ mod tests_private_api { // And the Responder for _ in 0..3 { - internal_api.watcher.add_random_tracker_to_responder(); + let appointment = generate_dummy_appointment(None).inner; + internal_api + .watcher + .add_random_tracker_to_responder(UUID::new(appointment.locator, user_id)); } let response = internal_api @@ -667,7 +452,7 @@ mod tests_private_api { .into_inner(); // Given get_tower_info checks data in memory, the data added to the Responder in the test won't be added to the Watcher too. - assert_eq!(response.tower_id, internal_api.watcher.tower_id.to_vec()); + assert_eq!(response.tower_id, internal_api.watcher.tower_id.serialize()); assert_eq!(response.n_registered_users, 1); assert_eq!(response.n_watcher_appointments, 2); assert_eq!(response.n_responder_trackers, 3); @@ -675,7 +460,7 @@ mod tests_private_api { #[tokio::test] async fn test_get_users() { - let (internal_api, _s) = create_api().await; + let internal_api = create_api().await; let mut users = HashSet::new(); // Add a couple of users @@ -683,7 +468,7 @@ mod tests_private_api { let (_, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); internal_api.watcher.register(user_id).unwrap(); - users.insert(user_id.to_vec()); + users.insert(user_id.serialize()); } let response = internal_api @@ -697,7 +482,7 @@ mod tests_private_api { #[tokio::test] async fn test_get_users_empty() { - let (internal_api, _s) = create_api().await; + let internal_api = create_api().await; let response = internal_api .get_users(Request::new(())) @@ -710,7 +495,7 @@ mod tests_private_api { #[tokio::test] async fn test_get_user() { - let (internal_api, _s) = create_api().await; + let internal_api = create_api().await; // Register a user and get it back let (user_sk, user_pk) = get_random_keypair(); @@ -719,7 +504,7 @@ mod tests_private_api { let response = internal_api .get_user(Request::new(msgs::GetUserRequest { - user_id: user_id.to_vec(), + user_id: user_id.serialize(), })) .await .unwrap() @@ -730,16 +515,17 @@ mod tests_private_api { assert!(response.appointments.is_empty()); // Add an appointment and check back - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - let user_signature = cryptography::sign(&appointment.inner.to_vec(), &user_sk); + let appointment = generate_dummy_appointment(None).inner; + let uuid = UUID::new(appointment.locator, user_id); + let user_signature = cryptography::sign(&appointment.serialize(), &user_sk).unwrap(); internal_api .watcher - .add_appointment(appointment.inner, user_signature) + .add_appointment(appointment.clone(), user_signature) .unwrap(); let response = internal_api .get_user(Request::new(msgs::GetUserRequest { - user_id: user_id.to_vec(), + user_id: user_id.serialize(), })) .await .unwrap() @@ -747,19 +533,19 @@ mod tests_private_api { assert_eq!(response.available_slots, SLOTS - 1); assert_eq!(response.subscription_expiry, START_HEIGHT as u32 + DURATION); - assert_eq!(response.appointments, Vec::from([uuid.to_vec()])); + assert_eq!(response.appointments, Vec::from([uuid.serialize()])); } #[tokio::test] async fn test_get_user_not_found() { - let (internal_api, _s) = create_api().await; + let internal_api = create_api().await; // Non-registered user let (_, user_pk) = get_random_keypair(); match internal_api .get_user(Request::new(msgs::GetUserRequest { - user_id: UserId(user_pk).to_vec(), + user_id: UserId(user_pk).serialize(), })) .await { @@ -773,7 +559,7 @@ mod tests_private_api { #[tokio::test] async fn test_stop() { - let (internal_api, _s) = create_api().await; + let internal_api = create_api().await; assert!(!internal_api.shutdown_trigger.is_triggered()); internal_api.stop(Request::new(())).await.unwrap(); @@ -785,54 +571,52 @@ mod tests_private_api { mod tests_public_api { use super::*; - use crate::responder::{ConfirmationStatus, TransactionTracker}; + use crate::extended_appointment::UUID; use crate::test_utils::{ - create_api, create_api_with_config, generate_dummy_appointment, get_random_tx, ApiConfig, - DURATION, SLOTS, + create_api, create_api_with_config, generate_dummy_appointment, ApiConfig, DURATION, SLOTS, }; - use crate::watcher::Breach; use teos_common::cryptography::{self, get_random_keypair}; #[tokio::test] async fn test_register() { - let (internal_api, _s) = create_api().await; + let internal_api = create_api().await; let (_, user_pk) = get_random_keypair(); // Registering (even multiple times) should work for _ in 0..2 { let response = internal_api - .register(Request::new(common_msgs::RegisterRequest { - user_id: UserId(user_pk).to_vec(), + .register(Request::new(msgs::RegisterRequest { + user_id: UserId(user_pk).serialize(), })) .await .unwrap() .into_inner(); - assert!(matches!(response, common_msgs::RegisterResponse { .. })) + assert!(matches!(response, msgs::RegisterResponse { .. })) } } #[tokio::test] async fn test_register_wrong_user_id() { - let (internal_api, _s) = create_api().await; + let internal_api = create_api().await; let mut user_ids = Vec::new(); // Wrong user id size let (_, user_pk) = get_random_keypair(); - let mut user_id_vec = UserId(user_pk).to_vec(); + let mut user_id_vec = UserId(user_pk).serialize(); user_id_vec.pop(); user_ids.push(user_id_vec); // Wrong format (does not start with 2 nor 3) - user_id_vec = UserId(user_pk).to_vec(); + user_id_vec = UserId(user_pk).serialize(); user_id_vec[0] = 1; user_ids.push(user_id_vec); for user_id in user_ids { match internal_api - .register(Request::new(common_msgs::RegisterRequest { user_id })) + .register(Request::new(msgs::RegisterRequest { user_id })) .await { Err(status) => { @@ -846,14 +630,14 @@ mod tests_public_api { #[tokio::test] async fn test_register_max_slots() { - let (internal_api, _s) = create_api_with_config(ApiConfig::new(u32::MAX, DURATION)).await; + let internal_api = create_api_with_config(ApiConfig::new(u32::MAX, DURATION)).await; let (_, user_pk) = get_random_keypair(); - let user_id = UserId(user_pk).to_vec(); + let user_id = UserId(user_pk).serialize(); // First registration should go trough internal_api - .register(Request::new(common_msgs::RegisterRequest { + .register(Request::new(msgs::RegisterRequest { user_id: user_id.clone(), })) .await @@ -861,7 +645,7 @@ mod tests_public_api { // Trying to add more slots (re-register) must fail match internal_api - .register(Request::new(common_msgs::RegisterRequest { user_id })) + .register(Request::new(msgs::RegisterRequest { user_id })) .await { Err(status) => { @@ -874,14 +658,14 @@ mod tests_public_api { #[tokio::test] async fn test_register_service_unavailable() { - let (internal_api, _s) = + let internal_api = create_api_with_config(ApiConfig::new(u32::MAX, DURATION).bitcoind_unreachable()).await; let (_, user_pk) = get_random_keypair(); - let user_id = UserId(user_pk).to_vec(); + let user_id = UserId(user_pk).serialize(); match internal_api - .register(Request::new(common_msgs::RegisterRequest { user_id })) + .register(Request::new(msgs::RegisterRequest { user_id })) .await { Err(status) => { @@ -894,44 +678,41 @@ mod tests_public_api { #[tokio::test] async fn test_add_appointment() { - let (internal_api, _s) = create_api().await; + let internal_api = create_api().await; // User must be registered let (user_sk, user_pk) = get_random_keypair(); internal_api.watcher.register(UserId(user_pk)).unwrap(); let appointment = generate_dummy_appointment(None).inner; - let signature = cryptography::sign(&appointment.to_vec(), &user_sk); + let user_signature = cryptography::sign(&appointment.serialize(), &user_sk).unwrap(); let response = internal_api - .add_appointment(Request::new(common_msgs::AddAppointmentRequest { + .add_appointment(Request::new(msgs::AddAppointmentRequest { appointment: Some(appointment.clone().into()), - signature, + signature: user_signature.clone(), })) .await .unwrap() .into_inner(); - assert!(matches!( - response, - common_msgs::AddAppointmentResponse { .. } - )); + assert!(matches!(response, msgs::AddAppointmentResponse { .. })); } #[tokio::test] async fn test_add_appointment_non_registered() { - let (internal_api, _s) = create_api().await; + let internal_api = create_api().await; // User is not registered this time let (user_sk, _) = get_random_keypair(); let appointment = generate_dummy_appointment(None).inner; - let signature = cryptography::sign(&appointment.to_vec(), &user_sk); + let user_signature = cryptography::sign(&appointment.serialize(), &user_sk).unwrap(); match internal_api - .add_appointment(Request::new(common_msgs::AddAppointmentRequest { + .add_appointment(Request::new(msgs::AddAppointmentRequest { appointment: Some(appointment.clone().into()), - signature, + signature: user_signature.clone(), })) .await { @@ -948,19 +729,19 @@ mod tests_public_api { #[tokio::test] async fn test_add_appointment_not_enough_slots() { - let (internal_api, _s) = create_api_with_config(ApiConfig::new(0, DURATION)).await; + let internal_api = create_api_with_config(ApiConfig::new(0, DURATION)).await; // User is registered but has no slots let (user_sk, user_pk) = get_random_keypair(); internal_api.watcher.register(UserId(user_pk)).unwrap(); let appointment = generate_dummy_appointment(None).inner; - let signature = cryptography::sign(&appointment.to_vec(), &user_sk); + let user_signature = cryptography::sign(&appointment.serialize(), &user_sk).unwrap(); match internal_api - .add_appointment(Request::new(common_msgs::AddAppointmentRequest { + .add_appointment(Request::new(msgs::AddAppointmentRequest { appointment: Some(appointment.clone().into()), - signature, + signature: user_signature.clone(), })) .await { @@ -977,19 +758,19 @@ mod tests_public_api { #[tokio::test] async fn test_add_appointment_subscription_expired() { - let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0)).await; + let internal_api = create_api_with_config(ApiConfig::new(SLOTS, 0)).await; // User is registered but subscription is expired let (user_sk, user_pk) = get_random_keypair(); internal_api.watcher.register(UserId(user_pk)).unwrap(); let appointment = generate_dummy_appointment(None).inner; - let signature = cryptography::sign(&appointment.to_vec(), &user_sk); + let user_signature = cryptography::sign(&appointment.serialize(), &user_sk).unwrap(); match internal_api - .add_appointment(Request::new(common_msgs::AddAppointmentRequest { + .add_appointment(Request::new(msgs::AddAppointmentRequest { appointment: Some(appointment.clone().into()), - signature, + signature: user_signature.clone(), })) .await { @@ -1003,30 +784,22 @@ mod tests_public_api { #[tokio::test] async fn test_add_appointment_already_triggered() { - let (internal_api, _s) = create_api().await; + let internal_api = create_api().await; let (user_sk, user_pk) = get_random_keypair(); let user_id = UserId(user_pk); internal_api.watcher.register(user_id).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), - ); + let appointment = generate_dummy_appointment(None).inner; + let user_signature = cryptography::sign(&appointment.serialize(), &user_sk).unwrap(); internal_api - .get_watcher() - .add_dummy_tracker_to_responder(&tracker); + .watcher + .add_random_tracker_to_responder(UUID::new(appointment.locator, user_id)); - // 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.into()), - signature, + .add_appointment(Request::new(msgs::AddAppointmentRequest { + appointment: Some(appointment.clone().into()), + signature: user_signature.clone(), })) .await { @@ -1042,17 +815,17 @@ mod tests_public_api { #[tokio::test] async fn test_add_appointment_service_unavailable() { - let (internal_api, _s) = + let internal_api = create_api_with_config(ApiConfig::new(u32::MAX, DURATION).bitcoind_unreachable()).await; let (user_sk, _) = get_random_keypair(); let appointment = generate_dummy_appointment(None).inner; - let signature = cryptography::sign(&appointment.to_vec(), &user_sk); + let user_signature = cryptography::sign(&appointment.serialize(), &user_sk).unwrap(); match internal_api - .add_appointment(Request::new(common_msgs::AddAppointmentRequest { + .add_appointment(Request::new(msgs::AddAppointmentRequest { appointment: Some(appointment.clone().into()), - signature, + signature: user_signature.clone(), })) .await { @@ -1066,7 +839,7 @@ mod tests_public_api { #[tokio::test] async fn test_get_appointment() { - let (internal_api, _s) = create_api().await; + let internal_api = create_api().await; // The user must be registered let (user_sk, user_pk) = get_random_keypair(); @@ -1074,7 +847,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); + let user_signature = cryptography::sign(&appointment.serialize(), &user_sk).unwrap(); internal_api .watcher .add_appointment(appointment.clone(), user_signature) @@ -1083,23 +856,20 @@ mod tests_public_api { // Get the appointment through the API let message = format!("get appointment {}", appointment.locator); let response = internal_api - .get_appointment(Request::new(common_msgs::GetAppointmentRequest { - locator: appointment.locator.to_vec(), - signature: cryptography::sign(message.as_bytes(), &user_sk), + .get_appointment(Request::new(msgs::GetAppointmentRequest { + locator: appointment.locator.serialize(), + signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(), })) .await .unwrap() .into_inner(); - assert!(matches!( - response, - common_msgs::GetAppointmentResponse { .. } - )); + assert!(matches!(response, msgs::GetAppointmentResponse { .. })) } #[tokio::test] async fn test_get_appointment_non_registered() { - let (internal_api, _s) = create_api().await; + let internal_api = create_api().await; // Add a first user to link the appointment to him let (user_sk, user_pk) = get_random_keypair(); @@ -1111,9 +881,9 @@ mod tests_public_api { // Try to get the appointment through the API let message = format!("get appointment {}", appointment.locator); match internal_api - .get_appointment(Request::new(common_msgs::GetAppointmentRequest { - locator: appointment.locator.to_vec(), - signature: cryptography::sign(message.as_bytes(), &user_sk), + .get_appointment(Request::new(msgs::GetAppointmentRequest { + locator: appointment.locator.serialize(), + signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(), })) .await { @@ -1127,7 +897,7 @@ mod tests_public_api { #[tokio::test] async fn test_get_appointment_non_existent() { - let (internal_api, _s) = create_api().await; + let internal_api = create_api().await; // The user is registered but the appointment does not exist let (user_sk, user_pk) = get_random_keypair(); @@ -1138,9 +908,9 @@ mod tests_public_api { let message = format!("get appointment {}", appointment.locator); match internal_api - .get_appointment(Request::new(common_msgs::GetAppointmentRequest { - locator: appointment.locator.to_vec(), - signature: cryptography::sign(message.as_bytes(), &user_sk), + .get_appointment(Request::new(msgs::GetAppointmentRequest { + locator: appointment.locator.serialize(), + signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(), })) .await { @@ -1154,7 +924,7 @@ mod tests_public_api { #[tokio::test] async fn test_get_appointment_subscription_expired() { - let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0)).await; + let internal_api = create_api_with_config(ApiConfig::new(SLOTS, 0)).await; // Register the user let (user_sk, user_pk) = get_random_keypair(); @@ -1166,9 +936,9 @@ mod tests_public_api { // Try to get the appointment through the API let message = format!("get appointment {}", appointment.locator); match internal_api - .get_appointment(Request::new(common_msgs::GetAppointmentRequest { - locator: appointment.locator.to_vec(), - signature: cryptography::sign(message.as_bytes(), &user_sk), + .get_appointment(Request::new(msgs::GetAppointmentRequest { + locator: appointment.locator.serialize(), + signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(), })) .await { @@ -1182,16 +952,16 @@ mod tests_public_api { #[tokio::test] async fn test_get_appointment_service_unavailable() { - let (internal_api, _s) = + let internal_api = create_api_with_config(ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable()).await; let (user_sk, _) = get_random_keypair(); let appointment = generate_dummy_appointment(None).inner; let message = format!("get appointment {}", appointment.locator); match internal_api - .get_appointment(Request::new(common_msgs::GetAppointmentRequest { - locator: appointment.locator.to_vec(), - signature: cryptography::sign(message.as_bytes(), &user_sk), + .get_appointment(Request::new(msgs::GetAppointmentRequest { + locator: appointment.locator.serialize(), + signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(), })) .await { @@ -1205,7 +975,7 @@ mod tests_public_api { #[tokio::test] async fn test_get_subscription_info() { - let (internal_api, _s) = create_api().await; + let internal_api = create_api().await; // The user must be registered let (user_sk, user_pk) = get_random_keypair(); @@ -1214,22 +984,19 @@ mod tests_public_api { // Get the subscription info though the 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), + .get_subscription_info(Request::new(msgs::GetSubscriptionInfoRequest { + signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(), })) .await .unwrap() .into_inner(); - assert!(matches!( - response, - common_msgs::GetSubscriptionInfoResponse { .. } - )); + assert!(matches!(response, msgs::GetSubscriptionInfoResponse { .. })) } #[tokio::test] async fn test_get_subscription_info_non_registered() { - let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0)).await; + let internal_api = create_api_with_config(ApiConfig::new(SLOTS, 0)).await; // The user is not registered let (user_sk, _) = get_random_keypair(); @@ -1237,8 +1004,8 @@ mod tests_public_api { // Try to get the subscription info though the 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), + .get_subscription_info(Request::new(msgs::GetSubscriptionInfoRequest { + signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(), })) .await { @@ -1252,7 +1019,7 @@ mod tests_public_api { #[tokio::test] async fn test_get_subscription_info_expired() { - let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0)).await; + let internal_api = create_api_with_config(ApiConfig::new(SLOTS, 0)).await; // The user is registered but the subscription has expired let (user_sk, user_pk) = get_random_keypair(); @@ -1261,8 +1028,8 @@ mod tests_public_api { // Try to get the subscription info though the 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), + .get_subscription_info(Request::new(msgs::GetSubscriptionInfoRequest { + signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(), })) .await { @@ -1276,14 +1043,14 @@ mod tests_public_api { #[tokio::test] async fn test_get_subscription_info_service_unavailable() { - let (internal_api, _s) = + let internal_api = create_api_with_config(ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable()).await; let (user_sk, _) = get_random_keypair(); 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), + .get_subscription_info(Request::new(msgs::GetSubscriptionInfoRequest { + signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(), })) .await { diff --git a/teos/src/api/mod.rs b/teos/src/api/mod.rs index e901b04..87c71cc 100644 --- a/teos/src/api/mod.rs +++ b/teos/src/api/mod.rs @@ -1,4 +1,43 @@ pub mod http; pub mod internal; -pub mod serde; -pub mod tor; + +pub mod serde_status { + use serde::de::{self, Deserializer}; + use serde::ser::Serializer; + use std::str::FromStr; + + use teos_common::appointment::AppointmentStatus; + + pub fn serialize(status: &i32, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&AppointmentStatus::from(*status).to_string()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct StatusVisitor; + + impl<'de> de::Visitor<'de> for StatusVisitor { + type Value = i32; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string containing the status") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + let status = AppointmentStatus::from_str(v) + .map_err(|_| E::custom("given status is unknown"))?; + Ok(status as i32) + } + } + + deserializer.deserialize_any(StatusVisitor) + } +} diff --git a/teos/src/api/serde.rs b/teos/src/api/serde.rs deleted file mode 100644 index d8c2183..0000000 --- a/teos/src/api/serde.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::protos as msgs; - -use teos_common::net::AddressType; - -impl msgs::NetworkAddress { - pub fn from_ipv4(address: String, port: u16) -> Self { - Self { - address_type: AddressType::IpV4 as i32, - address, - port: port as u32, - } - } - - pub fn from_torv3(address: String, port: u16) -> Self { - Self { - address_type: AddressType::TorV3 as i32, - address, - port: port as u32, - } - } -} - -pub mod serde_address_type { - use serde::de::{self, Deserializer}; - use serde::Serializer; - use std::str::FromStr; - - use super::AddressType; - - pub fn serialize(status: &i32, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&AddressType::from(*status).to_string()) - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct StatusVisitor; - - impl<'de> de::Visitor<'de> for StatusVisitor { - type Value = i32; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a string containing the address type") - } - - fn visit_str(self, v: &str) -> Result - where - E: de::Error, - { - let status = AddressType::from_str(v) - .map_err(|_| E::custom("given address type is unknown"))?; - Ok(status as i32) - } - } - - deserializer.deserialize_any(StatusVisitor) - } -} diff --git a/teos/src/api/tor.rs b/teos/src/api/tor.rs deleted file mode 100644 index d066125..0000000 --- a/teos/src/api/tor.rs +++ /dev/null @@ -1,210 +0,0 @@ -use std::convert::TryInto; -use std::io::{Error, ErrorKind}; -use std::net::SocketAddr; -use std::path::PathBuf; - -use tokio::fs; -use tokio::net::TcpStream; -use torut::control::UnauthenticatedConn; -use torut::onion::TorSecretKeyV3; -use triggered::{Listener, Trigger}; - -pub struct TorAPI { - sk: TorSecretKeyV3, - api_endpoint: SocketAddr, - onion_port: u16, - tor_control_port: u16, -} - -impl TorAPI { - pub async fn new( - api_endpoint: SocketAddr, - onion_port: u16, - tor_control_port: u16, - path: PathBuf, - ) -> Self { - let key = if let Some(key) = TorAPI::load_sk(path.clone()).await { - key - } else { - log::info!("Generating fresh Tor secret key"); - let key = TorSecretKeyV3::generate(); - TorAPI::store_sk(&key, path).await; - key - }; - - Self { - sk: key, - api_endpoint, - onion_port, - tor_control_port, - } - } - - pub fn get_onion_address(&self) -> String { - self.sk.public().get_onion_address().to_string() - } - - /// Loads a Tor key from disk (if found). - async fn load_sk(path: PathBuf) -> Option { - log::info!("Loading Tor secret key from disk"); - let key = fs::read(path.join("onion_v3_sk")) - .await - .map_err(|e| log::warn!("Tor secret key cannot be loaded. {e}")) - .ok()?; - let key: [u8; 64] = key - .try_into() - .map_err(|_| log::error!("Cannot convert loaded data into Tor secret key")) - .ok()?; - - Some(TorSecretKeyV3::from(key)) - } - - /// Stores a Tor key to disk. - async fn store_sk(key: &TorSecretKeyV3, path: PathBuf) { - if let Err(e) = fs::write(path.join("onion_v3_sk"), key.as_bytes()).await { - log::error!("Cannot store Tor secret key. {e}"); - } - } - - /// Tries to connect to the Tor control port - async fn connect_tor_cp(&self) -> Result { - let sock = TcpStream::connect(format!("127.0.0.1:{}", self.tor_control_port)) - .await - .map_err(|_| { - Error::new( - ErrorKind::ConnectionRefused, - "failed to connect to Tor control port", - ) - })?; - Ok(sock) - } - - /// Expose an onion service that re-directs to the public api. - pub async fn expose_onion_service( - &self, - service_ready: Trigger, - shutdown_signal_tor: Listener, - ) -> Result<(), Error> { - let stream = self - .connect_tor_cp() - .await - .map_err(|e| Error::new(ErrorKind::ConnectionRefused, e))?; - - let mut unauth_conn = UnauthenticatedConn::new(stream); - - let pre_auth = unauth_conn - .load_protocol_info() - .await - .map_err(|e| Error::new(ErrorKind::ConnectionRefused, e))?; - - let auth_data = pre_auth - .make_auth_data()? - .expect("failed to make auth data"); - - unauth_conn.authenticate(&auth_data).await.map_err(|_| { - Error::new( - ErrorKind::PermissionDenied, - "failed to authenticate with Tor", - ) - })?; - - let mut auth_conn = unauth_conn.into_authenticated().await; - - auth_conn.set_async_event_handler(Some(|_| async move { Ok(()) })); - - auth_conn - .add_onion_v3( - &self.sk, - false, - false, - false, - None, - &mut [(self.onion_port, self.api_endpoint)].iter(), - ) - .await - .map_err(|e| { - Error::new( - ErrorKind::Other, - format!("failed to create onion hidden service: {e}"), - ) - })?; - - log::info!( - "Onion service: {}:{}", - self.get_onion_address(), - self.onion_port - ); - service_ready.trigger(); - shutdown_signal_tor.await; - - auth_conn - .del_onion( - &self - .sk - .public() - .get_onion_address() - .get_address_without_dot_onion(), - ) - .await - .unwrap(); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use tempdir::TempDir; - - use teos_common::test_utils::get_random_user_id; - - #[tokio::test] - async fn test_store_load_sk() { - let key = TorSecretKeyV3::generate(); - let tmp_path = TempDir::new(&format!("data_dir_{}", get_random_user_id())).unwrap(); - - TorAPI::store_sk(&key, tmp_path.path().into()).await; - let loaded_key = TorAPI::load_sk(tmp_path.path().into()).await; - - assert_eq!(key, loaded_key.unwrap()) - } - - #[tokio::test] - async fn test_load_sk_inexistent() { - let tmp_path = TempDir::new(&format!("data_dir_{}", get_random_user_id())).unwrap(); - let loaded_key = TorAPI::load_sk(tmp_path.path().into()).await; - - assert_eq!(loaded_key, None); - } - - #[tokio::test] - async fn test_load_sk_wrong_format() { - let tmp_path = TempDir::new(&format!("data_dir_{}", get_random_user_id())).unwrap(); - fs::write(tmp_path.path().join("onion_v3_sk"), "random stuff") - .await - .unwrap(); - let loaded_key = TorAPI::load_sk(tmp_path.path().into()).await; - - assert_eq!(loaded_key, None); - } - - #[tokio::test] - async fn test_connect_tor_cp_fail() { - let wrong_cp = 9000; - let tmp_path = TempDir::new(&format!("data_dir_{}", get_random_user_id())).unwrap(); - let tor_api = TorAPI::new( - "127.0.1.1:9814".parse().unwrap(), - 9814, - wrong_cp, - tmp_path.path().into(), - ) - .await; - - match tor_api.connect_tor_cp().await { - Ok(_) => {} - Err(e) => { - assert_eq!("failed to connect to Tor control port", e.to_string()) - } - } - } -} diff --git a/teos/src/bitcoin_cli.rs b/teos/src/bitcoin_cli.rs index 4b73bde..6d66c28 100644 --- a/teos/src/bitcoin_cli.rs +++ b/teos/src/bitcoin_cli.rs @@ -9,19 +9,17 @@ * 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::Transaction; -use bitcoincore_rpc::{Auth, RawTx}; +use bitcoin::hashes::hex::ToHex; +use bitcoin::{Block, Transaction}; use lightning::util::ser::Writeable; -use lightning_block_sync::http::{HttpEndpoint, JsonResponse}; +use lightning_block_sync::http::HttpEndpoint; use lightning_block_sync::rpc::RpcClient; -use lightning_block_sync::{AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource}; +use lightning_block_sync::{AsyncBlockSourceResult, BlockHeaderData, BlockSource}; /// A simple implementation of a bitcoind client (`bitcoin-cli`) with the minimal functionality required by the tower. pub struct BitcoindClient<'a> { @@ -32,39 +30,39 @@ pub struct BitcoindClient<'a> { /// The port to connect to. port: u16, /// The RPC user `bitcoind` is configured with. - rpc_user: String, + rpc_user: &'a str, /// The RPC password for the given user. - rpc_password: String, + rpc_password: &'a str, } impl BlockSource for &BitcoindClient<'_> { /// Gets a block header given its hash. fn get_header<'a>( - &'a self, + &'a mut self, header_hash: &'a BlockHash, height_hint: Option, ) -> AsyncBlockSourceResult<'a, BlockHeaderData> { Box::pin(async move { - let rpc = self.bitcoind_rpc_client.lock().await; + let mut rpc = self.bitcoind_rpc_client.lock().await; rpc.get_header(header_hash, height_hint).await }) } /// Gets a block given its hash. fn get_block<'a>( - &'a self, + &'a mut self, header_hash: &'a BlockHash, - ) -> AsyncBlockSourceResult<'a, BlockData> { + ) -> AsyncBlockSourceResult<'a, Block> { Box::pin(async move { - let rpc = self.bitcoind_rpc_client.lock().await; + let mut rpc = self.bitcoind_rpc_client.lock().await; rpc.get_block(header_hash).await }) } /// Get the best block known by our node. - fn get_best_block(&self) -> AsyncBlockSourceResult<(BlockHash, Option)> { + fn get_best_block(&mut self) -> AsyncBlockSourceResult<(BlockHash, Option)> { Box::pin(async move { - let rpc = self.bitcoind_rpc_client.lock().await; + let mut rpc = self.bitcoind_rpc_client.lock().await; rpc.get_best_block().await }) } @@ -77,34 +75,12 @@ impl<'a> BitcoindClient<'a> { pub async fn new( host: &'a str, port: u16, - auth: Auth, - teos_network: &'a str, + rpc_user: &'a str, + rpc_password: &'a str, ) -> std::io::Result> { let http_endpoint = HttpEndpoint::for_host(host.to_owned()).with_port(port); - 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 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)), @@ -114,25 +90,17 @@ impl<'a> BitcoindClient<'a> { rpc_password, }; - // Test that bitcoind is reachable. - let btc_network = client.get_chain().await?; - - // Assert teos runs on the same chain/network as bitcoind. - if btc_network != teos_network { - Err(Error::new( - ErrorKind::InvalidInput, - format!("bitcoind is running on {btc_network} but teosd is set to run on {teos_network}"), - )) - } else { - Ok(client) + // Test that bitcoind is reachable + match client.get_best_block_hash_and_height().await { + Ok(_) => Ok(client), + Err(e) => Err(e), } } /// Gets a fresh RPC client. - pub fn get_new_rpc_client(&self) -> RpcClient { + pub fn get_new_rpc_client(&self) -> std::io::Result { 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) } @@ -140,46 +108,26 @@ impl<'a> BitcoindClient<'a> { pub async fn get_best_block_hash_and_height( &self, ) -> Result<(BlockHash, Option), std::io::Error> { - let rpc = self.bitcoind_rpc_client.lock().await; + let mut rpc = self.bitcoind_rpc_client.lock().await; rpc.call_method::<(BlockHash, Option)>("getblockchaininfo", &[]) .await } /// Sends a transaction to the network. pub async fn send_raw_transaction(&self, raw_tx: &Transaction) -> Result { - let rpc = self.bitcoind_rpc_client.lock().await; + let mut rpc = self.bitcoind_rpc_client.lock().await; - let raw_tx_json = serde_json::json!(raw_tx.encode().raw_hex()); + let raw_tx_json = serde_json::json!(raw_tx.encode().to_hex()); rpc.call_method::("sendrawtransaction", &[raw_tx_json]) .await } /// Gets a transaction given its id. pub async fn get_raw_transaction(&self, txid: &Txid) -> Result { - let rpc = self.bitcoind_rpc_client.lock().await; + let mut rpc = self.bitcoind_rpc_client.lock().await; - let txid_hex = serde_json::json!(txid.encode().raw_hex()); + let txid_hex = serde_json::json!(txid.encode().to_hex()); rpc.call_method::("getrawtransaction", &[txid_hex]) .await } - - /// Gets bitcoind's network. - pub async fn get_chain(&self) -> std::io::Result { - // A wrapper type to extract "chain" key from getblockchaininfo JsonResponse. - struct BtcNetwork(String); - impl TryInto for JsonResponse { - type Error = std::io::Error; - fn try_into(self) -> std::io::Result { - Ok(BtcNetwork(self.0["chain"].as_str().unwrap().to_string())) - } - } - - // Ask the RPC client for the network bitcoind is running on. - let rpc = self.bitcoind_rpc_client.lock().await; - let btc_network = rpc - .call_method::("getblockchaininfo", &[]) - .await?; - - Ok(btc_network.0) - } } diff --git a/teos/src/carrier.rs b/teos/src/carrier.rs index 75d0fe6..51e58d2 100644 --- a/teos/src/carrier.rs +++ b/teos/src/carrier.rs @@ -6,7 +6,7 @@ use std::sync::{Arc, Condvar, Mutex}; use crate::responder::ConfirmationStatus; use crate::{errors, rpc_errors}; -use bitcoin::{Transaction, Txid}; +use bitcoin::{BlockHash, Transaction, Txid}; use bitcoincore_rpc::{ jsonrpc::error::Error::Rpc as RpcError, jsonrpc::error::Error::Transport as TransportError, Client as BitcoindClient, Error::JsonRpc as JsonRpcError, RpcApi, @@ -22,7 +22,7 @@ pub struct Carrier { /// A map of receipts already issued by the [Carrier]. /// Used to prevent potentially re-sending the same transaction over and over. issued_receipts: HashMap, - /// The last known block height. + /// The last known block header. block_height: u32, } @@ -41,11 +41,6 @@ impl Carrier { } } - /// The last known block height. - pub(crate) fn block_height(&self) -> u32 { - self.block_height - } - /// Clears the receipts cached by the [Carrier]. Should be called periodically to prevent it from /// growing unbounded. pub(crate) fn clear_receipts(&mut self) { @@ -80,49 +75,49 @@ 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.compute_txid()) { - log::info!("Transaction already sent: {}", tx.compute_txid()); + if let Some(receipt) = self.issued_receipts.get(&tx.txid()) { + log::info!("Transaction already sent: {}", tx.txid()); return *receipt; } - log::info!("Pushing transaction to the network: {}", tx.compute_txid()); + log::info!("Pushing transaction to the network: {}", tx.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.compute_txid()); + log::info!("Transaction successfully delivered: {}", tx.txid()); ConfirmationStatus::InMempoolSince(self.block_height) } Err(JsonRpcError(RpcError(rpcerr))) => match rpcerr.code { // Since we're pushing a raw transaction to the network we can face several rejections rpc_errors::RPC_VERIFY_REJECTED => { - log::error!("Transaction couldn't be broadcast. {rpcerr:?}"); + log::error!("Transaction couldn't be broadcast. {:?}", rpcerr); ConfirmationStatus::Rejected(rpc_errors::RPC_VERIFY_REJECTED) } rpc_errors::RPC_VERIFY_ERROR => { - log::error!("Transaction couldn't be broadcast. {rpcerr:?}"); + log::error!("Transaction couldn't be broadcast. {:?}", rpcerr); ConfirmationStatus::Rejected(rpc_errors::RPC_VERIFY_ERROR) } rpc_errors::RPC_VERIFY_ALREADY_IN_CHAIN => { log::info!( - "Transaction was confirmed long ago, not keeping track of it: {}", - tx.compute_txid() + "Transaction is already in the blockchain: {}. Getting confirmation count", + tx.txid() ); - // Given we are not using txindex, if a transaction bounces we cannot get its confirmation count. However, [send_transaction] is guarded by - // checking whether the transaction id can be found in the [Responder]'s [TxIndex], meaning that if the transaction bounces it was confirmed long - // ago (> IRREVOCABLY_RESOLVED), so we don't need to worry about it. - ConfirmationStatus::IrrevocablyResolved + ConfirmationStatus::ConfirmedIn(self.get_tx_height(&tx.txid()).unwrap()) } 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.compute_txid()); + log::info!("Transaction cannot be deserialized: {}", tx.txid()); ConfirmationStatus::Rejected(rpc_errors::RPC_DESERIALIZATION_ERROR) } _ => { // If something else happens (unlikely but possible) log it so we can treat it in future releases. - log::error!("Unexpected rpc error when calling sendrawtransaction: {rpcerr:?}"); + log::error!( + "Unexpected rpc error when calling sendrawtransaction: {:?}", + rpcerr + ); ConfirmationStatus::Rejected(errors::UNKNOWN_JSON_RPC_EXCEPTION) } }, @@ -134,48 +129,87 @@ impl Carrier { } Err(e) => { // TODO: This may need finer catching. - log::error!("Unexpected error when calling sendrawtransaction: {e:?}"); + log::error!("Unexpected error when calling sendrawtransaction: {:?}", e); ConfirmationStatus::Rejected(errors::UNKNOWN_JSON_RPC_EXCEPTION) } }; - self.issued_receipts.insert(tx.compute_txid(), receipt); + self.issued_receipts.insert(tx.txid(), receipt); receipt } - /// Checks whether a given transaction can be found in the mempool. - /// - /// This uses `getrawtransaction` under the hood and, therefore, its behavior depends on whether `txindex` is enabled in bitcoind. - /// If `txindex` is disabled (default), it will only pull data from the mempool. Otherwise, it will also pull data from the transaction - /// index. Hence, we need to check whether the returned struct has any of the block related datum set (such as `blockhash`). - pub(crate) fn in_mempool(&self, txid: &Txid) -> bool { + /// Gets the block height at where a given [Transaction] was confirmed at (if any). + fn get_tx_height(&self, txid: &Txid) -> Option { + if let Some(block_hash) = self.get_block_hash_for_tx(txid) { + self.get_block_height(&block_hash) + } else { + None + } + } + + /// Queries the height of a given [Block](bitcoin::Block). Returns it if the block can be found. Returns [None] otherwise. + fn get_block_height(&self, block_hash: &BlockHash) -> Option { self.hang_until_bitcoind_reachable(); - match self.bitcoin_cli.get_raw_transaction_info(txid, None) { - Ok(tx) => tx.blockhash.is_none(), + match self.bitcoin_cli.get_block_header_info(block_hash) { + Ok(header_data) => Some(header_data.height as u32), Err(JsonRpcError(RpcError(rpcerr))) => match rpcerr.code { rpc_errors::RPC_INVALID_ADDRESS_OR_KEY => { - log::info!("Transaction not found in mempool: {txid}"); - false + log::info!("Block not found: {}", block_hash); + None } e => { - // DISCUSS: This could result in a silent error with unknown consequences - log::error!("Unexpected error code when calling getrawtransaction: {e}"); - false + log::error!("Unexpected error code when calling getblockheader: {}", e); + None } }, Err(JsonRpcError(TransportError(_))) => { // Connection refused, bitcoind is down. log::error!("Connection lost with bitcoind, retrying request when possible"); self.flag_bitcoind_unreachable(); - self.in_mempool(txid) + self.get_block_height(block_hash) } // TODO: This may need finer catching. Err(e) => { - // DISCUSS: This could result in a silent error with unknown consequences - log::error!("Unexpected JSONRPCError when calling getrawtransaction: {e}"); - false + log::error!("Unexpected JSONRPCError when calling getblockheader: {}", e); + None + } + } + } + + /// Gets the block hash where a given [Transaction] was confirmed at (if any). + pub(crate) fn get_block_hash_for_tx(&self, txid: &Txid) -> Option { + self.hang_until_bitcoind_reachable(); + + match self.bitcoin_cli.get_raw_transaction_info(txid, None) { + Ok(tx_data) => tx_data.blockhash, + Err(JsonRpcError(RpcError(rpcerr))) => match rpcerr.code { + rpc_errors::RPC_INVALID_ADDRESS_OR_KEY => { + log::info!("Transaction not found in mempool nor blockchain: {}", txid); + None + } + e => { + log::error!( + "Unexpected error code when calling getrawtransaction: {}", + e + ); + None + } + }, + Err(JsonRpcError(TransportError(_))) => { + // Connection refused, bitcoind is down. + log::error!("Connection lost with bitcoind, retrying request when possible"); + self.flag_bitcoind_unreachable(); + self.get_block_hash_for_tx(txid) + } + // TODO: This may need finer catching. + Err(e) => { + log::error!( + "Unexpected JSONRPCError when calling getrawtransaction: {}", + e + ); + None } } } @@ -184,13 +218,13 @@ 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}; - use teos_common::test_utils::{TXID_HEX, TX_HEX}; + use crate::test_utils::{ + get_random_tx, start_server, BitcoindMock, MockOptions, START_HEIGHT, TX_HEX, + }; - use bitcoin::consensus; + use bitcoin::consensus::deserialize; use bitcoin::hashes::hex::FromHex; use bitcoincore_rpc::Auth; @@ -208,17 +242,18 @@ mod tests { #[test] fn test_clear_receipts() { - let bitcoind_mock = BitcoindMock::new(MockOptions::default()); + let bitcoind_mock = BitcoindMock::new(MockOptions::empty()); let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; + start_server(bitcoind_mock); let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); // Lets add some dummy data into the cache for i in 0..10 { carrier.issued_receipts.insert( - get_random_tx().compute_txid(), + get_random_tx().txid(), ConfirmationStatus::ConfirmedIn(start_height - i), ); } @@ -231,38 +266,20 @@ mod tests { #[test] fn test_send_transaction_ok() { - let bitcoind_mock = BitcoindMock::new(MockOptions::default()); + let bitcoind_mock = BitcoindMock::new(MockOptions::empty()); let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock.server); + start_server(bitcoind_mock); let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - let tx = consensus::deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); + let tx = deserialize::(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); let r = carrier.send_transaction(&tx); assert_eq!(r, ConfirmationStatus::InMempoolSince(start_height)); // Check the receipt is on the cache - assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r); - } - - #[test] - fn test_send_transaction_ok_already_in_mempool() { - let bitcoind_mock = BitcoindMock::new(MockOptions::in_mempool()); - let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); - let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); - let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock.server); - - let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - let tx = consensus::deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); - let r = carrier.send_transaction(&tx); - - assert_eq!(r, ConfirmationStatus::InMempoolSince(start_height)); - - // Check the receipt is on the cache - assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r); + assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r); } #[test] @@ -273,10 +290,10 @@ mod tests { let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock.server); + start_server(bitcoind_mock); let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - let tx = consensus::deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); + let tx = deserialize::(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); let r = carrier.send_transaction(&tx); assert_eq!( @@ -285,7 +302,7 @@ mod tests { ); // Check the receipt is on the cache - assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r); + assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r); } #[test] @@ -295,10 +312,10 @@ mod tests { let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock.server); + start_server(bitcoind_mock); let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - let tx = consensus::deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); + let tx = deserialize::(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); let r = carrier.send_transaction(&tx); assert_eq!( @@ -307,27 +324,29 @@ mod tests { ); // Check the receipt is on the cache - assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r); + assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r); } #[test] fn test_send_transaction_verify_already_in_chain() { - let bitcoind_mock = BitcoindMock::new(MockOptions::with_error( + let bitcoind_mock = BitcoindMock::new(MockOptions::new( rpc_errors::RPC_VERIFY_ALREADY_IN_CHAIN as i64, + BlockHash::default(), + START_HEIGHT, )); let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock.server); + start_server(bitcoind_mock); let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - let tx = consensus::deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); + let tx = deserialize::(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); let r = carrier.send_transaction(&tx); - assert_eq!(r, ConfirmationStatus::IrrevocablyResolved); + assert_eq!(r, ConfirmationStatus::ConfirmedIn(start_height)); // Check the receipt is on the cache - assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r); + assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r); } #[test] @@ -337,10 +356,10 @@ mod tests { let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock.server); + start_server(bitcoind_mock); let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - let tx = consensus::deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); + let tx = deserialize::(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); let r = carrier.send_transaction(&tx); assert_eq!( @@ -349,19 +368,19 @@ mod tests { ); // Check the receipt is on the cache - assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r); + assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r); } #[test] fn test_send_transaction_connection_error() { // Try to connect to an offline bitcoind. - let bitcoind_mock = BitcoindMock::new(MockOptions::default()); + let bitcoind_mock = BitcoindMock::new(MockOptions::empty()); let bitcoind_reachable = Arc::new((Mutex::new(false), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable.clone(), start_height); - let tx = consensus::deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); + let tx = deserialize::(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); let delay = std::time::Duration::new(3, 0); thread::spawn(move || { @@ -382,86 +401,91 @@ mod tests { } #[test] - fn test_in_mempool() { - let bitcoind_mock = BitcoindMock::new(MockOptions::in_mempool()); - let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); - let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); - let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock.server); - - let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - let txid = Txid::from_str(TXID_HEX).unwrap(); - assert!(carrier.in_mempool(&txid)); - } - - #[test] - fn test_not_in_mempool() { - let bitcoind_mock = BitcoindMock::new(MockOptions::default()); - let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); - let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); - let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock.server); - - let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - let txid = Txid::from_str(TXID_HEX).unwrap(); - assert!(!carrier.in_mempool(&txid)); - } - - #[test] - fn test_not_in_mempool_via_error() { - let bitcoind_mock = BitcoindMock::new(MockOptions::with_error( - rpc_errors::RPC_INVALID_ADDRESS_OR_KEY as i64, - )); - let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); - let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); - let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock.server); - - let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - let txid = Txid::from_str(TXID_HEX).unwrap(); - assert!(!carrier.in_mempool(&txid)); - } - - #[test] - fn test_in_mempool_unexpected_error() { + fn test_get_tx_height_ok() { + let target_height = 21; let bitcoind_mock = - BitcoindMock::new(MockOptions::with_error(rpc_errors::RPC_MISC_ERROR as i64)); + BitcoindMock::new(MockOptions::with_block(BlockHash::default(), target_height)); let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let start_height = START_HEIGHT as u32; - start_server(bitcoind_mock.server); + start_server(bitcoind_mock); let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); - let txid = Txid::from_str(TXID_HEX).unwrap(); - assert!(!carrier.in_mempool(&txid)); - } - - #[test] - fn test_in_mempool_connection_error() { - // Try to connect to an offline bitcoind. - let bitcoind_mock = BitcoindMock::new(MockOptions::default()); - let bitcoind_reachable = Arc::new((Mutex::new(false), Condvar::new())); - let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); - let start_height = START_HEIGHT as u32; - let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable.clone(), start_height); - - let txid = Txid::from_str(TXID_HEX).unwrap(); - let delay = std::time::Duration::new(3, 0); - - thread::spawn(move || { - thread::sleep(delay); - let (reachable, notifier) = &*bitcoind_reachable; - *reachable.lock().unwrap() = true; - notifier.notify_all(); - }); - - let before = std::time::Instant::now(); - carrier.in_mempool(&txid); - - // Check the request has hanged for ~delay + let tx = deserialize::(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); assert_eq!( - (std::time::Instant::now() - before).as_secs(), - delay.as_secs() + carrier.get_tx_height(&tx.txid()), + Some(target_height as u32) ); } + + #[test] + fn test_get_tx_height_not_found() { + // Hee we are not testing the case where the block hash is unknown (which will also return None). This is because we only + // learn block hashes from bitcoind, and once a block is known, it cannot disappear (ir can be disconnected, but not banish). + let bitcoind_mock = BitcoindMock::new(MockOptions::empty()); + let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); + let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); + let start_height = START_HEIGHT as u32; + start_server(bitcoind_mock); + + let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); + let tx = deserialize::(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); + assert_eq!(carrier.get_tx_height(&tx.txid()), None); + } + + #[test] + fn test_get_block_height_ok() { + let target_height = 21; + let block_hash = BlockHash::default(); + let bitcoind_mock = BitcoindMock::new(MockOptions::with_block(block_hash, target_height)); + let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); + let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); + let start_height = START_HEIGHT as u32; + start_server(bitcoind_mock); + + let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); + assert_eq!( + carrier.get_block_height(&block_hash), + Some(target_height as u32) + ); + } + + #[test] + fn test_get_block_height_not_found() { + let bitcoind_mock = BitcoindMock::new(MockOptions::empty()); + let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); + let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); + let start_height = START_HEIGHT as u32; + start_server(bitcoind_mock); + + let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); + assert_eq!(carrier.get_block_height(&BlockHash::default()), None); + } + + #[test] + fn test_get_block_hash_for_tx_ok() { + let block_hash = BlockHash::default(); + let bitcoind_mock = BitcoindMock::new(MockOptions::with_block(block_hash, 21)); + let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); + let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); + let start_height = START_HEIGHT as u32; + start_server(bitcoind_mock); + + let tx = deserialize::(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); + let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); + assert_eq!(carrier.get_block_hash_for_tx(&tx.txid()), Some(block_hash)); + } + + #[test] + fn test_get_block_hash_for_tx_not_found() { + let bitcoind_mock = BitcoindMock::new(MockOptions::empty()); + let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); + let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); + let start_height = START_HEIGHT as u32; + start_server(bitcoind_mock); + + let tx = deserialize::(&Vec::from_hex(TX_HEX).unwrap()).unwrap(); + let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height); + assert_eq!(carrier.get_block_hash_for_tx(&tx.txid()), None); + } } diff --git a/teos/src/chain_monitor.rs b/teos/src/chain_monitor.rs index 50a690b..9c9f2e6 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 last known block header by the [ChainMonitor]. + /// The lat 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>, @@ -100,7 +100,7 @@ where Err(e) => match e.kind() { BlockSourceErrorKind::Persistent => { // FIXME: This may need finer catching - log::error!("Unexpected persistent error: {e:?}"); + log::error!("Unexpected persistent error: {:?}", e); } BlockSourceErrorKind::Transient => { // Treating all transient as connection errors at least for now. @@ -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}; @@ -156,18 +156,13 @@ mod tests { } impl chain::Listen for DummyListener { - fn filtered_block_connected( - &self, - header: &bitcoin::block::Header, - _: &chain::transaction::TransactionData, - _: u32, - ) { + fn block_connected(&self, block: &bitcoin::Block, _: u32) { self.connected_blocks .borrow_mut() - .insert(header.block_hash()); + .insert(block.block_hash()); } - fn block_disconnected(&self, header: &bitcoin::block::Header, _: u32) { + fn block_disconnected(&self, header: &bitcoin::BlockHeader, _: u32) { self.disconnected_blocks .borrow_mut() .insert(header.block_hash()); @@ -264,7 +259,10 @@ 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!(cm.dbm.lock().unwrap().load_last_known_block().is_none()); + assert!(matches!( + cm.dbm.lock().unwrap().load_last_known_block(), + Err { .. } + )); 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 93ca300..47a36bc 100644 --- a/teos/src/cli.rs +++ b/teos/src/cli.rs @@ -1,97 +1,49 @@ -use hex::FromHex; use serde_json::to_string_pretty as pretty_json; +use std::fs; use std::str::FromStr; use structopt::StructOpt; -use tokio::fs; -use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; use tonic::Request; use teos::cli_config::{Command, Config, Opt}; use teos::config; use teos::protos as msgs; 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(); let path = config::data_dir_absolute_path(opt.data_dir.clone()); // Create data dir if it does not exist - fs::create_dir_all(&path).await.unwrap_or_else(|e| { - eprintln!("Cannot create data dir: {e:?}"); + fs::create_dir_all(&path).unwrap_or_else(|e| { + eprintln!("Cannot create data dir: {:?}", e); std::process::exit(1); }); 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")) - .await - .expect("unable to read client key from disk"); - let certificate = fs::read(path.join("client.pem")) - .await - .expect("unable to read client cert from disk"); - let ca_cert = Certificate::from_pem( - fs::read(path.join("ca.pem")) + // Create gRPC client and send request + let mut client = + PrivateTowerServicesClient::connect(format!("http://{}:{}", conf.rpc_bind, conf.rpc_port)) .await - .expect("unable to read ca cert from disk"), - ); - - let tls = ClientTlsConfig::new() - .domain_name("localhost") - .ca_certificate(ca_cert) - .identity(Identity::from_pem(certificate, key)); - - 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| { - eprintln!("Could not configure tls: {e:?}"); - std::process::exit(1); - }) - .connect() - .await - .unwrap_or_else(|_| { - eprintln!("Could not connect to tower. Is teosd running?"); - std::process::exit(1); - }); - - let mut client = PrivateTowerServicesClient::new(channel); + .unwrap_or_else(|e| { + eprintln!("Cannot connect to the tower. Connection refused"); + if conf.debug { + eprintln!("{:?}", e); + } + std::process::exit(1); + }); match command { Command::GetAllAppointments => { let appointments = client.get_all_appointments(Request::new(())).await.unwrap(); println!("{}", pretty_json(&appointments.into_inner()).unwrap()); } - Command::GetAppointments(appointments_data) => { - match Locator::from_hex(&appointments_data.locator) { - Ok(locator) => { - match client - .get_appointments(Request::new(msgs::GetAppointmentsRequest { - locator: locator.to_vec(), - })) - .await - { - Ok(appointments) => { - println!("{}", pretty_json(&appointments.into_inner()).unwrap()) - } - Err(status) => handle_error(status.message()), - } - } - Err(e) => handle_error(e), - }; - } Command::GetTowerInfo => { let info = client.get_tower_info(Request::new(())).await.unwrap(); println!("{}", pretty_json(&info.into_inner()).unwrap()) @@ -100,22 +52,22 @@ async fn main() { let users = client.get_users(Request::new(())).await.unwrap(); println!("{}", pretty_json(&users.into_inner()).unwrap()); } - Command::GetUser(user) => { - match UserId::from_str(&user.user_id) { + Command::GetUser(data) => { + match UserId::from_str(&data.user_id) { Ok(user_id) => { match client .get_user(Request::new(msgs::GetUserRequest { - user_id: user_id.to_vec(), + user_id: user_id.serialize(), })) .await { Ok(response) => { println!("{}", pretty_json(&response.into_inner()).unwrap()) } - Err(status) => handle_error(status.message()), + Err(status) => println!("{}", status.message()), } } - Err(e) => handle_error(e), + Err(e) => println!("{}", e), }; } Command::Stop => { diff --git a/teos/src/cli_config.rs b/teos/src/cli_config.rs index ba085b8..888b7fb 100644 --- a/teos/src/cli_config.rs +++ b/teos/src/cli_config.rs @@ -4,12 +4,10 @@ use serde::Deserialize; use structopt::StructOpt; #[derive(Debug, StructOpt, Clone)] -#[structopt(rename_all = "lower_case")] +#[structopt(rename_all = "snake_case")] pub enum Command { /// Gets information about all appointments stored in the tower GetAllAppointments, - /// Gets information about specific appointments stored in the tower using a locator - GetAppointments(GetAppointmentsData), /// Gets generic information about the tower, like tower id and aggregate data on users and appointments GetTowerInfo, /// Gets an array with the user ids of all the users registered to the tower @@ -21,23 +19,17 @@ pub enum Command { } #[derive(Debug, StructOpt, Clone)] -#[structopt(rename_all = "snake_case")] +#[structopt(rename_all = "lowercase")] pub struct GetUserData { /// The user identifier (33-byte compressed public key). pub user_id: String, } -#[derive(Debug, StructOpt, Clone)] -pub struct GetAppointmentsData { - /// The locator of the appointments (16-byte hexadecimal string). - pub locator: String, -} - /// Holds all the command line options and commands. #[derive(StructOpt, Debug)] #[structopt(rename_all = "lowercase")] #[structopt( - version = env!("CARGO_PKG_VERSION"), + version = "0.0.1", about = "The Eye of Satoshi - CLI", name = "teos-cli" )] @@ -54,6 +46,10 @@ pub struct Opt { #[structopt(long, default_value = "~/.teos")] pub data_dir: String, + /// Runs teos-cli in debug mode [default: false] + #[structopt(long)] + pub debug: bool, + /// Command #[structopt(subcommand)] pub command: Command, @@ -70,6 +66,7 @@ pub struct Opt { pub struct Config { pub rpc_bind: String, pub rpc_port: u16, + pub debug: bool, } impl Config { @@ -81,6 +78,8 @@ impl Config { if options.rpc_port.is_some() { self.rpc_port = options.rpc_port.unwrap(); } + + self.debug |= options.debug; } } @@ -95,6 +94,7 @@ impl Default for Config { Self { rpc_bind: "localhost".into(), rpc_port: 8814, + debug: false, } } } diff --git a/teos/src/conf_template.toml b/teos/src/conf_template.toml index 6193b35..a895063 100644 --- a/teos/src/conf_template.toml +++ b/teos/src/conf_template.toml @@ -1,26 +1,20 @@ # API api_bind = "127.0.0.1" api_port = 9814 -tor_control_port = 9051 -onion_hidden_service_port = 9814 -tor_support = false # RPC rpc_bind = "127.0.0.1" rpc_port = 8814 # bitcoind -btc_network = "mainnet" +btc_network = "bitcoin" 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 +btc_rpc_port = 8442 # Flags debug = false -deps_debug = false overwrite_key = false # General diff --git a/teos/src/config.rs b/teos/src/config.rs index f1db3de..473a0f1 100644 --- a/teos/src/config.rs +++ b/teos/src/config.rs @@ -1,7 +1,10 @@ //! Logic related to the tower configuration and command line parameter parsing. -use serde::{Deserialize, Serialize}; +use bitcoin::network::constants::Network; +use serde::Deserialize; +use std; use std::path::PathBuf; +use std::str::FromStr; use structopt::StructOpt; pub fn data_dir_absolute_path(data_dir: String) -> PathBuf { @@ -16,13 +19,24 @@ pub fn data_dir_absolute_path(data_dir: String) -> PathBuf { } } -pub fn from_file(path: &PathBuf) -> T { - match std::fs::read(path) { - Ok(file_content) => toml::from_slice::(&file_content).unwrap_or_else(|e| { - eprintln!("Couldn't parse config file: {e}"); +pub fn from_file(path: PathBuf) -> T { + match std::fs::read(&path) { + Ok(file_content) => toml::from_slice::(&file_content).map_or_else( + |e| { + println!("Couldn't parse config file: {}", e); + println!("Loading default configuration"); + T::default() + }, + |config| { + println!("Loading configuration from file"); + config + }, + ), + Err(e) => { + println!("Couldn't read config file: {}", e); + println!("Loading default configuration"); T::default() - }), - Err(_) => T::default(), + } } } @@ -38,18 +52,10 @@ 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")] -#[structopt(version = env!("CARGO_PKG_VERSION"), about = "The Eye of Satoshi - Lightning watchtower")] +#[structopt(version = "0.0.1", about = "The Eye of Satoshi - Lightning watchtower")] pub struct Opt { /// Address teos HTTP(s) API will bind to [default: localhost] #[structopt(long)] @@ -67,22 +73,18 @@ pub struct Opt { #[structopt(long)] pub rpc_port: Option, - /// Network bitcoind is connected to. Either mainnet, testnet, signet or regtest [default: mainnet] + /// Network bitcoind is connected to. Either bitcoin, testnet, signet or regtest [default: bitcoin] #[structopt(long)] pub btc_network: Option, - /// bitcoind rpcuser + /// bitcoind rpcuser [default: user] #[structopt(long)] pub btc_rpc_user: Option, - /// bitcoind rpcpassword + /// bitcoind rpcpassword [default: passwd] #[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, @@ -99,30 +101,9 @@ pub struct Opt { #[structopt(long)] pub debug: bool, - /// Runs third party libs in debug mode - #[structopt(long)] - pub deps_debug: bool, - /// Overwrites the tower secret key. THIS IS IRREVERSIBLE AND WILL CHANGE YOUR TOWER ID #[structopt(long)] pub overwrite_key: bool, - - /// If set, creates a Tor endpoint to serve API data. This endpoint is additional to the clearnet HTTP API - #[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, - - /// Port for the onion hidden service to listen on [default: 9814] - #[structopt(long)] - pub onion_hidden_service_port: Option, } /// Holds all configuration options. @@ -131,7 +112,7 @@ pub struct Opt { /// - Defaults /// - Configuration file /// - Command line options -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[derive(Debug, Deserialize, Clone, PartialEq, Eq)] #[serde(default)] pub struct Config { // API @@ -145,16 +126,13 @@ 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, // Flags pub debug: bool, - pub deps_debug: bool, pub overwrite_key: bool, - pub force_update: bool, // General pub subscription_slots: u32, @@ -166,32 +144,9 @@ pub struct Config { // Internal API pub internal_api_bind: String, pub internal_api_port: u32, - - // Tor - pub tor_support: bool, - pub tor_control_port: u16, - pub onion_hidden_service_port: u16, } 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() { @@ -215,27 +170,15 @@ 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(); } if options.btc_rpc_port.is_some() { self.btc_rpc_port = options.btc_rpc_port.unwrap(); } - if options.tor_control_port.is_some() { - self.tor_control_port = options.tor_control_port.unwrap(); - } - if options.onion_hidden_service_port.is_some() { - self.onion_hidden_service_port = options.onion_hidden_service_port.unwrap(); - } - self.tor_support |= options.tor_support; 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. @@ -247,60 +190,30 @@ 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> { - 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(), - )); + 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())); } - // Normalize the network option to the ones used by bitcoind. - if ["mainnet", "testnet"].contains(&self.btc_network.as_str()) { - self.btc_network = self.btc_network.trim_end_matches("net").into(); - } - - let default_rpc_port = match self.btc_network.as_str() { - "main" => 8332, - "test" => 18332, - "regtest" => 18443, - "signet" => 38332, - _ => return Err(ConfigError(format!("btc_network not recognized. Expected {{mainnet, testnet, signet, regtest}}, received {}", self.btc_network))) - }; - - // Set the port to it's default (depending on the network) if it has not been - // overwritten at this point. - if self.btc_rpc_port == 0 { - self.btc_rpc_port = default_rpc_port; - } - - Ok(()) - } - - /// Checks whether the config has been set with only with default values. - 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() + match Network::from_str(&self.btc_network) { + Ok(network) => { + // Set the port to it's default (depending on the network) if it has not been + // overwritten at this point. + if self.btc_rpc_port == 0 { + self.btc_rpc_port = match network { + Network::Testnet => 18333, + Network::Signet => 38333, + Network::Regtest => 18443, + _ => 8442, } - ); + } + Ok(()) + } + Err(_) => { + Err(ConfigError(format!("btc_network not recognized. Expected {{bitcoin, testnet, signet, regtest}}, received {}", + self.btc_network))) } } } @@ -317,22 +230,16 @@ impl Default for Config { Self { api_bind: "127.0.0.1".into(), api_port: 9814, - tor_support: false, - tor_control_port: 9051, - onion_hidden_service_port: 9814, rpc_bind: "127.0.0.1".into(), rpc_port: 8814, - btc_network: "mainnet".into(), + btc_network: "bitcoin".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, @@ -353,23 +260,17 @@ mod tests { Self { api_bind: None, api_port: None, - tor_support: false, - tor_control_port: None, - onion_hidden_service_port: None, rpc_bind: None, rpc_port: None, 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"), debug: false, - deps_debug: false, overwrite_key: false, - force_update: false, } } } @@ -389,7 +290,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.clone_from(&config_clone.api_bind); + config.api_bind = config_clone.api_bind.clone(); assert_eq!(config, config_clone); } @@ -398,9 +299,7 @@ mod tests { // Tests that the default configuration does not pass verification checks. This is on purpose so some fields are // required to be updated by the user. let mut config = Config::default(); - assert!( - matches!(config.verify(), Err(ConfigError(e)) if e.contains("No valid bitcoind auth provided")) - ); + assert!(matches!(config.verify(), Err(ConfigError { .. }))); } #[test] @@ -423,20 +322,6 @@ mod tests { btc_network: "wrong_network".to_owned(), ..Default::default() }; - assert!( - matches!(config.verify(), Err(ConfigError(e)) if e.contains("btc_network not recognized")) - ); - } - - #[test] - fn test_config_verify_tor_set() { - let mut config = Config { - btc_rpc_user: "user".to_owned(), - btc_rpc_password: "password".to_owned(), - tor_support: true, - ..Default::default() - }; - - config.verify().unwrap() + assert!(matches!(config.verify(), Err(ConfigError { .. }))); } } diff --git a/teos/src/dbm.rs b/teos/src/dbm.rs index 7322628..83729f5 100644 --- a/teos/src/dbm.rs +++ b/teos/src/dbm.rs @@ -1,68 +1,37 @@ -//! Logic related to the tower database manager (DBM), component in charge of persisting data on disk. +//! Logic related to the database manager (DBM), component in charge of persisting data on disk. //! -use std::collections::HashMap; -use std::iter::FromIterator; +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::str::FromStr; +use rusqlite::ffi::{SQLITE_CONSTRAINT_FOREIGNKEY, SQLITE_CONSTRAINT_PRIMARYKEY}; use rusqlite::limits::Limit; -use rusqlite::{params, params_from_iter, Connection, Error as SqliteError}; +use rusqlite::{params, params_from_iter, Connection, Error as SqliteError, ErrorCode, Params}; -use bitcoin::consensus; +use bitcoin::consensus::deserialize; use bitcoin::hashes::Hash; use bitcoin::secp256k1::SecretKey; -use bitcoin::BlockHash; +use bitcoin::util::psbt::serialize::Serialize; +use bitcoin::{BlockHash, Transaction}; use teos_common::appointment::{Appointment, Locator}; -use teos_common::dbm::{DatabaseConnection, DatabaseManager, Error}; +use teos_common::constants::ENCRYPTED_BLOB_MAX_SIZE; use teos_common::UserId; -use crate::extended_appointment::{ExtendedAppointment, UUID}; +use crate::extended_appointment::{compute_appointment_slots, ExtendedAppointment, UUID}; use crate::gatekeeper::UserInfo; -use crate::responder::{ConfirmationStatus, PenaltySummary, TransactionTracker}; +use crate::responder::{ConfirmationStatus, TransactionTracker}; -const TABLES: [&str; 6] = [ - "CREATE TABLE IF NOT EXISTS users ( - user_id INT PRIMARY KEY, - available_slots INT NOT NULL, - subscription_start INT NOT NULL, - subscription_expiry INT NOT NULL -)", - "CREATE TABLE IF NOT EXISTS appointments ( - UUID INT PRIMARY KEY, - locator INT NOT NULL, - encrypted_blob BLOB NOT NULL, - to_self_delay INT NOT NULL, - user_signature BLOB NOT NULL, - start_block INT NOT NULL, - user_id INT NOT NULL, - FOREIGN KEY(user_id) - REFERENCES users(user_id) - ON DELETE CASCADE -)", - "CREATE TABLE IF NOT EXISTS trackers ( - UUID INT PRIMARY KEY, - dispute_tx BLOB NOT NULL, - penalty_tx BLOB NOT NULL, - height INT NOT NULL, - confirmed BOOL NOT NULL, - FOREIGN KEY(UUID) - REFERENCES appointments(UUID) - ON DELETE CASCADE -)", - "CREATE TABLE IF NOT EXISTS last_known_block ( - id INT PRIMARY KEY, - block_hash INT NOT NULL -)", - "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 -)", -]; +/// Packs the errors than can raise when interacting with the underlying database. +#[derive(Debug)] +pub enum Error { + AlreadyExists, + MissingForeignKey, + MissingField, + NotFound, + Unknown(SqliteError), +} /// Component in charge of interacting with the underlying database. /// @@ -73,47 +42,133 @@ pub struct DBM { connection: Connection, } -impl DatabaseConnection for DBM { - fn get_connection(&self) -> &Connection { - &self.connection - } - - fn get_mut_connection(&mut self) -> &mut Connection { - &mut self.connection - } -} - impl DBM { /// Creates a new [DBM] instance. pub fn new(db_path: PathBuf) -> Result { let connection = Connection::open(db_path)?; connection.execute("PRAGMA foreign_keys=1;", [])?; let mut dbm = Self { connection }; - dbm.create_tables(Vec::from_iter(TABLES))?; + dbm.create_tables()?; Ok(dbm) } + /// Creates the database tables if not present. + /// + /// The database consists of the following tables: + /// - users + /// - appointments + /// - trackers + /// - last_known_block + /// - keys + fn create_tables(&mut self) -> Result<(), SqliteError> { + let tx = self.connection.transaction().unwrap(); + tx.execute( + "CREATE TABLE IF NOT EXISTS users ( + user_id INT PRIMARY KEY, + available_slots INT NOT NULL, + subscription_expiry INT NOT NULL + )", + [], + )?; + tx.execute( + "CREATE TABLE IF NOT EXISTS appointments ( + UUID INT PRIMARY KEY, + locator INT NOT NULL, + encrypted_blob BLOB NOT NULL, + to_self_delay INT NOT NULL, + user_signature BLOB NOT NULL, + start_block INT NOT NULL, + user_id INT NOT NULL, + FOREIGN KEY(user_id) + REFERENCES users(user_id) + ON DELETE CASCADE + )", + [], + )?; + tx.execute( + "CREATE TABLE IF NOT EXISTS trackers ( + UUID INT PRIMARY KEY, + dispute_tx BLOB NOT NULL, + penalty_tx BLOB NOT NULL, + height INT NOT NULL, + confirmed BOOL NOT NULL, + FOREIGN KEY(UUID) + REFERENCES appointments(UUID) + ON DELETE CASCADE + )", + [], + )?; + tx.execute( + "CREATE TABLE IF NOT EXISTS last_known_block ( + id INT PRIMARY KEY, + block_hash INT NOT NULL + )", + [], + )?; + + tx.execute( + "CREATE TABLE IF NOT EXISTS keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key INT NOT NULL + )", + [], + )?; + tx.commit() + } + + /// Generic method to store data into the database. + fn store_data(&self, query: &str, params: P) -> Result<(), Error> { + match self.connection.execute(query, params) { + Ok(_) => Ok(()), + Err(e) => match e { + SqliteError::SqliteFailure(ie, _) => match ie.code { + ErrorCode::ConstraintViolation => match ie.extended_code { + SQLITE_CONSTRAINT_FOREIGNKEY => Err(Error::MissingForeignKey), + SQLITE_CONSTRAINT_PRIMARYKEY => Err(Error::AlreadyExists), + _ => Err(Error::Unknown(e)), + }, + _ => Err(Error::Unknown(e)), + }, + _ => Err(Error::Unknown(e)), + }, + } + } + + /// Generic method to remove data from the database. + fn remove_data(&self, query: &str, params: P) -> Result<(), Error> { + match self.connection.execute(query, params).unwrap() { + 0 => Err(Error::NotFound), + _ => Ok(()), + } + } + + /// Generic method to update data from the database. + fn update_data(&self, query: &str, params: P) -> Result<(), Error> { + // Updating data is fundamentally the same as deleting it in terms of interface. + // A query is sent and either no row is modified or some rows are + self.remove_data(query, params) + } + /// Stores a user ([UserInfo]) into the database. pub(crate) fn store_user(&self, user_id: UserId, user_info: &UserInfo) -> Result<(), Error> { let query = - "INSERT INTO users (user_id, available_slots, subscription_start, subscription_expiry) VALUES (?1, ?2, ?3, ?4)"; + "INSERT INTO users (user_id, available_slots, subscription_expiry) VALUES (?1, ?2, ?3)"; match self.store_data( query, params![ - user_id.to_vec(), + user_id.serialize(), user_info.available_slots, - user_info.subscription_start, user_info.subscription_expiry, ], ) { Ok(x) => { - log::debug!("User successfully stored: {user_id}"); + log::debug!("User successfully stored: {}", user_id); Ok(x) } Err(e) => { - log::error!("Couldn't store user: {user_id}. Error: {e:?}"); + log::error!("Couldn't store user: {}. Error: {:?}", user_id, e); Err(e) } } @@ -122,109 +177,98 @@ impl DBM { /// Updates an existing user ([UserInfo]) in the database. pub(crate) fn update_user(&self, user_id: UserId, user_info: &UserInfo) { let query = - "UPDATE users SET available_slots=(?1), subscription_start=(?2), subscription_expiry=(?3) WHERE user_id=(?4)"; + "UPDATE users SET available_slots=(?1), subscription_expiry=(?2) WHERE user_id=(?3)"; match self.update_data( query, params![ user_info.available_slots, - user_info.subscription_start, user_info.subscription_expiry, - user_id.to_vec(), + user_id.serialize(), ], ) { Ok(_) => { - log::debug!("User's info successfully updated: {user_id}"); + log::debug!("User's info successfully updated: {}", user_id); } Err(_) => { - log::error!("User not found, data cannot be updated: {user_id}"); + log::error!("User not found, data cannot be updated: {}", user_id); } } } - /// Loads the associated locators ([Locator]) of a given user ([UserId]). - pub(crate) fn load_user_locators(&self, user_id: UserId) -> Vec { + /// Loads the associated appointments ([Appointment]) of a given user ([UserInfo]). + pub(crate) fn load_user_appointments(&self, user_id: UserId) -> HashMap { let mut stmt = self .connection - .prepare("SELECT locator FROM appointments WHERE user_id=(?)") + .prepare("SELECT UUID, encrypted_blob FROM appointments WHERE user_id=(?)") .unwrap(); + let mut rows = stmt.query([user_id.serialize()]).unwrap(); - 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() + 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::deserialize(&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 } /// Loads all users from the database. pub(crate) fn load_all_users(&self) -> HashMap { let mut users = HashMap::new(); - let mut stmt = self - .connection - .prepare("SELECT user_id, available_slots, subscription_start, subscription_expiry FROM users") - .unwrap(); + let mut stmt = self.connection.prepare("SELECT * FROM users").unwrap(); let mut rows = stmt.query([]).unwrap(); while let Ok(Some(row)) = rows.next() { let raw_userid: Vec = row.get(0).unwrap(); - let user_id = UserId::from_slice(&raw_userid).unwrap(); + let user_id = UserId::deserialize(&raw_userid).unwrap(); let slots = row.get(1).unwrap(); - let start = row.get(2).unwrap(); - let expiry = row.get(3).unwrap(); + let expiry = row.get(2).unwrap(); - users.insert(user_id, UserInfo::new(slots, start, expiry)); + users.insert( + user_id, + UserInfo::with_appointments(slots, expiry, self.load_user_appointments(user_id)), + ); } users } /// Removes some users from the database in batch. - pub(crate) fn batch_remove_users(&mut self, users: &[UserId]) -> usize { + pub(crate) fn batch_remove_users(&mut self, users: &HashSet) -> usize { let limit = self.connection.limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER) as usize; let tx = self.connection.transaction().unwrap(); let iter = users .iter() - .map(|uuid| uuid.to_vec()) + .map(|uuid| uuid.serialize()) .collect::>>(); for chunk in iter.chunks(limit) { let query = "DELETE FROM users WHERE user_id IN ".to_owned(); let placeholders = format!("(?{})", (", ?").repeat(chunk.len() - 1)); - match tx.execute(&format!("{query}{placeholders}"), params_from_iter(chunk)) { + match tx.execute( + &format!("{}{}", query, placeholders), + params_from_iter(chunk), + ) { Ok(_) => log::debug!("Users deletion added to db transaction"), - Err(e) => log::error!("Couldn't add deletion query to transaction. Error: {e:?}"), + Err(e) => log::error!("Couldn't add deletion query to transaction. Error: {:?}", e), } } match tx.commit() { Ok(_) => log::debug!("Users successfully deleted"), - Err(e) => log::error!("Couldn't delete users. Error: {e:?}"), + Err(e) => log::error!("Couldn't delete users. Error: {:?}", e), } (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, @@ -235,32 +279,28 @@ impl DBM { match self.store_data( query, params![ - uuid.to_vec(), - appointment.locator().to_vec(), + uuid.serialize(), + appointment.locator().serialize(), appointment.encrypted_blob(), appointment.to_self_delay(), appointment.user_signature, appointment.start_block, - appointment.user_id.to_vec(), + appointment.user_id.serialize(), ], ) { Ok(x) => { - log::debug!("Appointment successfully stored: {uuid}"); + log::debug!("Appointment successfully stored: {}", uuid); Ok(x) } Err(e) => { - log::error!("Couldn't store appointment: {uuid}. Error: {e:?}"); + log::error!("Couldn't store appointment: {}. Error: {:?}", uuid, e); Err(e) } } } /// Updates an existing [Appointment] in the database. - pub(crate) fn update_appointment( - &self, - uuid: UUID, - appointment: &ExtendedAppointment, - ) -> Result<(), Error> { + pub(crate) fn update_appointment(&self, uuid: UUID, appointment: &ExtendedAppointment) { // 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)"; @@ -271,91 +311,59 @@ impl DBM { appointment.to_self_delay(), appointment.user_signature, appointment.start_block, - uuid.to_vec(), + uuid.serialize(), ], ) { Ok(_) => { - log::debug!("Appointment successfully updated: {uuid}"); - Ok(()) + log::debug!("Appointment successfully updated: {}", uuid); } - Err(e) => { - log::error!("Appointment not found, data cannot be updated: {uuid}. Error: {e:?}"); - Err(e) + Err(_) => { + log::error!("Appointment not found, data cannot be updated: {}", uuid); } } } /// Loads an [Appointment] from the database. - pub(crate) fn load_appointment(&self, uuid: UUID) -> Option { - let key = uuid.to_vec(); + pub(crate) fn load_appointment(&self, uuid: UUID) -> Result { + let key = uuid.serialize(); let mut stmt = self .connection - .prepare( - "SELECT locator, encrypted_blob, to_self_delay, user_signature, start_block, user_id - FROM appointments WHERE UUID=(?)" - ) + .prepare("SELECT * FROM appointments WHERE UUID=(?)") .unwrap(); stmt.query_row([key], |row| { - let raw_locator: Vec = row.get(0).unwrap(); - let encrypted_blob = row.get(1).unwrap(); - let to_self_delay = row.get(2).unwrap(); - let user_signature = row.get(3).unwrap(); - let start_block = row.get(4).unwrap(); - let raw_userid: Vec = row.get(5).unwrap(); + let raw_locator: Vec = row.get(1).unwrap(); + let locator = Locator::deserialize(&raw_locator).unwrap(); + let raw_userid: Vec = row.get(6).unwrap(); + let user_id = UserId::deserialize(&raw_userid).unwrap(); - let locator = Locator::from_slice(&raw_locator).unwrap(); - let user_id = UserId::from_slice(&raw_userid).unwrap(); - let appointment = Appointment::new(locator, encrypted_blob, to_self_delay); + let appointment = Appointment::new(locator, row.get(2).unwrap(), row.get(3).unwrap()); Ok(ExtendedAppointment::new( appointment, user_id, - user_signature, - start_block, + row.get(4).unwrap(), + row.get(5).unwrap(), )) }) - .ok() + .map_err(|_| Error::NotFound) } - /// 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 - /// matching this locator. If no locator is given, all the appointments in the database would be returned. - pub(crate) fn load_appointments( - &self, - locator: Option, - ) -> HashMap { + /// Loads all appointments from the database. + pub(crate) fn load_all_appointments(&self) -> HashMap { let mut appointments = HashMap::new(); - - let mut sql = - "SELECT a.UUID, a.locator, a.encrypted_blob, a.to_self_delay, a.user_signature, a.start_block, a.user_id - FROM appointments as a LEFT JOIN trackers as t ON a.UUID=t.UUID WHERE t.UUID IS NULL".to_string(); - // If a locator was passed, filter based on it. - if locator.is_some() { - sql.push_str(" AND a.locator=(?)"); - } - let mut stmt = self.connection.prepare(&sql).unwrap(); - - let mut rows = if let Some(locator) = locator { - stmt.query([locator.to_vec()]).unwrap() - } else { - stmt.query([]).unwrap() - }; + let mut stmt = self + .connection + .prepare("SELECT * FROM appointments as a LEFT JOIN trackers as t ON a.UUID=t.UUID WHERE t.UUID IS NULL") + .unwrap(); + let mut rows = stmt.query([]).unwrap(); while let Ok(Some(row)) = rows.next() { let raw_uuid: Vec = row.get(0).unwrap(); - let uuid = UUID::from_slice(&raw_uuid[0..20]).unwrap(); + let uuid = UUID::deserialize(&raw_uuid[0..20]).unwrap(); let raw_locator: Vec = row.get(1).unwrap(); - let locator = Locator::from_slice(&raw_locator).unwrap(); + let locator = Locator::deserialize(&raw_locator).unwrap(); let raw_userid: Vec = row.get(6).unwrap(); - let user_id = UserId::from_slice(&raw_userid).unwrap(); + let user_id = UserId::deserialize(&raw_userid).unwrap(); let appointment = Appointment::new(locator, row.get(2).unwrap(), row.get(3).unwrap()); @@ -373,129 +381,74 @@ 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=(?)"; - match self.remove_data(query, params![uuid.to_vec()]) { + match self.remove_data(query, params![uuid.serialize()]) { Ok(_) => { - log::debug!("Appointment successfully removed: {uuid}"); + log::debug!("Appointment successfully removed: {}", uuid); } Err(_) => { - log::error!("Appointment not found, data cannot be removed: {uuid}"); + log::error!("Appointment not found, data cannot be removed: {}", uuid); } } } - /// 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. + /// Removes some appointments from the database in batch and updates the associated users giving back + /// the freed appointment slots pub(crate) fn batch_remove_appointments( &mut self, - appointments: &[UUID], + appointments: &HashSet, updated_users: &HashMap, ) -> usize { let limit = self.connection.limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER) as usize; let tx = self.connection.transaction().unwrap(); let iter = appointments .iter() - .map(|uuid| uuid.to_vec()) + .map(|uuid| uuid.serialize()) .collect::>>(); for chunk in iter.chunks(limit) { let query = "DELETE FROM appointments WHERE UUID IN ".to_owned(); let placeholders = format!("(?{})", (", ?").repeat(chunk.len() - 1)); - match tx.execute(&format!("{query}{placeholders}"), params_from_iter(chunk)) { + match tx.execute( + &format!("{}{}", query, placeholders), + params_from_iter(chunk), + ) { Ok(_) => log::debug!("Appointments deletion added to db transaction"), - Err(e) => log::error!("Couldn't add deletion query to transaction. Error: {e:?}"), + Err(e) => log::error!("Couldn't add deletion query to transaction. Error: {:?}", e), } } for (id, info) in updated_users.iter() { let query = "UPDATE users SET available_slots=(?1) WHERE user_id=(?2)"; - match tx.execute(query, params![info.available_slots, id.to_vec(),]) { + match tx.execute(query, params![info.available_slots, id.serialize(),]) { Ok(_) => log::debug!("User update added to db transaction"), - Err(e) => log::error!("Couldn't add update query to transaction. Error: {e:?}"), + Err(e) => log::error!("Couldn't add update query to transaction. Error: {:?}", e), }; } match tx.commit() { Ok(_) => log::debug!("Appointments successfully deleted"), - Err(e) => log::error!("Couldn't delete appointments. Error: {e:?}"), + Err(e) => log::error!("Couldn't delete appointments. Error: {:?}", e), } (appointments.len() as f64 / limit as f64).ceil() as usize } - /// Loads the [`UUID`]s of appointments triggered by `locator`. - pub(crate) fn load_uuids(&self, locator: Locator) -> Vec { + /// Loads the locator associated to a given UUID + pub(crate) fn load_locator(&self, uuid: UUID) -> Result { let mut stmt = self .connection - .prepare("SELECT UUID from appointments WHERE locator=(?)") + .prepare("SELECT locator FROM appointments WHERE UUID=(?)") .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) + stmt.query_row([uuid.serialize()], |row| { + let raw_locator: Vec = row.get(0).unwrap(); + Ok(Locator::deserialize(&raw_locator).unwrap()) }) - .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 + .map_err(|_| Error::NotFound) } /// Stores a [TransactionTracker] into the database. @@ -511,67 +464,39 @@ impl DBM { match self.store_data( query, params![ - uuid.to_vec(), - consensus::serialize(&tracker.dispute_tx), - consensus::serialize(&tracker.penalty_tx), + uuid.serialize(), + tracker.dispute_tx.serialize(), + tracker.penalty_tx.serialize(), height, confirmed, ], ) { Ok(x) => { - log::debug!("Tracker successfully stored: {uuid}"); + log::debug!("Tracker successfully stored: {}", uuid); Ok(x) } Err(e) => { - log::error!("Couldn't store tracker: {uuid}. Error: {e:?}"); - Err(e) - } - } - } - - /// 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:?}"); + log::error!("Couldn't store tracker: {}. Error: {:?}", uuid, e); Err(e) } } } /// Loads a [TransactionTracker] from the database. - pub(crate) fn load_tracker(&self, uuid: UUID) -> Option { - let key = uuid.to_vec(); - let mut stmt = self - .connection.prepare( - "SELECT t.dispute_tx, t.penalty_tx, t.height, t.confirmed, a.user_id - FROM trackers as t INNER JOIN appointments as a ON t.UUID=a.UUID WHERE t.UUID=(?)" - ) - .unwrap(); + pub(crate) fn load_tracker(&self, uuid: UUID) -> Result { + let key = uuid.serialize(); + let mut stmt = self.connection.prepare( + "SELECT t.*, a.user_id FROM trackers as t INNER JOIN appointments as a ON t.UUID=a.UUID WHERE t.UUID=(?)").unwrap(); stmt.query_row([key], |row| { - let raw_dispute_tx: 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(); - let raw_userid: Vec = row.get(4).unwrap(); - - let dispute_tx = consensus::deserialize(&raw_dispute_tx).unwrap(); - let penalty_tx = consensus::deserialize(&raw_penalty_tx).unwrap(); - let user_id = UserId::from_slice(&raw_userid).unwrap(); + let raw_dispute_tx: Vec = row.get(1).unwrap(); + let dispute_tx = deserialize::(&raw_dispute_tx).unwrap(); + let raw_penalty_tx: Vec = row.get(2).unwrap(); + let penalty_tx = deserialize::(&raw_penalty_tx).unwrap(); + let height: u32 = row.get(3).unwrap(); + let confirmed: bool = row.get(4).unwrap(); + let raw_userid: Vec = row.get(5).unwrap(); + let user_id = UserId::deserialize(&raw_userid).unwrap(); Ok(TransactionTracker { dispute_tx, @@ -580,52 +505,29 @@ impl DBM { user_id, }) }) - .ok() + .map_err(|_| Error::NotFound) } - /// 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 - /// matching this locator. If no locator is given, all the trackers in the database would be returned. - pub(crate) fn load_trackers( - &self, - locator: Option, - ) -> HashMap { + /// Loads all trackers from the database. + pub(crate) fn load_all_trackers(&self) -> HashMap { let mut trackers = HashMap::new(); - - let mut sql = "SELECT t.UUID, t.dispute_tx, t.penalty_tx, t.height, t.confirmed, a.user_id - FROM trackers as t INNER JOIN appointments as a ON t.UUID=a.UUID" - .to_string(); - // If a locator was passed, filter based on it. - if locator.is_some() { - sql.push_str(" WHERE a.locator=(?)"); - } - let mut stmt = self.connection.prepare(&sql).unwrap(); - - let mut rows = if let Some(locator) = locator { - stmt.query([locator.to_vec()]).unwrap() - } else { - stmt.query([]).unwrap() - }; + let mut stmt = self + .connection + .prepare("SELECT t.*, a.user_id 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 uuid = UUID::from_slice(&raw_uuid[0..20]).unwrap(); + let uuid = UUID::deserialize(&raw_uuid[0..20]).unwrap(); let raw_dispute_tx: Vec = row.get(1).unwrap(); - let dispute_tx = consensus::deserialize(&raw_dispute_tx).unwrap(); + let dispute_tx = deserialize::(&raw_dispute_tx).unwrap(); let raw_penalty_tx: Vec = row.get(2).unwrap(); - let penalty_tx = consensus::deserialize(&raw_penalty_tx).unwrap(); + let penalty_tx = deserialize::(&raw_penalty_tx).unwrap(); let height: u32 = row.get(3).unwrap(); let confirmed: bool = row.get(4).unwrap(); let raw_userid: Vec = row.get(5).unwrap(); - let user_id = UserId::from_slice(&raw_userid).unwrap(); + let user_id = UserId::deserialize(&raw_userid).unwrap(); trackers.insert( uuid, @@ -641,74 +543,14 @@ 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_byte_array().to_vec()]) + self.store_data(query, params![block_hash.to_vec()]) } /// Loads the last known block from the database. - pub fn load_last_known_block(&self) -> Option { + pub fn load_last_known_block(&self) -> Result { let mut stmt = self .connection .prepare("SELECT block_hash FROM last_known_block WHERE id=0") @@ -718,7 +560,7 @@ impl DBM { let raw_hash: Vec = row.get(0).unwrap(); Ok(BlockHash::from_slice(&raw_hash).unwrap()) }) - .ok() + .map_err(|_| Error::NotFound) } /// Stores the tower secret key into the database. @@ -726,14 +568,14 @@ impl DBM { /// When a new key is generated, old keys are not overwritten but are not retrievable from the API either. pub fn store_tower_key(&self, sk: &SecretKey) -> Result<(), Error> { let query = "INSERT INTO keys (key) VALUES (?)"; - self.store_data(query, params![sk.display_secret().to_string()]) + self.store_data(query, params![sk.to_string()]) } /// Loads the last known tower secret key from the database. /// /// 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) -> Option { + pub fn load_tower_key(&self) -> Result { let mut stmt = self .connection .prepare( @@ -745,52 +587,50 @@ impl DBM { let sk: String = row.get(0).unwrap(); Ok(SecretKey::from_str(&sk).unwrap()) }) - .ok() + .map_err(|_| Error::NotFound) } } #[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_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, - SUBSCRIPTION_START, + get_random_tracker, get_random_user_id, }; + use std::iter::FromIterator; + use teos_common::cryptography::get_random_bytes; impl DBM { pub(crate) fn in_memory() -> Result { let connection = Connection::open_in_memory()?; connection.execute("PRAGMA foreign_keys=1;", [])?; let mut dbm = Self { connection }; - dbm.create_tables(Vec::from_iter(TABLES))?; + dbm.create_tables()?; Ok(dbm) } - pub(crate) fn load_user(&self, user_id: UserId) -> Option { - let key = user_id.to_vec(); + pub(crate) fn load_user(&self, user_id: UserId) -> Result { + let key = user_id.serialize(); let mut stmt = self .connection - .prepare( - "SELECT available_slots, subscription_start, subscription_expiry - FROM users WHERE user_id=(?)", - ) + .prepare("SELECT available_slots, subscription_expiry FROM users WHERE user_id=(?)") .unwrap(); - 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() + let user = stmt + .query_row([&key], |row| { + let slots = row.get(0).unwrap(); + let expiry = row.get(1).unwrap(); + Ok(UserInfo::with_appointments( + slots, + expiry, + self.load_user_appointments(user_id), + )) + }) + .map_err(|_| Error::NotFound)?; + + Ok(user) } } @@ -798,7 +638,7 @@ mod tests { fn test_create_tables() { let connection = Connection::open_in_memory().unwrap(); let mut dbm = DBM { connection }; - dbm.create_tables(Vec::from_iter(TABLES)).unwrap(); + dbm.create_tables().unwrap(); } #[test] @@ -806,25 +646,46 @@ mod tests { let dbm = DBM::in_memory().unwrap(); let user_id = get_random_user_id(); - let mut user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + let mut user = UserInfo::new(21, 42); assert!(matches!(dbm.store_user(user_id, &user), Ok { .. })); assert_eq!(dbm.load_user(user_id).unwrap(), user); // User info should be updatable but only via the update_user method - user = UserInfo::new(AVAILABLE_SLOTS * 2, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + user = UserInfo::new(42, 21); assert!(matches!( dbm.store_user(user_id, &user), Err(Error::AlreadyExists) )); } + #[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(21, 42); + + 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!(dbm.load_user(user_id).is_none()); + assert!(matches!(dbm.load_user(user_id), Err(Error::NotFound))); } #[test] @@ -832,7 +693,7 @@ mod tests { let dbm = DBM::in_memory().unwrap(); let user_id = get_random_user_id(); - let mut user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + let mut user = UserInfo::new(21, 42); dbm.store_user(user_id, &user).unwrap(); assert_eq!(dbm.load_user(user_id).unwrap(), user); @@ -842,30 +703,6 @@ 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(); @@ -873,13 +710,20 @@ mod tests { for i in 1..11 { let user_id = get_random_user_id(); - let user = UserInfo::new( - AVAILABLE_SLOTS + i, - SUBSCRIPTION_START + i, - SUBSCRIPTION_EXPIRY + i, - ); - users.insert(user_id, user); + let user = UserInfo::new(i, i * 2); + users.insert(user_id, user.clone()); 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); @@ -895,15 +739,15 @@ mod tests { dbm.connection .set_limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER, limit); - let mut to_be_deleted = Vec::new(); + let mut to_be_deleted = HashSet::new(); let mut rest = HashSet::new(); for i in 1..100 { let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + let user = UserInfo::new(21, 42); dbm.store_user(user_id, &user).unwrap(); if i % 2 == 0 { - to_be_deleted.push(user_id); + to_be_deleted.insert(user_id); } else { rest.insert(user_id); } @@ -912,7 +756,13 @@ mod tests { // Check that the db transaction had 5 (100/2*10) queries on it assert_eq!(dbm.batch_remove_users(&to_be_deleted), 5); // Check user data was deleted - assert_eq!(rest, dbm.load_all_users().keys().cloned().collect()); + assert_eq!( + rest, + dbm.load_all_users() + .keys() + .cloned() + .collect::>() + ); } #[test] @@ -926,7 +776,7 @@ mod tests { // Add the user and link an appointment (this is usually done once the appointment) // is added after the user creation, but for the test purpose it can be done all at once. - let info = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + let info = UserInfo::new(21, 42); dbm.store_user(appointment.user_id, &info).unwrap(); // Appointment only @@ -935,9 +785,12 @@ mod tests { Ok { .. } )); - dbm.batch_remove_users(&[appointment.user_id]); - assert!(dbm.load_user(appointment.user_id).is_none()); - assert!(dbm.load_appointment(uuid).is_none()); + 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))); // Appointment + Tracker dbm.store_user(appointment.user_id, &info).unwrap(); @@ -947,59 +800,33 @@ mod tests { )); assert!(matches!(dbm.store_tracker(uuid, &tracker), Ok { .. })); - 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()); + 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))); } #[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(); // 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); + let user = UserInfo::new(21, 42); dbm.store_user(user_id, &user).unwrap(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); @@ -1028,7 +855,7 @@ mod tests { dbm.store_appointment(uuid, &appointment), Err(Error::MissingForeignKey) )); - assert!((dbm.load_tracker(uuid).is_none())); + assert!(matches!(dbm.load_tracker(uuid), Err(Error::NotFound))); } #[test] @@ -1036,23 +863,7 @@ mod tests { let dbm = DBM::in_memory().unwrap(); let uuid = generate_uuid(); - 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)); + assert!(matches!(dbm.load_appointment(uuid), Err(Error::NotFound))); } #[test] @@ -1060,7 +871,7 @@ mod tests { let dbm = DBM::in_memory().unwrap(); let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + let user = UserInfo::new(21, 42); dbm.store_user(user_id, &user).unwrap(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); @@ -1078,8 +889,7 @@ 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) - .unwrap(); + dbm.update_appointment(uuid, &another_modified_appointment); assert_eq!(dbm.load_appointment(uuid).unwrap(), modified_appointment); assert_ne!( dbm.load_appointment(uuid).unwrap(), @@ -1094,11 +904,7 @@ mod tests { for i in 1..11 { let user_id = get_random_user_id(); - let user = UserInfo::new( - AVAILABLE_SLOTS + i, - SUBSCRIPTION_START + i, - SUBSCRIPTION_EXPIRY + i, - ); + let user = UserInfo::new(i, i * 2); dbm.store_user(user_id, &user).unwrap(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); @@ -1106,12 +912,12 @@ mod tests { appointments.insert(uuid, appointment); } - assert_eq!(dbm.load_appointments(None), appointments); + assert_eq!(dbm.load_all_appointments(), appointments); // If an appointment has an associated tracker, it should not be loaded since it is seen // as a triggered appointment let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + let user = UserInfo::new(21, 42); dbm.store_user(user_id, &user).unwrap(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); @@ -1122,98 +928,7 @@ mod tests { dbm.store_tracker(uuid, &tracker).unwrap(); // We should get all the appointments back except from the triggered one - assert_eq!(dbm.load_appointments(None), appointments); - } - - #[test] - fn test_load_appointments_with_locator() { - let dbm = DBM::in_memory().unwrap(); - let mut appointments = HashMap::new(); - let dispute_tx = get_random_tx(); - let dispute_txid = dispute_tx.compute_txid(); - let locator = Locator::new(dispute_txid); - - for i in 1..11 { - 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 some appointments belong to a specific dispute tx and some with random ones. - // We will use the locator for that dispute tx to query these appointments. - if i % 2 == 0 { - let (uuid, appointment) = - generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); - dbm.store_appointment(uuid, &appointment).unwrap(); - // Store the appointments made using our dispute tx. - appointments.insert(uuid, appointment); - } else { - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - dbm.store_appointment(uuid, &appointment).unwrap(); - } - } - - // Validate that no other appointments than the ones with our locator are returned. - assert_eq!(dbm.load_appointments(Some(locator)), appointments); - - // If an appointment has an associated tracker, it should not be loaded since it is seen - // as a triggered appointment - let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - dbm.store_user(user_id, &user).unwrap(); - - // Generate an appointment for our dispute tx, thus it gets the same locator as the ones generated above. - let (uuid, appointment) = - generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); - dbm.store_appointment(uuid, &appointment).unwrap(); - - // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. - let tracker = get_random_tracker(user_id, ConfirmationStatus::InMempoolSince(100)); - dbm.store_tracker(uuid, &tracker).unwrap(); - - // We should get all the appointments matching our locator back except from the triggered one - 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()); + assert_eq!(dbm.load_all_appointments(), appointments); } #[test] @@ -1227,22 +942,18 @@ mod tests { .set_limit(Limit::SQLITE_LIMIT_VARIABLE_NUMBER, limit); let user_id = get_random_user_id(); - let mut user = UserInfo::new( - AVAILABLE_SLOTS + 123, - SUBSCRIPTION_START, - SUBSCRIPTION_EXPIRY, - ); + let mut user = UserInfo::new(500, 42); dbm.store_user(user_id, &user).unwrap(); let mut rest = HashSet::new(); for i in 1..6 { - let mut to_be_deleted = Vec::new(); + let mut to_be_deleted = HashSet::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.push(uuid); + to_be_deleted.insert(uuid); } else { rest.insert(uuid); } @@ -1251,7 +962,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)]); + let updated_users = HashMap::from_iter([(user_id, user.clone())]); // Check that the db transaction had i queries on it assert_eq!( @@ -1259,7 +970,13 @@ mod tests { i as usize ); // Check appointment data was deleted and users properly updated - assert_eq!(rest, dbm.load_appointments(None).keys().cloned().collect()); + assert_eq!( + rest, + dbm.load_all_appointments() + .keys() + .cloned() + .collect::>() + ); assert_eq!( dbm.load_user(user_id).unwrap().available_slots, user.available_slots @@ -1275,7 +992,7 @@ mod tests { // The confirmation status doesn't really matter here, it can be any of {ConfirmedIn, InMempoolSince}. let tracker = get_random_tracker(appointment.user_id, ConfirmationStatus::ConfirmedIn(21)); - let info = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + let info = UserInfo::new(21, 42); // Add the user b/c of FK restrictions dbm.store_user(appointment.user_id, &info).unwrap(); @@ -1286,8 +1003,11 @@ mod tests { Ok { .. } )); - dbm.batch_remove_appointments(&[uuid], &HashMap::from_iter([(appointment.user_id, info)])); - assert!(dbm.load_appointment(uuid).is_none()); + 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))); // Appointment + Tracker assert!(matches!( @@ -1296,96 +1016,48 @@ mod tests { )); assert!(matches!(dbm.store_tracker(uuid, &tracker), Ok { .. })); - 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()); + 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))); } #[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_uuids() { + fn test_load_locator() { let dbm = DBM::in_memory().unwrap(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); - let dispute_tx = get_random_tx(); - let dispute_txid = dispute_tx.compute_txid(); - let mut uuids = HashSet::new(); + // 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(21, 42); + dbm.store_user(user_id, &user).unwrap(); - // 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(); + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - let (uuid, appointment) = - generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); - dbm.store_appointment(uuid, &appointment).unwrap(); + assert!(matches!( + dbm.store_appointment(uuid, &appointment), + Ok { .. } + )); - 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 - ); + // We should be able to load the locator now the appointment exists + assert_eq!(dbm.load_locator(uuid).unwrap(), appointment.locator()); } #[test] - fn test_batch_check_locators_exist() { + fn test_load_nonexistent_locator() { 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(); - // 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 - ); + let (uuid, _) = generate_dummy_appointment_with_user(get_random_user_id(), None); + assert!(matches!(dbm.load_locator(uuid), Err(Error::NotFound))); } #[test] @@ -1395,7 +1067,7 @@ mod tests { // In order to add a tracker we need the associated appointment to be present (which // at the same time requires an associated user to be present) let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + let user = UserInfo::new(21, 42); dbm.store_user(user_id, &user).unwrap(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); @@ -1412,7 +1084,7 @@ mod tests { let dbm = DBM::in_memory().unwrap(); let user_id = get_random_user_id(); - let user = UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY); + let user = UserInfo::new(21, 42); dbm.store_user(user_id, &user).unwrap(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); @@ -1445,44 +1117,12 @@ 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!(dbm.load_tracker(uuid).is_none()); + assert!(matches!(dbm.load_tracker(uuid), Err(Error::NotFound))); } #[test] @@ -1492,11 +1132,7 @@ mod tests { for i in 1..11 { let user_id = get_random_user_id(); - let user = UserInfo::new( - AVAILABLE_SLOTS + i, - SUBSCRIPTION_START + i, - SUBSCRIPTION_EXPIRY + i, - ); + let user = UserInfo::new(i, i * 2); dbm.store_user(user_id, &user).unwrap(); let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); @@ -1508,205 +1144,7 @@ mod tests { trackers.insert(uuid, tracker); } - assert_eq!(dbm.load_trackers(None), trackers); - } - - #[test] - fn test_load_trackers_with_locator() { - let dbm = DBM::in_memory().unwrap(); - let mut trackers = HashMap::new(); - let dispute_tx = get_random_tx(); - let dispute_txid = dispute_tx.compute_txid(); - let locator = Locator::new(dispute_txid); - let status = ConfirmationStatus::InMempoolSince(42); - - for i in 1..11 { - 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 tracker = get_random_tracker(user_id, status); - - // Let some trackers belong to our dispute tx and some belong to random ones. - let (uuid, appointment) = if i % 2 == 0 { - let (uuid, app) = - generate_dummy_appointment_with_user(user_id, Some(&dispute_txid)); - // Store the trackers of appointments made with our dispute tx. - trackers.insert(uuid, tracker.clone()); - (uuid, app) - } else { - generate_dummy_appointment_with_user(user_id, None) - }; - dbm.store_appointment(uuid, &appointment).unwrap(); - dbm.store_tracker(uuid, &tracker).unwrap(); - } - - 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); + assert_eq!(dbm.load_all_trackers(), trackers); } #[test] @@ -1727,18 +1165,6 @@ mod tests { fn test_store_load_nonexistent_last_known_block() { let dbm = DBM::in_memory().unwrap(); - assert!(dbm.load_last_known_block().is_none()); - } - - #[test] - fn test_store_load_tower_key() { - let dbm = DBM::in_memory().unwrap(); - - assert!(dbm.load_tower_key().is_none()); - for _ in 0..7 { - let sk = get_random_keypair().0; - dbm.store_tower_key(&sk).unwrap(); - assert_eq!(dbm.load_tower_key().unwrap(), sk); - } + assert!(matches!(dbm.load_last_known_block(), Err(Error::NotFound))); } } diff --git a/teos/src/extended_appointment.rs b/teos/src/extended_appointment.rs index 6545a7f..77af560 100644 --- a/teos/src/extended_appointment.rs +++ b/teos/src/extended_appointment.rs @@ -6,11 +6,11 @@ use std::fmt; use bitcoin::hashes::{ripemd160, Hash}; +use crate::protos as msgs; use teos_common::appointment::{Appointment, Locator}; use teos_common::UserId; /// Unique identifier used to identify appointments. -#[allow(clippy::upper_case_acronyms)] #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] pub(crate) struct UUID([u8; 20]); @@ -21,18 +21,18 @@ impl UUID { /// when a user requests it without having to perform lookups based on the [Locator], and match what [UUID] belongs to what user (if any). /// Therefore, it provides a hard-to-forge id while reducing the tower lookups and the required data to be stored (no reverse maps). pub fn new(locator: Locator, user_id: UserId) -> Self { - let mut uuid_data = locator.to_vec(); - uuid_data.extend(user_id.0.serialize()); - UUID(ripemd160::Hash::hash(&uuid_data).to_byte_array()) + let mut uuid_data = locator.serialize(); + uuid_data.extend(&user_id.0.serialize()); + UUID(ripemd160::Hash::hash(&uuid_data).into_inner()) } /// Serializes the [UUID] returning its byte representation. - pub fn to_vec(self) -> Vec { + pub fn serialize(&self) -> Vec { self.0.to_vec() } /// Builds a [UUID] from its byte representation. - pub fn from_slice(data: &[u8]) -> Result { + pub fn deserialize(data: &[u8]) -> Result { data.try_into().map(Self) } } @@ -46,6 +46,8 @@ 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]. @@ -58,6 +60,18 @@ 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( @@ -89,23 +103,52 @@ impl ExtendedAppointment { self.inner.to_self_delay } - pub fn uuid(&self) -> UUID { - UUID::new(self.inner.locator, self.user_id) + /// Computes the summary of the [ExtendedAppointment]. + pub fn get_summary(&self) -> AppointmentSummary { + AppointmentSummary { + locator: self.locator(), + user_id: self.user_id, + } } } +impl From for msgs::Appointment { + fn from(a: Appointment) -> Self { + Self { + locator: a.locator.serialize(), + encrypted_blob: a.encrypted_blob.clone(), + to_self_delay: a.to_self_delay, + } + } +} + +/// Computes the number of slots an appointment takes from a user subscription. +/// +/// This is based on the [encrypted_blob](Appointment::encrypted_blob) size and the slot size that was defined by the [Gatekeeper](crate::gatekeeper::Gatekeeper). +pub(crate) fn compute_appointment_slots(blob_size: usize, blob_max_size: usize) -> u32 { + (blob_size as f32 / blob_max_size as f32).ceil() as u32 +} + #[cfg(test)] mod tests { use super::*; - use crate::test_utils::generate_uuid; + use crate::test_utils::get_random_user_id; + use teos_common::appointment::Appointment; + use teos_common::cryptography::get_random_bytes; #[test] - fn test_uuid_ser_deser() { - let original_uuid = generate_uuid(); - assert_eq!( - UUID::from_slice(&original_uuid.to_vec()).unwrap(), - original_uuid - ); + fn test_get_summary() { + let locator = Locator::deserialize(&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); } } diff --git a/teos/src/gatekeeper.rs b/teos/src/gatekeeper.rs index 99e7452..4ca5376 100644 --- a/teos/src/gatekeeper.rs +++ b/teos/src/gatekeeper.rs @@ -1,37 +1,51 @@ //! Logic related to the Gatekeeper, the component in charge of managing access to the tower resources. -use lightning::chain; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; +use std::iter::FromIterator; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{Arc, Mutex}; -use teos_common::appointment::{compute_appointment_slots, Locator}; +use lightning::chain; + use teos_common::constants::ENCRYPTED_BLOB_MAX_SIZE; use teos_common::cryptography; use teos_common::receipts::RegistrationReceipt; use teos_common::UserId; use crate::dbm::DBM; -use crate::extended_appointment::{ExtendedAppointment, UUID}; +use crate::extended_appointment::{compute_appointment_slots, ExtendedAppointment, UUID}; /// Data regarding a user subscription with the tower. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct UserInfo { /// Number of appointment slots available for a given user. pub(crate) available_slots: u32, - /// Block height where the user subscription starts. - pub(crate) subscription_start: u32, - /// Block height where the user subscription expires. + /// Block height where the user subscription will expire. 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 { /// Creates a new [UserInfo] instance. - pub fn new(available_slots: u32, subscription_start: u32, subscription_expiry: u32) -> Self { + pub fn new(available_slots: u32, subscription_expiry: u32) -> Self { 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_expiry: u32, + appointments: HashMap, + ) -> Self { + UserInfo { + available_slots, + subscription_expiry, + appointments, } } } @@ -114,9 +128,8 @@ impl Gatekeeper { } /// Gets the data held by the tower about a given user. - 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))) + pub(crate) fn get_user_info(&self, user_id: UserId) -> Option { + self.registered_users.lock().unwrap().get(&user_id).cloned() } /// Authenticates a user. @@ -156,10 +169,7 @@ impl Gatekeeper { .available_slots .checked_add(self.subscription_slots) .ok_or(MaxSlotsReached)?; - user_info.subscription_expiry = user_info - .subscription_expiry - .checked_add(self.subscription_duration) - .unwrap_or(u32::MAX); + user_info.subscription_expiry = block_count + self.subscription_duration; self.dbm.lock().unwrap().update_user(user_id, user_info); user_info @@ -168,7 +178,6 @@ impl Gatekeeper { None => { let user_info = UserInfo::new( self.subscription_slots, - block_count, block_count + self.subscription_duration, ); self.dbm @@ -185,7 +194,6 @@ impl Gatekeeper { Ok(RegistrationReceipt::new( user_id, user_info.available_slots, - user_info.subscription_start, user_info.subscription_expiry, )) } @@ -200,13 +208,7 @@ 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_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 used_slots = user_info.appointments.get(&uuid).map_or(0, |x| *x); let required_slots = compute_appointment_slots(appointment.encrypted_blob().len(), ENCRYPTED_BLOB_MAX_SIZE); @@ -215,6 +217,7 @@ 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); @@ -244,50 +247,56 @@ 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) -> 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) + 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())) .collect() } - /// Deletes these appointments from the database and updates the user's information. - /// - /// 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(); + /// 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() + } - 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() - }; + /// 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(), + ) + } - // 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); + /// Deletes a collection of appointments from the users' subscriptions (from memory only) + /// and updates the available_slots count for the given user. + /// + /// 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(); + + 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()); + }; } + + updated_users } } @@ -295,28 +304,16 @@ impl chain::Listen for Gatekeeper { /// Handles the monitoring process by the [Gatekeeper]. /// /// This is mainly used to keep track of time and expire / outdate subscriptions when needed. - fn filtered_block_connected( - &self, - header: &bitcoin::block::Header, - _: &chain::transaction::TransactionData, - height: u32, - ) { - log::info!("New block received: {}", header.block_hash()); + fn block_connected(&self, block: &bitcoin::Block, height: u32) { + log::info!("New block received: {}", block.block_hash()); // Expired user deletion is delayed. Users are deleted when their subscription is outdated, not expired. - let outdated_users = self.get_outdated_users(height); - if !outdated_users.is_empty() { - // 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); - } + let outdated_users = self.get_outdated_user_ids(height); + self.registered_users + .lock() + .unwrap() + .retain(|id, _| !outdated_users.contains(id)); + self.dbm.lock().unwrap().batch_remove_users(&outdated_users); // Update last known block height self.last_known_block_height @@ -324,7 +321,7 @@ impl chain::Listen for Gatekeeper { } /// Handles reorgs in the [Gatekeeper]. Simply updates the last_known_block_height. - fn block_disconnected(&self, header: &bitcoin::block::Header, height: u32) { + fn block_disconnected(&self, header: &bitcoin::BlockHeader, 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 @@ -336,12 +333,13 @@ impl chain::Listen for Gatekeeper { mod tests { use super::*; - use crate::test_utils::{generate_dummy_appointment_with_user, get_random_tracker, Blockchain}; + use crate::dbm::Error as DBError; + use crate::test_utils::{ + generate_dummy_appointment, generate_dummy_appointment_with_user, generate_uuid, + get_random_user_id, Blockchain, + }; use lightning::chain::Listen; use teos_common::cryptography::{get_random_bytes, get_random_keypair}; - use teos_common::test_utils::get_random_user_id; - - use crate::responder::ConfirmationStatus; const SLOTS: u32 = 21; const DURATION: u32 = 500; @@ -365,11 +363,21 @@ mod tests { &self.registered_users } - pub(crate) fn add_outdated_user(&self, user_id: UserId, outdates_at: u32) { + pub(crate) fn add_outdated_user( + &self, + user_id: UserId, + outdates_at: u32, + appointments: Option>, + ) { self.add_update_user(user_id).unwrap(); let mut registered_users = self.registered_users.lock().unwrap(); - let user = registered_users.get_mut(&user_id).unwrap(); + let mut 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); + } + } } } @@ -435,7 +443,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); + let signature = cryptography::sign(message, &user_sk).unwrap(); assert_eq!( gatekeeper.authenticate_user(message, &signature), Err(AuthenticationFailure("User not found.")) @@ -464,11 +472,7 @@ mod tests { // The data should have been also added to the database assert_eq!( gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap(), - UserInfo::new( - receipt.available_slots(), - receipt.subscription_start(), - receipt.subscription_expiry() - ) + UserInfo::new(receipt.available_slots(), receipt.subscription_expiry()) ); // Let generate a new block and add the user again to check that both the slots and expiry are updated. @@ -478,10 +482,13 @@ mod tests { .store(chain.get_block_count(), Ordering::Relaxed); let updated_receipt = gatekeeper.add_update_user(user_id).unwrap(); - assert_eq!(updated_receipt.available_slots(), SLOTS * 2); + assert_eq!( + updated_receipt.available_slots(), + receipt.available_slots() * 2 + ); assert_eq!( updated_receipt.subscription_expiry(), - START_HEIGHT as u32 + DURATION * 2 + receipt.subscription_expiry() + 1 ); // Data in the database should have been updated too @@ -489,7 +496,6 @@ mod tests { gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap(), UserInfo::new( updated_receipt.available_slots(), - updated_receipt.subscription_start(), updated_receipt.subscription_expiry() ) ); @@ -513,7 +519,6 @@ mod tests { gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap(), UserInfo::new( updated_receipt.available_slots(), - updated_receipt.subscription_start(), updated_receipt.subscription_expiry() ) ); @@ -542,32 +547,25 @@ 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(); - let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap(); - assert!(user_locators.contains(&appointment.locator())); + assert!(gatekeeper.registered_users.lock().unwrap()[&user_id] + .appointments + .contains_key(&uuid)); assert_eq!(slots_before, available_slots + 1); - // Slots should have been updated in the database too. + // 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 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. - // We don't really need to update the appointment in the DB since it's the very same appointment. + // Adding the exact same appointment should leave the slots count unchanged let mut updated_slot_count = gatekeeper .add_update_appointment(user_id, uuid, &appointment) .unwrap(); - - let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap(); - assert!(user_locators.contains(&appointment.locator())); + assert!(gatekeeper.registered_users.lock().unwrap()[&user_id] + .appointments + .contains_key(&uuid)); 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); @@ -577,18 +575,10 @@ mod tests { updated_slot_count = gatekeeper .add_update_appointment(user_id, uuid, &bigger_appointment) .unwrap(); - // 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!(gatekeeper.registered_users.lock().unwrap()[&user_id] + .appointments + .contains_key(&uuid)); 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); @@ -596,43 +586,26 @@ mod tests { updated_slot_count = gatekeeper .add_update_appointment(user_id, uuid, &appointment) .unwrap(); - // 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!(gatekeeper.registered_users.lock().unwrap()[&user_id] + .appointments + .contains_key(&uuid)); 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 (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + let new_uuid = generate_uuid(); updated_slot_count = gatekeeper - .add_update_appointment(user_id, uuid, &appointment) + .add_update_appointment(user_id, new_uuid, &appointment) .unwrap(); - // 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!(gatekeeper.registered_users.lock().unwrap()[&user_id] + .appointments + .contains_key(&new_uuid)); 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() @@ -641,10 +614,9 @@ mod tests { .unwrap() .available_slots = 0; assert!(matches!( - gatekeeper.add_update_appointment(user_id, uuid, &appointment), + gatekeeper.add_update_appointment(user_id, generate_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); @@ -690,179 +662,148 @@ 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), vec![]); + assert_eq!(gatekeeper.get_outdated_users(i).len(), 0); } // 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), vec![]); + assert_eq!(gatekeeper.get_outdated_users(start_height).len(), 0); // Add an outdated user and check again - gatekeeper.add_outdated_user(user_id, start_height); - assert_eq!(gatekeeper.get_outdated_users(start_height), vec![user_id]); + 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])); } #[test] - 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(); + 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)); - for _ in 0..n_users { + // 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() { + let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT)); + + // 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 { let user_id = get_random_user_id(); - 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. + 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 .dbm .lock() .unwrap() - .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() + .load_user(*user_id) .unwrap() - .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); - } - - // Delete these appointments and refund their owners their slots back. - gatekeeper.delete_appointments(uuids_to_delete.clone(), true); - - 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, - correct_remaining_slots + .available_slots, + gatekeeper.subscription_slots ); } + for (_, user_id) in rest.iter() { + assert!(!gatekeeper + .registered_users + .lock() + .unwrap() + .contains_key(user_id)); + } + + // 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)); + + // The slot count is back to default + assert_eq!( + gatekeeper.registered_users.lock().unwrap()[user_id].available_slots, + gatekeeper.subscription_slots + ); + } + for (_, user_id) in rest.iter() { + assert!(!gatekeeper + .registered_users + .lock() + .unwrap() + .contains_key(user_id)); + } } #[test] - fn test_filtered_block_connected() { + fn test_block_connected() { // block_connected in the Gatekeeper is used to keep track of time in order to manage the users' subscription expiry. // Remove users that get outdated at the new block's height from registered_users and the database. let mut chain = Blockchain::default().with_height(START_HEIGHT); @@ -874,7 +815,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) + gatekeeper.add_outdated_user(*user_id, chain.tip().height + 1, None) } // Connect a new block. Outdated users are deleted @@ -887,7 +828,10 @@ mod tests { .lock() .unwrap() .contains_key(user_id)); - assert!(gatekeeper.dbm.lock().unwrap().load_user(*user_id).is_none()); + assert!(matches!( + gatekeeper.dbm.lock().unwrap().load_user(*user_id), + Err(DBError::NotFound) + )); } // Check that the last_known_block_header has been properly updated diff --git a/teos/src/lib.rs b/teos/src/lib.rs index 2e87bcf..f3c05b6 100644 --- a/teos/src/lib.rs +++ b/teos/src/lib.rs @@ -2,8 +2,6 @@ //! //! A watchtower implementation written in Rust. -// FIXME: This is a temporary fix. See https://github.com/tokio-rs/prost/issues/661 -#[allow(clippy::derive_partial_eq_without_eq)] pub mod protos { tonic::include_proto!("teos.v2"); } @@ -21,8 +19,6 @@ pub mod gatekeeper; pub mod responder; #[doc(hidden)] mod rpc_errors; -pub mod tls; -mod tx_index; pub mod watcher; #[cfg(test)] diff --git a/teos/src/main.rs b/teos/src/main.rs index 15c1591..963bf75 100644 --- a/teos/src/main.rs +++ b/teos/src/main.rs @@ -1,59 +1,57 @@ -use log::LevelFilter; -use simple_logger::SimpleLogger; +use simple_logger::init_with_level; 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 tonic::transport::Server; -use bitcoin::network::Network; +use bitcoin::network::constants::Network; use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; -use bitcoincore_rpc::{Auth, Client, RpcApi}; +use bitcoincore_rpc::{Auth, Client}; use lightning_block_sync::init::validate_best_block_header; use lightning_block_sync::poll::{ ChainPoller, Poll, Validate, ValidatedBlock, ValidatedBlockHeader, }; -use lightning_block_sync::{BlockSource, BlockSourceError, SpvClient, UnboundedCache}; +use lightning_block_sync::{BlockSource, SpvClient, UnboundedCache}; +use teos::api::http; 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, AuthMethod, Config, Opt}; +use teos::config::{self, Config, Opt}; use teos::dbm::DBM; use teos::gatekeeper::Gatekeeper; -use teos::protos as msgs; use teos::protos::private_tower_services_server::PrivateTowerServicesServer; use teos::protos::public_tower_services_server::PublicTowerServicesServer; use teos::responder::Responder; -use teos::tls::tls_init; use teos::watcher::Watcher; -use teos_common::constants::IRREVOCABLY_RESOLVED; use teos_common::cryptography::get_random_keypair; -use teos_common::TowerId; +use teos_common::UserId; async fn get_last_n_blocks( poller: &mut ChainPoller, mut last_known_block: ValidatedBlockHeader, n: usize, -) -> Result, BlockSourceError> +) -> Vec where B: DerefMut + Sized + Send + Sync, T: BlockSource, { - let mut last_n_blocks = Vec::with_capacity(n); + let mut last_n_blocks = Vec::new(); for _ in 0..n { - 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?; + let block = poller.fetch_block(&last_known_block).await.unwrap(); + last_known_block = poller + .look_up_previous_header(&last_known_block) + .await + .unwrap(); last_n_blocks.push(block); } - Ok(last_n_blocks) + last_n_blocks } fn create_new_tower_keypair(db: &DBM) -> (SecretKey, PublicKey) { @@ -66,61 +64,34 @@ 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:?}"); + eprintln!("Cannot create data dir: {:?}", e); std::process::exit(1); }); // Load conf (from file or defaults) and patch it with the command line parameters received (if any) - let mut conf = config::from_file::(&conf_file_path); - let is_default = conf.is_default(); + let mut conf = config::from_file::(path.join("teos.toml")); conf.patch_with_options(opt); conf.verify().unwrap_or_else(|e| { - eprintln!("{e}"); + eprintln!("{}", e); std::process::exit(1); }); // Set log level - SimpleLogger::new() - .with_level(if conf.deps_debug { - LevelFilter::Debug - } else { - LevelFilter::Warn - }) - .with_module_level( - "teos", - if conf.debug { - LevelFilter::Debug - } else { - LevelFilter::Info - }, - ) - .init() - .unwrap(); + if conf.debug { + init_with_level(log::Level::Debug).unwrap() + } else { + init_with_level(log::Level::Info).unwrap() + } // 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:?}"); + 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(), )); @@ -132,30 +103,24 @@ 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 { - log::info!("Tower keys not found. Creating a fresh set"); - create_new_tower_keypair(&locked_db) + 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_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"), - }; + log::info!("tower_id: {}", tower_pk); // Initialize our bitcoind client let (bitcoin_cli, bitcoind_reachable) = match BitcoindClient::new( &conf.btc_rpc_connect, conf.btc_rpc_port, - btc_rpc_auth.clone(), - &conf.btc_network, + &conf.btc_rpc_user, + &conf.btc_rpc_password, ) .await { @@ -164,12 +129,8 @@ async fn main() { Arc::new((Mutex::new(true), Condvar::new())), ), Err(e) => { - let e_msg = match e.kind() { - ErrorKind::InvalidData => "invalid btcrpcuser or btcrpcpassword".into(), - _ => e.to_string(), - }; - log::error!("Failed to connect to bitcoind. Error: {e_msg}"); - std::process::exit(1); + log::error!("Failed to connect to bitcoind client: {}", e); + return; } }; @@ -182,81 +143,27 @@ async fn main() { }; let rpc = Arc::new( Client::new( - &format!("{schema}{}:{}", conf.btc_rpc_connect, conf.btc_rpc_port), - btc_rpc_auth, + &format!("{}{}:{}", schema, conf.btc_rpc_connect, conf.btc_rpc_port), + Auth::UserPass(conf.btc_rpc_user.clone(), conf.btc_rpc_password.clone()), ) .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 Some(block_hash) = last_known_block { - let mut last_known_header = derefed + let tip = if let Ok(block_hash) = dbm.lock().unwrap().load_last_known_block() { + derefed .get_header(&block_hash, None) .await .unwrap() .validate(block_hash) - .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 + .unwrap() } else { - validate_best_block_header(&derefed).await.unwrap() + validate_best_block_header(&mut derefed).await.unwrap() }; + log::info!("Last known block: {}", tip.header.block_hash()); - // DISCUSS: This is not really required (and only triggered in regtest). This is only in place so the caches can be - // populated with enough blocks mainly because the size of the cache is based on the amount of blocks passed when initializing. - // However, we could add an additional parameter to specify the size of the cache, and initialize with however may blocks we - // could pull from the backend. Adding this functionality just for regtest seemed unnecessary though, hence the check. - if tip.height < IRREVOCABLY_RESOLVED { - log::error!( - "Not enough blocks to start teosd (required: {IRREVOCABLY_RESOLVED}). Mine at least {} more", - IRREVOCABLY_RESOLVED - tip.height - ); - std::process::exit(1); - } - - log::info!( - "Current chain tip: {} (height: {})", - tip.header.block_hash(), - tip.height - ); + let mut poller = ChainPoller::new(&mut derefed, Network::from_str(&conf.btc_network).unwrap()); + let last_n_blocks = get_last_n_blocks(&mut poller, tip, 6).await; // Build components let gatekeeper = Arc::new(Gatekeeper::new( @@ -267,38 +174,17 @@ async fn main() { 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) - }; + let carrier = Carrier::new(rpc, bitcoind_reachable.clone(), tip.deref().height); + let responder = Arc::new(Responder::new(carrier, gatekeeper.clone(), dbm.clone())); + let watcher = Arc::new(Watcher::new( + gatekeeper.clone(), + responder.clone(), + last_n_blocks, + tip.height, + tower_sk, + UserId(tower_pk), + dbm.clone(), + )); if watcher.is_fresh() & responder.is_fresh() & gatekeeper.is_fresh() { log::info!("Fresh bootstrap"); @@ -307,14 +193,13 @@ async fn main() { } let (shutdown_trigger, shutdown_signal_rpc_api) = triggered::trigger(); - let shutdown_signal_internal_api = shutdown_signal_rpc_api.clone(); + let shutdown_signal_internal_rpc_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 - // 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)); + // last, so both the Watcher and the Responder can query the necessary data from it during data deletion. + let listener = &(watcher.clone(), &(responder, gatekeeper)); let cache = &mut UnboundedCache::new(); let spv_client = SpvClient::new(tip, poller, cache, listener); let mut chain_monitor = ChainMonitor::new( @@ -332,65 +217,31 @@ async fn main() { log::info!("Bootstrap completed. Turning on interfaces"); // Build interfaces - let http_api_addr = format!("{}:{}", conf.api_bind, conf.api_port) - .parse() - .unwrap(); - let mut addresses = vec![msgs::NetworkAddress::from_ipv4( - conf.api_bind.clone(), - conf.api_port, - )]; - - // Create Tor endpoint if required - let tor_api = if conf.tor_support { - let tor_api = TorAPI::new( - http_api_addr, - conf.onion_hidden_service_port, - conf.tor_control_port, - path_network, - ) - .await; - addresses.push(msgs::NetworkAddress::from_torv3( - tor_api.get_onion_address(), - conf.onion_hidden_service_port, - )); - - Some(tor_api) - } else { - None - }; - - let internal_api = Arc::new(InternalAPI::new( + let rpc_api = Arc::new(InternalAPI::new( watcher, - addresses, bitcoind_reachable.clone(), shutdown_trigger, )); - let internal_api_cloned = internal_api.clone(); + let internal_rpc_api = rpc_api.clone(); let rpc_api_addr = format!("{}:{}", conf.rpc_bind, conf.rpc_port) .parse() .unwrap(); - let internal_api_addr = format!("{}:{}", conf.internal_api_bind, conf.internal_api_port) + let internal_rpc_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 + ); + let http_api_addr = format!("{}:{}", conf.api_bind, conf.api_port) .parse() .unwrap(); - - // Generate mtls certificates to data directory so the admin can securely connect - // to the server to perform administrative tasks. - let (identity, ca_cert) = tls_init(&path).unwrap_or_else(|e| { - eprintln!("Couldn't generate tls certificates: {e:?}"); - std::process::exit(1); - }); - - let tls = ServerTlsConfig::new() - .identity(identity) - .client_ca_root(Certificate::from_pem(ca_cert)); // Start tasks let private_api_task = task::spawn(async move { Server::builder() - .tls_config(tls) - .expect("couldn't configure tls") - .add_service(PrivateTowerServicesServer::new(internal_api)) + .add_service(PrivateTowerServicesServer::new(rpc_api)) .serve_with_shutdown(rpc_api_addr, shutdown_signal_rpc_api) .await .unwrap(); @@ -398,50 +249,22 @@ async fn main() { let public_api_task = task::spawn(async move { Server::builder() - .add_service(PublicTowerServicesServer::new(internal_api_cloned)) - .serve_with_shutdown(internal_api_addr, shutdown_signal_internal_api) + .add_service(PublicTowerServicesServer::new(internal_rpc_api)) + .serve_with_shutdown(internal_rpc_api_addr, shutdown_signal_internal_rpc_api) .await .unwrap(); }); - let (http_service_ready, ready_signal_http) = triggered::trigger(); let http_api_task = task::spawn(http::serve( http_api_addr, - internal_api_addr, - http_service_ready, + internal_rpc_api_uri, shutdown_signal_http, )); - ready_signal_http.await; - - // Add Tor Onion Service for public API - let mut tor_task = Option::None; - let (tor_service_ready, ready_signal_tor) = triggered::trigger(); - if let Some(tor_api) = tor_api { - log::info!("Starting up Tor hidden service"); - - tor_task = Some(task::spawn(async move { - if let Err(e) = tor_api - .expose_onion_service(tor_service_ready, shutdown_signal_tor) - .await - { - eprintln!("Cannot connect to the Tor backend: {e}"); - std::process::exit(1); - } - })); - - ready_signal_tor.await - } - - log::info!("Tower ready"); chain_monitor.monitor_chain().await; // Wait until shutdown http_api_task.await.unwrap(); private_api_task.await.unwrap(); public_api_task.await.unwrap(); - if let Some(tor_task) = tor_task { - tor_task.await.unwrap(); - } - - log::info!("Shutting down tower"); + log::info!("Shutting down tower") } diff --git a/teos/src/responder.rs b/teos/src/responder.rs index f816aac..ae742e6 100644 --- a/teos/src/responder.rs +++ b/teos/src/responder.rs @@ -1,24 +1,21 @@ //! Logic related to the Responder, the components in charge of making sure breaches get properly punished. -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; +use std::iter::FromIterator; use std::sync::{Arc, Mutex}; -use bitcoin::hashes::Hash; -use bitcoin::{consensus, BlockHash}; -use bitcoin::{Transaction, Txid}; - +use bitcoin::util::psbt::serialize::Serialize; +use bitcoin::{BlockHeader, Transaction, Txid}; use lightning::chain; -use lightning_block_sync::poll::ValidatedBlock; use teos_common::constants; -use teos_common::protos as common_msgs; use teos_common::UserId; use crate::carrier::Carrier; use crate::dbm::DBM; use crate::extended_appointment::UUID; -use crate::gatekeeper::Gatekeeper; -use crate::tx_index::TxIndex; +use crate::gatekeeper::{Gatekeeper, UserInfo}; +use crate::protos as msgs; use crate::watcher::Breach; /// Number of missed confirmations to wait before rebroadcasting a transaction. @@ -29,8 +26,15 @@ const CONFIRMATIONS_BEFORE_RETRY: u8 = 6; pub enum ConfirmationStatus { ConfirmedIn(u32), InMempoolSince(u32), - IrrevocablyResolved, Rejected(i32), + ReorgedOut, +} + +/// Reason why the tracker is deleted. Used for logging purposes. +enum DeletionReason { + Outdated, + Rejected, + Completed, } impl ConfirmationStatus { @@ -55,14 +59,17 @@ impl ConfirmationStatus { None } } +} - /// Whether the transaction was accepted by the underlying node. - pub fn accepted(&self) -> bool { - matches!( - self, - ConfirmationStatus::ConfirmedIn(_) | &ConfirmationStatus::InMempoolSince(_) - ) - } +/// Minimal data required in memory to keep track of transaction trackers. +#[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. @@ -90,40 +97,23 @@ impl TransactionTracker { user_id, } } -} -impl From for common_msgs::Tracker { - fn from(t: TransactionTracker) -> Self { - common_msgs::Tracker { - 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), + /// 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, } } } -/// 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, +impl From for msgs::Tracker { + fn from(t: TransactionTracker) -> Self { + msgs::Tracker { + dispute_txid: t.dispute_tx.txid().to_vec(), + penalty_txid: t.penalty_tx.txid().to_vec(), + penalty_rawtx: t.penalty_tx.serialize(), } } } @@ -135,49 +125,52 @@ impl PenaltySummary { /// 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 local, pruned, [TxIndex] used to avoid the need of `txindex=1`. - tx_index: Mutex>, + /// 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 [Carrier] instance. Data is sent to the `bitcoind` through it. carrier: Mutex, /// A [Gatekeeper] instance. Data regarding users is requested to it. 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 { /// Creates a new [Responder] instance. - pub fn new( - last_n_blocs: &[ValidatedBlock], - last_known_block_height: u32, - carrier: Carrier, - gatekeeper: Arc, - dbm: Arc>, - ) -> Self { + pub fn new(carrier: Carrier, 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_all_trackers() { + 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), - tx_index: Mutex::new(TxIndex::new(last_n_blocs, last_known_block_height)), + trackers: Mutex::new(trackers), + tx_tracker_map: Mutex::new(tx_tracker_map), 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.get_trackers_count() == 0 + self.trackers.lock().unwrap().is_empty() } - /// 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.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() + self.trackers.lock().unwrap().len() } /// Data entry point for the [Responder]. Handles a [Breach] provided by the [Watcher](crate::watcher::Watcher). @@ -190,20 +183,18 @@ impl Responder { breach: Breach, user_id: UserId, ) -> ConfirmationStatus { - let mut carrier = self.carrier.lock().unwrap(); - let tx_index = self.tx_index.lock().unwrap(); + // 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; + } - // Check whether the transaction is in mempool or part of our internal txindex. Send it to our node otherwise. - 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 { - carrier.send_transaction(&breach.penalty_tx) - }; - - if status.accepted() { + let status = self + .carrier + .lock() + .unwrap() + .send_transaction(&breach.penalty_tx); + if !matches!(status, ConfirmationStatus::Rejected { .. }) { self.add_tracker(uuid, breach, user_id, status); } @@ -225,177 +216,264 @@ impl Responder { user_id: UserId, status: ConfirmationStatus, ) { - if self - .dbm + let tracker = TransactionTracker::new(breach, user_id, status); + + self.trackers .lock() .unwrap() - .store_tracker(uuid, &TransactionTracker::new(breach, user_id, status)) - .is_ok() - { - log::info!("New tracker added (uuid={uuid})"); + .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); } else { - log::error!( - "Failed to store tracker in database (uuid={uuid}). It might be already stored." - ); + tx_tracker_map.insert(tracker.penalty_tx.txid(), HashSet::from_iter(vec![uuid])); } + + 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 { - self.dbm.lock().unwrap().tracker_exists(uuid) + // 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 + } } /// 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 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(); + /// Returns the set of completed trackers. + fn check_confirmations(&self, txids: &[Txid], current_height: u32) -> HashSet { + let mut completed_trackers = HashSet::new(); - 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 { + for (uuid, tracker) in self.trackers.lock().unwrap().iter_mut() { + if let ConfirmationStatus::ConfirmedIn(h) = tracker.status { + if current_height - h == constants::IRREVOCABLY_RESOLVED { // Tracker is deep enough in the chain, it can be deleted - completed_trackers.push(uuid); - } else { - log::info!("{uuid} received a confirmation (count={confirmations})"); + completed_trackers.insert(*uuid); } - } else if let ConfirmationStatus::InMempoolSince(h) = penalty_summary.status { + } 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 { // Log all transactions that have missed confirmations log::info!( "Transaction missed a confirmation: {} (missed conf count: {})", - penalty_summary.penalty_txid, + tracker.penalty_txid, current_height - h ); } } - (!completed_trackers.is_empty()).then_some(completed_trackers) + completed_trackers } - /// Handles the reorged out trackers when we start connecting to the stronger chain. + /// 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. /// - /// 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. - /// - /// 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(); + /// 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; - 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 - ); - true + 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)); } - // 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 + } 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). + /// + /// 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(); + let mut carrier = self.carrier.lock().unwrap(); + + for (uuid, (penalty_tx, dispute_tx)) in txs.into_iter() { + let status = if let Some(dispute_tx) = dispute_tx { + // The tracker was reorged out, and the dispute may potentially not be in the chain anymore. + if carrier.get_block_hash_for_tx(&dispute_tx.txid()).is_some() { + // Dispute tx is on chain, so we only need to care about the penalty + 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: {:?})", + dispute_tx.txid(), + e ); - true + status + } else { + // The dispute was accepted, so we can rebroadcast the penalty. + carrier.send_transaction(&penalty_tx) + } } - 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 - ), + } 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) }; - 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() - } + if let ConfirmationStatus::Rejected(_) = status { + rejected.insert(uuid); } else { - rejected.push(uuid) + // 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.is_empty()).then_some(rejected) + (accepted, rejected) } - /// Rebroadcasts a list of penalty transactions that have missed too many confirmations. - /// - /// 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(); + // DISCUSS: Check comment regarding callbacks in watcher.rs - // 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. + /// + /// 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), + } + + 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); + } } } + } - (!rejected.is_empty()).then_some(rejected) + /// 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, + ) { + self.delete_trackers_from_memory(uuids, reason); + self.dbm + .lock() + .unwrap() + .batch_remove_appointments(uuids, updated_users); } } @@ -411,70 +489,73 @@ impl chain::Listen for Responder { /// Every time a block is received the tracking conditions are checked against the monitored [TransactionTracker]s and /// data deletion is performed accordingly. Moreover, lack of confirmations is check for the tracked transactions and /// rebroadcasting is performed for those that have missed too many. - fn filtered_block_connected( - &self, - header: &bitcoin::block::Header, - txdata: &chain::transaction::TransactionData, - height: u32, - ) { - log::info!("New block received: {}", header.block_hash()); + fn block_connected(&self, block: &bitcoin::Block, height: u32) { + log::info!("New block received: {}", block.header.block_hash()); self.carrier.lock().unwrap().update_height(height); - let txs = txdata - .iter() - .map(|(_, tx)| (tx.compute_txid(), header.block_hash())) - .collect(); - self.tx_index.lock().unwrap().update(*header, &txs); + if self.trackers.lock().unwrap().len() > 0 { + // Complete those appointments that are due at this height + let completed_trackers = self.check_confirmations( + &block + .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, + ); - 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 + 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"); } } - - // 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: &bitcoin::block::Header, height: u32) { + fn block_disconnected(&self, header: &BlockHeader, 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()); - // 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(), - ); + + 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; + } + } } } @@ -482,110 +563,77 @@ 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::ops::Deref; use std::sync::{Arc, Mutex}; - use crate::dbm::DBM; + use crate::dbm::{Error as DBError, DBM}; + use crate::gatekeeper::UserInfo; use crate::rpc_errors; use crate::test_utils::{ - 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, + create_carrier, generate_dummy_appointment_with_user, generate_uuid, get_random_breach, + get_random_tracker, get_random_tx, get_random_user_id, store_appointment_and_fks_to_db, + Blockchain, MockedServerQuery, DURATION, EXPIRY_DELTA, SLOTS, START_HEIGHT, }; - use teos_common::constants::IRREVOCABLY_RESOLVED; - 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 { - // 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() + *self.trackers.lock().unwrap() == *other.trackers.lock().unwrap() + && *self.tx_tracker_map.lock().unwrap() == *other.tx_tracker_map.lock().unwrap() } } impl Eq for Responder {} impl Responder { - pub(crate) fn get_trackers(&self) -> HashMap { - self.dbm.lock().unwrap().load_trackers(None) + pub(crate) fn get_trackers(&self) -> &Mutex> { + &self.trackers } pub(crate) fn get_carrier(&self) -> &Mutex { &self.carrier } - pub(crate) fn add_random_tracker(&self, status: ConfirmationStatus) -> TransactionTracker { + pub(crate) fn add_random_tracker(&self, uuid: UUID, status: ConfirmationStatus) { let user_id = get_random_user_id(); let tracker = get_random_tracker(user_id, status); - self.add_dummy_tracker(&tracker); - tracker - } + // 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])); - pub(crate) fn add_dummy_tracker(&self, tracker: &TransactionTracker) { - let (_, appointment) = generate_dummy_appointment_with_user( - tracker.user_id, - Some(&tracker.dispute_tx.compute_txid()), - ); - store_appointment_and_its_user(&self.dbm.lock().unwrap(), &appointment); + // Add data to the db + let (_, appointment) = + generate_dummy_appointment_with_user(user_id, Some(&tracker.dispute_tx.txid())); + store_appointment_and_fks_to_db(&self.dbm.lock().unwrap(), uuid, &appointment); self.dbm .lock() .unwrap() - .store_tracker(appointment.uuid(), tracker) + .store_tracker(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( - chain: &mut Blockchain, + fn create_responder( + chain: &Blockchain, gatekeeper: Arc, dbm: Arc>, query: MockedServerQuery, - ) -> (Responder, BitcoindStopper) { - let height = if chain.tip().height < IRREVOCABLY_RESOLVED { - chain.tip().height - } else { - IRREVOCABLY_RESOLVED - }; - - let last_n_blocks = get_last_n_blocks(chain, height as usize).await; - - let (carrier, bitcoind_stopper) = create_carrier(query, chain.tip().height); - ( - Responder::new(&last_n_blocks, chain.tip().height, carrier, gatekeeper, dbm), - bitcoind_stopper, - ) + ) -> Responder { + let tip = chain.tip(); + Responder::new(create_carrier(query, tip.deref().height), gatekeeper, dbm) } - async fn init_responder_with_chain_and_dbm( + fn init_responder_with_chain_and_dbm( mocked_query: MockedServerQuery, - chain: &mut Blockchain, + chain: &Blockchain, dbm: Arc>, - ) -> (Responder, BitcoindStopper) { + ) -> Responder { let gk = Gatekeeper::new( chain.get_block_count(), SLOTS, @@ -593,29 +641,35 @@ mod tests { EXPIRY_DELTA, dbm.clone(), ); - create_responder(chain, Arc::new(gk), dbm, mocked_query).await + create_responder(chain, Arc::new(gk), dbm, mocked_query) } - async fn init_responder(mocked_query: MockedServerQuery) -> (Responder, BitcoindStopper) { + fn init_responder(mocked_query: MockedServerQuery) -> Responder { let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); - let mut chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 10); - init_responder_with_chain_and_dbm(mocked_query, &mut chain, dbm).await + let chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 10); + init_responder_with_chain_and_dbm(mocked_query, &chain, dbm) } #[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 IrrevocablyResolved are never passed to store). + // that's either ConfirmedIn or InMempoolSince (Rejected and Reorged are never passed to store). let h = 21; + let statuses = [true, false]; - assert_eq!( - ConfirmationStatus::from_db_data(h, true), - ConfirmationStatus::ConfirmedIn(h) - ); - assert_eq!( - ConfirmationStatus::from_db_data(h, false), - ConfirmationStatus::InMempoolSince(h) - ); + 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) + ); + } + } } #[test] @@ -633,23 +687,26 @@ mod tests { Some((h, false)) ); assert_eq!(ConfirmationStatus::Rejected(0).to_db_data(), None); - assert_eq!(ConfirmationStatus::IrrevocablyResolved.to_db_data(), None); + assert_eq!(ConfirmationStatus::ReorgedOut.to_db_data(), None); } - #[tokio::test] - async fn test_responder_new() { + #[test] + fn test_responder_new() { // A fresh responder has no associated data - let mut chain = Blockchain::default().with_height(START_HEIGHT); + let chain = Blockchain::default().with_height(START_HEIGHT); let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); - let (responder, _s) = - init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &mut chain, dbm.clone()) - .await; + let responder = + init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &chain, dbm.clone()); assert!(responder.is_fresh()); // 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 { - let (user_id, uuid) = responder.store_dummy_appointment_to_db(); + // 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 breach = get_random_breach(); let s = if i % 2 == 0 { ConfirmationStatus::InMempoolSince(i) @@ -660,122 +717,91 @@ mod tests { } // Create a new Responder reusing the same DB and check that the data is loaded - let (another_r, _) = - init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &mut chain, dbm).await; + let another_r = init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &chain, dbm); assert!(!responder.is_fresh()); assert_eq!(responder, another_r); } - #[tokio::test] - async fn test_handle_breach_accepted() { + #[test] + fn test_handle_breach_delivered() { let start_height = START_HEIGHT as u32; - let (responder, _s) = init_responder(MockedServerQuery::Regular).await; + let responder = init_responder(MockedServerQuery::Regular); + + 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) ); - let tracker = responder.dbm.lock().unwrap().load_tracker(uuid).unwrap(); + assert!(responder.trackers.lock().unwrap().contains_key(&uuid)); assert_eq!( - tracker.status, + responder.trackers.lock().unwrap()[&uuid].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, user_id), + responder.handle_breach(uuid, another_breach.clone(), user_id), ConfirmationStatus::InMempoolSince(start_height) ); - // Getting the tracker should return the old one. + + assert!(responder.trackers.lock().unwrap().contains_key(&uuid)); assert_eq!( - tracker, - responder.dbm.lock().unwrap().load_tracker(uuid).unwrap() - ); - } - - #[tokio::test] - async fn test_handle_breach_accepted_in_mempool() { - let start_height = START_HEIGHT as u32; - let (responder, _s) = init_responder(MockedServerQuery::InMempoool).await; - - let (user_id, uuid) = responder.store_dummy_appointment_to_db(); - let breach = get_random_breach(); - - assert_eq!( - responder.handle_breach(uuid, breach, user_id), + responder.trackers.lock().unwrap()[&uuid].status, ConfirmationStatus::InMempoolSince(start_height) ); - let tracker = responder.dbm.lock().unwrap().load_tracker(uuid).unwrap(); - assert_eq!( - tracker.status, - ConfirmationStatus::InMempoolSince(start_height) - ); - } - - #[tokio::test] - async fn test_handle_breach_accepted_in_txindex() { - let (responder, _s) = init_responder(MockedServerQuery::Regular).await; - - let (user_id, uuid) = responder.store_dummy_appointment_to_db(); - - let breach = get_random_breach(); - 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(); - responder - .tx_index + assert!(!responder + .tx_tracker_map .lock() .unwrap() - .index_mut() - .insert(penalty_txid, target_block_hash); - let target_height = responder - .tx_index - .lock() - .unwrap() - .get_height(&target_block_hash) - .unwrap() as u32; - - assert_eq!( - responder.handle_breach(uuid, breach, user_id), - ConfirmationStatus::ConfirmedIn(target_height) - ); - let tracker = responder.dbm.lock().unwrap().load_tracker(uuid).unwrap(); - assert_eq!( - tracker.status, - ConfirmationStatus::ConfirmedIn(target_height) - ); + .contains_key(&another_breach.penalty_tx.txid())); } - #[tokio::test] - async fn test_handle_breach_rejected() { - let (responder, _s) = init_responder(MockedServerQuery::Error( + #[test] + fn test_handle_breach_rejected() { + let responder = init_responder(MockedServerQuery::Error( rpc_errors::RPC_VERIFY_ERROR as i64, - )) - .await; + )); 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.has_tracker(uuid)); + assert!(!responder.trackers.lock().unwrap().contains_key(&uuid)); + assert!(!responder + .tx_tracker_map + .lock() + .unwrap() + .contains_key(&penalty_txid)); } - #[tokio::test] - async fn test_add_tracker() { - let (responder, _s) = init_responder(MockedServerQuery::Regular).await; + #[test] + fn test_add_tracker() { + let responder = init_responder(MockedServerQuery::Regular); let start_height = START_HEIGHT as u32; - let (user_id, uuid) = responder.store_dummy_appointment_to_db(); + // 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 mut breach = get_random_breach(); responder.add_tracker( uuid, @@ -784,7 +810,21 @@ mod tests { ConfirmationStatus::InMempoolSince(start_height), ); - // Check that the data has been added to the responder. + // 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 assert_eq!( responder.dbm.lock().unwrap().load_tracker(uuid).unwrap(), TransactionTracker::new( @@ -795,9 +835,16 @@ mod tests { ); // Adding a confirmed tracker should result in the same but with the height being set. - - let (user_id, uuid) = responder.store_dummy_appointment_to_db(); + let uuid = generate_uuid(); breach = get_random_breach(); + + responder + .dbm + .lock() + .unwrap() + .store_appointment(uuid, &appointment) + .unwrap(); + responder.add_tracker( uuid, breach.clone(), @@ -805,6 +852,23 @@ 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( @@ -814,8 +878,15 @@ mod tests { ) ); - // Adding another breach with the same penalty transaction (but different uuid) - let (user_id, uuid) = responder.store_dummy_appointment_to_db(); + // 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(); + responder.add_tracker( uuid, breach.clone(), @@ -823,6 +894,16 @@ 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( @@ -833,15 +914,18 @@ mod tests { ); } - #[tokio::test] - async fn test_has_tracker() { + #[test] + fn test_has_tracker() { // Has tracker should return true as long as the given tracker is held by the Responder. // As long as the tracker is in Responder.trackers and Responder.tx_tracker_map, the return // must be true. - let (responder, _s) = init_responder(MockedServerQuery::Regular).await; + let responder = init_responder(MockedServerQuery::Regular); // Add a new tracker - let (user_id, uuid) = responder.store_dummy_appointment_to_db(); + let user_id = get_random_user_id(); + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); + store_appointment_and_fks_to_db(&responder.dbm.lock().unwrap(), uuid, &appointment); + let breach = get_random_breach(); responder.add_tracker( uuid, @@ -852,22 +936,28 @@ mod tests { assert!(responder.has_tracker(uuid)); - // Delete the tracker and check again. - responder.gatekeeper.delete_appointments(vec![uuid], false); + // Delete the tracker and check again (updated users are irrelevant here) + responder.delete_trackers( + &HashSet::from_iter([uuid]), + &HashMap::new(), + DeletionReason::Completed, + ); assert!(!responder.has_tracker(uuid)); } - #[tokio::test] - async fn test_get_tracker() { + #[test] + fn test_get_tracker() { // Should return a tracker as long as it exists let start_height = START_HEIGHT as u32; - let (responder, _s) = init_responder(MockedServerQuery::Regular).await; + let responder = init_responder(MockedServerQuery::Regular); // Store the user and the appointment in the database so we can add the tracker later on (due to FK restrictions) - let (user_id, uuid) = responder.store_dummy_appointment_to_db(); + 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); // Data should not be there before adding it - assert!(responder.dbm.lock().unwrap().load_tracker(uuid).is_none()); + assert_eq!(responder.get_tracker(uuid), None); // Data should be there now let breach = get_random_breach(); @@ -878,7 +968,7 @@ mod tests { ConfirmationStatus::InMempoolSince(start_height), ); assert_eq!( - responder.dbm.lock().unwrap().load_tracker(uuid).unwrap(), + responder.get_tracker(uuid).unwrap(), TransactionTracker::new( breach, user_id, @@ -886,14 +976,18 @@ mod tests { ) ); - // 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()); + // 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); } - #[tokio::test] - async fn test_check_confirmations() { - let (responder, _s) = init_responder(MockedServerQuery::Regular).await; + #[test] + fn test_check_confirmations() { + let responder = init_responder(MockedServerQuery::Regular); let target_height = (START_HEIGHT * 2) as u32; // Unconfirmed transactions that miss a confirmation will be added to missed_confirmations (if not there) or their missed confirmation count till be increased @@ -901,69 +995,67 @@ mod tests { let mut just_confirmed = HashSet::new(); let mut confirmed = HashSet::new(); let mut completed = HashSet::new(); - let mut txids = HashSet::new(); + let mut txids = Vec::new(); for i in 0..40 { - let (user_id, uuid) = responder.store_dummy_appointment_to_db(); + let user_id = get_random_user_id(); + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); let breach = get_random_breach(); - 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); - } + 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); } } // The trackers that were completed should be returned assert_eq!( completed, - HashSet::from_iter(responder.check_confirmations(txids, target_height).unwrap()) + responder.check_confirmations(&txids, target_height) ); // The ones in mempool should still be there (at the same height) for uuid in in_mempool { assert_eq!( responder - .dbm + .trackers .lock() .unwrap() - .load_tracker(uuid) + .get(&uuid) .unwrap() .status, ConfirmationStatus::InMempoolSince(21) @@ -974,10 +1066,10 @@ mod tests { for uuid in just_confirmed { assert_eq!( responder - .dbm + .trackers .lock() .unwrap() - .load_tracker(uuid) + .get(&uuid) .unwrap() .status, ConfirmationStatus::ConfirmedIn(target_height) @@ -988,10 +1080,10 @@ mod tests { for uuid in confirmed { assert_eq!( responder - .dbm + .trackers .lock() .unwrap() - .load_tracker(uuid) + .get(&uuid) .unwrap() .status, ConfirmationStatus::ConfirmedIn(42) @@ -999,211 +1091,504 @@ mod tests { } } - #[tokio::test] - async fn test_handle_reorged_txs() { - let (responder, _s) = init_responder(MockedServerQuery::InMempoool).await; - let mut trackers = Vec::new(); + #[test] + fn test_get_txs_to_rebroadcast() { + let responder = init_responder(MockedServerQuery::Regular); + let current_height = 100; - for _ in 0..10 { - let uuid = responder - .add_random_tracker(ConfirmationStatus::ConfirmedIn(42)) - .uuid(); - responder.reorged_trackers.lock().unwrap().insert(uuid); - trackers.push(uuid); - } + let user_id = get_random_user_id(); + responder + .dbm + .lock() + .unwrap() + .store_user(user_id, &UserInfo::new(21, 42)) + .unwrap(); - 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()); + // Transactions are flagged to be rebroadcast when they've been in mempool for longer than CONFIRMATIONS_BEFORE_RETRY + let mut txs = HashMap::new(); - // And all the reorged trackers should have in mempool since `height` status. - for uuid in trackers { - assert_eq!( - responder - .dbm - .lock() - .unwrap() - .load_tracker(uuid) - .unwrap() - .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 + 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() - .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); + .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_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; + #[test] + fn test_get_txs_to_rebroadcast_reorged() { + // For reorged transactions this works a bit different, the dispute transaction will also be returned here + let responder = init_responder(MockedServerQuery::Regular); + let current_height = 100; - for i in 0..height { - let status = if i % 4 == 0 { - ConfirmationStatus::ConfirmedIn(i) - } else { - ConfirmationStatus::InMempoolSince(i) - }; + let user_id = get_random_user_id(); + responder + .dbm + .lock() + .unwrap() + .store_user(user_id, &UserInfo::new(21, 42)) + .unwrap(); - let uuid = responder.add_random_tracker(status).uuid(); - statues.insert(uuid, status); - } + // Transactions are flagged to be rebroadcast when they've been in mempool for longer than CONFIRMATIONS_BEFORE_RETRY + let mut txs = HashMap::new(); - // `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 + 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() - .load_tracker(uuid) + .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 should 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) + } + + #[test] + fn test_get_outdated_trackers() { + let responder = init_responder(MockedServerQuery::Regular); + + // 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() - .status; - // All tracker statues shouldn't change since the submitted ones were all rejected. - assert_eq!(status, former_status); + .insert(uuid, tracker.get_summary()); + } + + // Check the expected data is there + assert_eq!( + responder.get_outdated_trackers(target_block_height), + target_uuids + ); + } + + #[test] + fn test_rebroadcast_accepted() { + // This test positive rebroadcast cases, including reorgs. However, complex reorg logic is not tested here, it will need a + // dedicated test (against bitcoind, not mocked). + let responder = init_responder(MockedServerQuery::Regular); + let 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(21, 42)) + .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()); + } + + #[test] + fn test_rebroadcast_rejected() { + // This test negative rebroadcast cases, including reorgs. However, complex reorg logic is not tested here, it will need a + // dedicated test (against bitcoind, not mocked). + let responder = init_responder(MockedServerQuery::Error( + rpc_errors::RPC_VERIFY_ERROR as i64, + )); + 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(21, 42)) + .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()); + } + + #[test] + fn test_delete_trackers_from_memory() { + let responder = init_responder(MockedServerQuery::Regular); + + // Add user to the database + let user_id = get_random_user_id(); + responder + .dbm + .lock() + .unwrap() + .store_user(user_id, &UserInfo::new(21, 42)) + .unwrap(); + + // Add some trackers both to memory and to the database + let mut to_be_deleted = HashMap::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()); + } + + // 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, + ); + + 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_filtered_block_connected() { + #[test] + fn test_delete_trackers() { + let responder = init_responder(MockedServerQuery::Regular); + + // Add user to the database + let user_id = get_random_user_id(); + responder + .dbm + .lock() + .unwrap() + .store_user(user_id, &UserInfo::new(21, 42)) + .unwrap(); + + // Delete trackers removes data from the trackers, tx_tracker_map maps, the database. The deletion of the later is + // better check in test_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(i, 42)); + } + } + + 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 { + assert_eq!( + responder + .dbm + .lock() + .unwrap() + .load_user(id) + .unwrap() + .available_slots, + info.available_slots + ) + } + } + + #[test] + fn test_block_connected() { let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); let start_height = START_HEIGHT * 2; let mut chain = Blockchain::default().with_height(start_height); - let (responder, _s) = - init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &mut chain, dbm).await; + let responder = init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &chain, dbm); - // filtered_block_connected is used to keep track of the confirmation received (or missed) by the trackers the Responder + // 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 invalid data (and update the data in the GK) + // - Delete completed and outdated data (including 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 0..21 { + for _ in 2..23 { let user_id = get_random_user_id(); + responder.gatekeeper.add_update_user(user_id).unwrap(); users.push(user_id); } - // COMPLETED TRACKERS SETUP - let mut completed_trackers = Vec::new(); - for i in 0..10 { - // Add these trackers to the first two users - let user_id = users[i % 2]; - let dispute_tx = get_random_tx(); - let (uuid, appointment) = - generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.compute_txid())); + let mut completed_trackers = HashMap::new(); + + // COMPLETED TRACKERS SETUP + for i in 0..10 { + // Adding two trackers to each user + let user_id = users[i % 2]; + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - responder - .gatekeeper - .add_update_appointment(user_id, uuid, &appointment) - .unwrap(); responder .dbm .lock() @@ -1212,42 +1597,58 @@ mod tests { .unwrap(); // Trackers complete in the next block. - let breach = Breach::new(dispute_tx, get_random_tx()); - let status = ConfirmationStatus::ConfirmedIn( - target_block_height - constants::IRREVOCABLY_RESOLVED, + let breach = get_random_breach(); + responder.add_tracker( + uuid, + breach.clone(), + user_id, + ConfirmationStatus::ConfirmedIn( + target_block_height - constants::IRREVOCABLY_RESOLVED, + ), ); - responder.add_tracker(uuid, breach.clone(), user_id, status); - completed_trackers.push(TransactionTracker::new(breach, user_id, status)); + responder + .gatekeeper + .get_registered_users() + .lock() + .unwrap() + .get_mut(&user_id) + .unwrap() + .appointments + .insert(uuid, 1); + + completed_trackers.insert(uuid, (user_id, breach)); } // OUTDATED TRACKER SETUP - 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(); + 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); responder .dbm .lock() .unwrap() - .store_appointment(uuid, &appointment) + .store_appointment(*uuid, &appointment) .unwrap(); - 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)); + let breach = get_random_breach(); + penalties.push(breach.penalty_tx.txid()); + responder.add_tracker( + *uuid, + breach, + *user_id, + ConfirmationStatus::InMempoolSince(target_block_height - 1), + ); } - // Outdate this user so their trackers are deleted + uuids.extend(pair.clone()); responder .gatekeeper - .add_outdated_user(user_id, target_block_height); + .add_outdated_user(*user_id, target_block_height, Some(pair)); } // CONFIRMATIONS SETUP @@ -1257,18 +1658,11 @@ mod tests { .add_update_user(standalone_user_id) .unwrap(); - let mut missed_confirmation_trackers = Vec::new(); - let mut just_confirmed_trackers = Vec::new(); + let mut transactions = Vec::new(); + let mut just_confirmed_txs = Vec::new(); for i in 0..10 { - 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(); + let (uuid, appointment) = + generate_dummy_appointment_with_user(standalone_user_id, None); responder .dbm .lock() @@ -1276,55 +1670,39 @@ mod tests { .store_appointment(uuid, &appointment) .unwrap(); - let breach = Breach::new(dispute_tx, get_random_tx()); + let breach = get_random_breach(); + transactions.push(breach.clone().penalty_tx.txid()); - let status = ConfirmationStatus::InMempoolSince(target_block_height - 1); - responder.add_tracker(uuid, breach.clone(), standalone_user_id, status); if i % 2 == 0 { - just_confirmed_trackers.push(TransactionTracker::new( - breach, - standalone_user_id, - status, - )); - } else { - missed_confirmation_trackers.push(TransactionTracker::new( - breach, - standalone_user_id, - status, - )); + just_confirmed_txs.push(breach.clone().penalty_tx); } + responder.add_tracker( + uuid, + breach, + standalone_user_id, + ConfirmationStatus::InMempoolSince(target_block_height - 1), + ); } // REBROADCAST SETUP - 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(); + let (uuid, appointment) = generate_dummy_appointment_with_user(standalone_user_id, None); - let breach = Breach::new(dispute_tx, get_random_tx()); - let status = ConfirmationStatus::InMempoolSince( + 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( 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 @@ -1333,22 +1711,13 @@ mod tests { .lock() .unwrap() .get_issued_receipts() - .insert( - get_random_tx().compute_txid(), - ConfirmationStatus::ConfirmedIn(21), - ); + .insert(get_random_tx().txid(), ConfirmationStatus::ConfirmedIn(21)); // Connecting a block should trigger all the state transitions - 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); + responder.block_connected( + &chain.generate(Some(just_confirmed_txs.clone())), + chain.get_block_count(), + ); // CARRIER CHECKS assert!(responder @@ -1366,93 +1735,88 @@ mod tests { // COMPLETED TRACKERS CHECKS // Data should have been removed - for tracker in completed_trackers { - assert!(responder - .dbm + for (uuid, (user_id, breach)) in completed_trackers { + assert!(!responder.trackers.lock().unwrap().contains_key(&uuid)); + assert!(!responder + .tx_tracker_map .lock() .unwrap() - .load_tracker(tracker.uuid()) - .is_none()); - let (_, user_locators) = responder.gatekeeper.get_user_info(tracker.user_id).unwrap(); - assert!(!user_locators.contains(&tracker.locator())); + .contains_key(&breach.penalty_tx.txid())); + assert!( + !responder.gatekeeper.get_registered_users().lock().unwrap()[&user_id] + .appointments + .contains_key(&uuid) + ); } // OUTDATED TRACKERS CHECKS - // 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()); + // 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)); } // CONFIRMATIONS CHECKS // The transaction confirmation count / confirmation missed should have been updated - 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) - ); + 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) + ); + } + } } // REBROADCAST CHECKS - for tracker in trackers_to_rebroadcast { - assert_eq!( - responder - .dbm - .lock() - .unwrap() - .load_tracker(tracker.uuid()) - .unwrap() - .status, - ConfirmationStatus::InMempoolSince(target_block_height), - ); - } + assert_eq!( + responder + .trackers + .lock() + .unwrap() + .get(&tracker_to_rebroadcast) + .unwrap() + .status, + ConfirmationStatus::InMempoolSince(target_block_height), + ); } - #[tokio::test] - async fn test_block_disconnected() { + #[test] + fn test_block_disconnected() { let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); - let mut chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 10); - let (responder, _s) = - init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &mut chain, dbm).await; + let chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 10); + let responder = init_responder_with_chain_and_dbm(MockedServerQuery::Regular, &chain, dbm); // Add user to the database let user_id = get_random_user_id(); - responder.gatekeeper.add_update_user(user_id).unwrap(); + responder + .dbm + .lock() + .unwrap() + .store_user(user_id, &UserInfo::new(21, 42)) + .unwrap(); let mut reorged = Vec::new(); - let block_range = START_HEIGHT - 10..START_HEIGHT; - 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())); + 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() @@ -1460,33 +1824,49 @@ mod tests { .store_appointment(uuid, &appointment) .unwrap(); - let breach = Breach::new(dispute_tx, get_random_tx()); + let breach = get_random_breach(); + responder.add_tracker( uuid, - breach, + breach.clone(), user_id, - ConfirmationStatus::ConfirmedIn(i as u32), + ConfirmationStatus::ConfirmedIn(i), ); reorged.push(uuid); } // Check that trackers are flagged as reorged if the height they were included at gets disconnected - for (i, uuid) in block_range.clone().zip(reorged.iter()).rev() { + for i in (0..10).rev() { // The header doesn't really matter, just the height - responder.block_disconnected(&chain.tip().header, i as u32); + responder.block_disconnected(&chain.tip().header, i); // Check that the proper tracker gets reorged at the proper height - assert!(responder.reorged_trackers.lock().unwrap().contains(uuid)); + assert_eq!( + responder + .trackers + .lock() + .unwrap() + .get(reorged.get(i as usize).unwrap()) + .unwrap() + .status, + ConfirmationStatus::ReorgedOut + ); + // Check that the carrier block_height has been updated - assert_eq!(responder.carrier.lock().unwrap().get_height(), i as u32); + assert_eq!(responder.carrier.lock().unwrap().get_height(), i); } // Check that all reorged trackers are still reorged - for uuid in reorged.iter() { - assert!(responder.reorged_trackers.lock().unwrap().contains(uuid)); + for uuid in reorged { + assert_eq!( + responder + .trackers + .lock() + .unwrap() + .get(&uuid) + .unwrap() + .status, + ConfirmationStatus::ReorgedOut + ); } - - // 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 16f9675..a1ab19a 100644 --- a/teos/src/test_utils.rs +++ b/teos/src/test_utils.rs @@ -8,39 +8,38 @@ */ use rand::Rng; +use std::convert::TryInto; use std::ops::Deref; use std::sync::{Arc, Condvar, Mutex}; use std::thread; use jsonrpc_http_server::jsonrpc_core::error::ErrorCode as JsonRpcErrorCode; use jsonrpc_http_server::jsonrpc_core::{Error as JsonRpcError, IoHandler, Params, Value}; -use jsonrpc_http_server::{CloseHandle, Server, ServerBuilder}; +use jsonrpc_http_server::{Server, ServerBuilder}; use bitcoincore_rpc::{Auth, Client as BitcoindClient}; -use bitcoin::block::Block; +use bitcoin::blockdata::block::{Block, BlockHeader}; use bitcoin::blockdata::constants::genesis_block; -use bitcoin::blockdata::script::{Builder, ScriptBuf}; +use bitcoin::blockdata::script::{Builder, Script}; use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxIn, TxOut}; use bitcoin::hash_types::BlockHash; use bitcoin::hash_types::Txid; +use bitcoin::hashes::hex::FromHex; use bitcoin::hashes::Hash; -use bitcoin::merkle_tree::calculate_root; -use bitcoin::pow::Work; -use bitcoin::Amount; -use bitcoin::Network; -use bitcoin::Witness; +use bitcoin::network::constants::Network; +use bitcoin::util::hash::bitcoin_merkle_root; +use bitcoin::util::psbt::serialize::Deserialize; +use bitcoin::util::uint::Uint256; use lightning_block_sync::poll::{ ChainPoller, Poll, Validate, ValidatedBlock, ValidatedBlockHeader, }; use lightning_block_sync::{ - AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource, BlockSourceError, - UnboundedCache, + AsyncBlockSourceResult, BlockHeaderData, BlockSource, BlockSourceError, UnboundedCache, }; -use teos_common::constants::IRREVOCABLY_RESOLVED; -use teos_common::cryptography::{get_random_bytes, get_random_keypair}; -use teos_common::test_utils::{generate_random_appointment, get_random_user_id, TXID_HEX, TX_HEX}; +use teos_common::appointment::{Appointment, Locator}; +use teos_common::cryptography::{encrypt, get_random_bytes, get_random_keypair}; use teos_common::UserId; use crate::api::internal::InternalAPI; @@ -48,20 +47,18 @@ use crate::carrier::Carrier; use crate::dbm::DBM; use crate::extended_appointment::{ExtendedAppointment, UUID}; use crate::gatekeeper::{Gatekeeper, UserInfo}; -use crate::protos as msgs; use crate::responder::{ConfirmationStatus, Responder, TransactionTracker}; -use crate::rpc_errors; use crate::watcher::{Breach, Watcher}; +pub(crate) static TX_HEX: &str = "010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff54038e830a1b4d696e656420627920416e74506f6f6c373432c2005b005e7a0ae3fabe6d6d7841cd582ead8ea5dd8e3de1173cae6fcd2a53c7362ebb7fb6f815604fe07cbe0200000000000000ac0e060005f90000ffffffff04d9476026000000001976a91411dbe48cc6b617f9c6adaf4d9ed5f625b1c7cb5988ac0000000000000000266a24aa21a9ed7248c6efddd8d99bfddd7f499f0b915bffa8253003cc934df1ff14a81301e2340000000000000000266a24b9e11b6d7054937e13f39529d6ad7e685e9dd4efa426f247d5f5a5bed58cdddb2d0fa60100000000000000002b6a2952534b424c4f434b3a054a68aa5368740e8b3e3c67bce45619c2cfd07d4d4f0936a5612d2d0034fa0a0120000000000000000000000000000000000000000000000000000000000000000000000000"; +pub(crate) static TXID_HEX: &str = + "338bda693c4a26e0d41a01f7f2887aaf48bf0bdf93e6415c9110b29349349d3e"; + pub(crate) const SLOTS: u32 = 21; pub(crate) const DURATION: u32 = 500; pub(crate) const EXPIRY_DELTA: u32 = 42; pub(crate) const START_HEIGHT: usize = 100; -pub(crate) const AVAILABLE_SLOTS: u32 = 21; -pub(crate) const SUBSCRIPTION_START: u32 = START_HEIGHT as u32; -pub(crate) const SUBSCRIPTION_EXPIRY: u32 = SUBSCRIPTION_START + 42; - #[derive(Clone, Default, Debug)] pub(crate) struct Blockchain { pub blocks: Vec, @@ -87,10 +84,23 @@ impl Blockchain { pub fn with_height(mut self, height: usize) -> Self { self.blocks.reserve_exact(height); - for _ in 1..=height { - self.generate(None); + let bits = BlockHeader::compact_target_from_u256(&Uint256::from_be_bytes([0xff; 32])); + for i in 1..=height { + let prev_block = &self.blocks[i - 1]; + let prev_blockhash = prev_block.block_hash(); + let time = prev_block.header.time + height as u32; + self.blocks.push(Block { + header: BlockHeader { + version: 0, + prev_blockhash, + merkle_root: Default::default(), + time, + bits, + nonce: 0, + }, + txdata: vec![], + }); } - self } @@ -156,11 +166,8 @@ 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() + Work::from_be_bytes(padded_bytes), + chainwork: self.blocks[0].header.work() + Uint256::from_u64(height as u64).unwrap(), height: height as u32, header: self.blocks[height].header, } @@ -190,36 +197,29 @@ impl Blockchain { } pub fn generate(&mut self, txs: Option>) -> Block { - let bits = bitcoin::Target::from_be_bytes([0xff; 32]).to_compact_lossy(); + let bits = BlockHeader::compact_target_from_u256(&Uint256::from_be_bytes([0xff; 32])); let prev_block = self.blocks.last().unwrap(); let prev_blockhash = prev_block.block_hash(); let time = prev_block.header.time + (self.blocks.len() + 1) as u32; let txdata = match txs { - Some(v) => { - if v.is_empty() { - vec![get_random_tx()] - } else { - v - } - } - None => vec![get_random_tx()], + Some(t) => t, + None => vec![], }; - 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: calculate_root(hashes).unwrap().into(), - time, - bits, - nonce: 0, + let hashes = txdata.iter().map(|obj| obj.txid().as_hash()); + + let block = Block { + header: BlockHeader { + version: 0, + prev_blockhash, + merkle_root: bitcoin_merkle_root(hashes).into(), + time, + bits, + nonce: 0, + }, + txdata, }; - while header.validate_pow(header.target()).is_err() { - header.nonce += 1; - } - - let block = Block { header, txdata }; self.blocks.push(block.clone()); block @@ -228,7 +228,7 @@ impl Blockchain { impl BlockSource for Blockchain { fn get_header<'a>( - &'a self, + &'a mut self, header_hash: &'a BlockHash, _height_hint: Option, ) -> AsyncBlockSourceResult<'a, BlockHeaderData> { @@ -251,7 +251,10 @@ impl BlockSource for Blockchain { }) } - fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult { + fn get_block<'a>( + &'a mut self, + header_hash: &'a BlockHash, + ) -> AsyncBlockSourceResult<'a, Block> { Box::pin(async move { for (height, block) in self.blocks.iter().enumerate() { if block.header.block_hash() == *header_hash { @@ -260,14 +263,15 @@ impl BlockSource for Blockchain { return Err(BlockSourceError::persistent("block not found")); } } - return Ok(BlockData::FullBlock(block.clone())); + + return Ok(block.clone()); } } Err(BlockSourceError::transient("block not found")) }) } - fn get_best_block(&self) -> AsyncBlockSourceResult<(BlockHash, Option)> { + fn get_best_block(&mut self) -> AsyncBlockSourceResult<(BlockHash, Option)> { Box::pin(async move { if *self.unreachable.lock().unwrap() { return Err(BlockSourceError::transient("Connection refused")); @@ -286,7 +290,13 @@ impl BlockSource for Blockchain { pub(crate) fn generate_uuid() -> UUID { let mut rng = rand::thread_rng(); - UUID::from_slice(&rng.gen::<[u8; 20]>()).unwrap() + UUID::deserialize(&rng.gen::<[u8; 20]>()).unwrap() +} + +pub(crate) fn get_random_user_id() -> UserId { + let (_, pk) = get_random_keypair(); + + UserId(pk) } pub(crate) fn get_random_tx() -> Transaction { @@ -294,26 +304,42 @@ pub(crate) fn get_random_tx() -> Transaction { let prev_txid_bytes = get_random_bytes(32); Transaction { - version: bitcoin::transaction::Version(2), - lock_time: bitcoin::locktime::absolute::LockTime::from_height(0).unwrap(), + version: 2, + lock_time: 0, input: vec![TxIn { previous_output: OutPoint::new( Txid::from_slice(&prev_txid_bytes).unwrap(), rng.gen_range(0..200), ), - script_sig: ScriptBuf::new(), - witness: Witness::new(), - sequence: bitcoin::Sequence(0), + script_sig: Script::new(), + witness: Vec::new(), + sequence: 0, }], output: vec![TxOut { script_pubkey: Builder::new().push_int(1).into_script(), - value: Amount::from_sat(rng.gen_range(0..21_000_000_000)), + value: rng.gen_range(0..21000000000), }], } } pub(crate) fn generate_dummy_appointment(dispute_txid: Option<&Txid>) -> ExtendedAppointment { - let appointment = generate_random_appointment(dispute_txid); + let dispute_txid = match dispute_txid { + Some(l) => *l, + None => { + let prev_txid_bytes = get_random_bytes(32); + Txid::from_slice(&prev_txid_bytes).unwrap() + } + }; + + let tx_bytes = Vec::from_hex(TX_HEX).unwrap(); + let penalty_tx = Transaction::deserialize(&tx_bytes).unwrap(); + + let mut raw_locator: [u8; 16] = get_random_bytes(16).try_into().unwrap(); + raw_locator.copy_from_slice(&dispute_txid[..16]); + let locator = Locator::deserialize(&raw_locator).unwrap(); + + let encrypted_blob = encrypt(&penalty_tx, &dispute_txid).unwrap(); + let appointment = Appointment::new(locator, encrypted_blob, 21); let user_id = get_random_user_id(); let user_signature = String::new(); let start_block = 42; @@ -328,7 +354,7 @@ pub(crate) fn generate_dummy_appointment_with_user( let mut app = generate_dummy_appointment(dispute_txid); app.user_id = user_id; - (app.uuid(), app) + (UUID::new(app.locator(), user_id), app) } pub(crate) fn get_random_breach() -> Breach { @@ -346,82 +372,62 @@ pub(crate) fn get_random_tracker( TransactionTracker::new(breach, user_id, status) } -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), - ) - // It's ok if the user is already stored. - .ok(); - dbm.store_appointment(appointment.uuid(), appointment) +pub(crate) fn store_appointment_and_fks_to_db( + dbm: &DBM, + uuid: UUID, + appointment: &ExtendedAppointment, +) { + dbm.store_user(appointment.user_id, &UserInfo::new(21, 42)) .unwrap(); + dbm.store_appointment(uuid, appointment).unwrap(); } pub(crate) async fn get_last_n_blocks(chain: &mut Blockchain, n: usize) -> Vec { - let mut last_n_blocks = Vec::with_capacity(n); - let mut last_known_block = Ok(chain.tip()); - let poller = ChainPoller::new(chain, Network::Regtest); + let tip = chain.tip(); + let mut poller = ChainPoller::new(chain, Network::Bitcoin); + let mut last_n_blocks = Vec::new(); + let mut last_known_block = tip; for _ in 0..n { - let header = last_known_block.unwrap(); - let block = poller.fetch_block(&header).await.unwrap(); + let block = poller.fetch_block(&last_known_block).await.unwrap(); + last_known_block = poller + .look_up_previous_header(&last_known_block) + .await + .unwrap(); last_n_blocks.push(block); - last_known_block = poller.look_up_previous_header(&header).await; } last_n_blocks } -pub(crate) fn get_full_blocks(last_n_blocks: &[ValidatedBlock]) -> 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, Error(i64), } -pub(crate) fn create_carrier(query: MockedServerQuery, height: u32) -> (Carrier, BitcoindStopper) { +pub(crate) fn create_carrier(query: MockedServerQuery, height: u32) -> Carrier { let bitcoind_mock = match query { - MockedServerQuery::Regular => BitcoindMock::new(MockOptions::default()), - MockedServerQuery::InMempoool => BitcoindMock::new(MockOptions::in_mempool()), + MockedServerQuery::Regular => BitcoindMock::new(MockOptions::empty()), MockedServerQuery::Error(x) => BitcoindMock::new(MockOptions::with_error(x)), }; let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap()); let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); - start_server(bitcoind_mock.server); + start_server(bitcoind_mock); - ( - Carrier::new(bitcoin_cli, bitcoind_reachable, height), - bitcoind_mock.stopper, - ) + Carrier::new(bitcoin_cli, bitcoind_reachable, height) } -pub(crate) async fn create_responder( - chain: &mut Blockchain, +pub(crate) fn create_responder( + tip: ValidatedBlockHeader, gatekeeper: Arc, dbm: Arc>, server_url: &str, ) -> Responder { - let height = chain.tip().height; - // For the local TxIndex logic to be sound, our index needs to have, at least, IRREVOCABLY_RESOLVED blocks - debug_assert!(height >= IRREVOCABLY_RESOLVED); - - let last_n_blocks = get_last_n_blocks(chain, IRREVOCABLY_RESOLVED as usize).await; - let bitcoin_cli = Arc::new(BitcoindClient::new(server_url, Auth::None).unwrap()); let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); - let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, height); + let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, tip.deref().height); - Responder::new(last_n_blocks.as_slice(), height, carrier, gatekeeper, dbm) + Responder::new(carrier, gatekeeper, dbm) } pub(crate) async fn create_watcher( @@ -430,23 +436,20 @@ pub(crate) async fn create_watcher( gatekeeper: Arc, bitcoind_mock: BitcoindMock, dbm: Arc>, -) -> (Watcher, BitcoindStopper) { +) -> Watcher { let last_n_blocks = get_last_n_blocks(chain, 6).await; - start_server(bitcoind_mock.server); + start_server(bitcoind_mock); let (tower_sk, tower_pk) = get_random_keypair(); let tower_id = UserId(tower_pk); - ( - Watcher::new( - gatekeeper, - responder, - last_n_blocks.as_slice(), - chain.get_block_count(), - tower_sk, - tower_id, - dbm, - ), - bitcoind_mock.stopper, + Watcher::new( + gatekeeper, + responder, + last_n_blocks, + chain.get_block_count(), + tower_sk, + tower_id, + dbm, ) } #[derive(Clone)] @@ -481,10 +484,8 @@ impl Default for ApiConfig { } } -pub(crate) async fn create_api_with_config( - api_config: ApiConfig, -) -> (Arc, BitcoindStopper) { - let bitcoind_mock = BitcoindMock::new(MockOptions::default()); +pub(crate) async fn create_api_with_config(api_config: ApiConfig) -> Arc { + let bitcoind_mock = BitcoindMock::new(MockOptions::empty()); let mut chain = Blockchain::default().with_height(START_HEIGHT); let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); @@ -495,9 +496,8 @@ pub(crate) async fn create_api_with_config( EXPIRY_DELTA, dbm.clone(), )); - let responder = - create_responder(&mut chain, gk.clone(), dbm.clone(), bitcoind_mock.url()).await; - let (watcher, stopper) = create_watcher( + let responder = create_responder(chain.tip(), gk.clone(), dbm.clone(), bitcoind_mock.url()); + let watcher = create_watcher( &mut chain, Arc::new(responder), gk.clone(), @@ -508,66 +508,58 @@ pub(crate) async fn create_api_with_config( let bitcoind_reachable = Arc::new((Mutex::new(api_config.bitcoind_reachable), Condvar::new())); let (shutdown_trigger, _) = triggered::trigger(); - ( - Arc::new(InternalAPI::new( - Arc::new(watcher), - vec![msgs::NetworkAddress::from_ipv4("address".to_string(), 21)], - bitcoind_reachable, - shutdown_trigger, - )), - stopper, - ) + Arc::new(InternalAPI::new( + Arc::new(watcher), + bitcoind_reachable, + shutdown_trigger, + )) } -pub(crate) async fn create_api() -> (Arc, BitcoindStopper) { +pub(crate) async fn create_api() -> Arc { create_api_with_config(ApiConfig::default()).await } - -#[derive(Clone)] -pub struct BitcoindStopper { - close_handle: CloseHandle, -} - -impl BitcoindStopper { - pub fn new(close_handle: CloseHandle) -> Self { - Self { close_handle } - } - - pub fn close_handle(&self) -> CloseHandle { - self.close_handle.clone() - } -} - -impl Drop for BitcoindStopper { - fn drop(&mut self) { - self.close_handle().close() - } -} - pub(crate) struct BitcoindMock { pub url: String, pub server: Server, - stopper: BitcoindStopper, } -#[derive(Default)] pub(crate) struct MockOptions { error_code: Option, - in_mempool: bool, + block_hash: Option, + height: Option, } impl MockOptions { - pub fn with_error(error_code: i64) -> Self { + pub fn new(error_code: i64, block_hash: BlockHash, height: usize) -> Self { Self { error_code: Some(error_code), - in_mempool: false, + block_hash: Some(block_hash), + height: Some(height), } } - pub fn in_mempool() -> Self { + pub fn empty() -> Self { Self { error_code: None, - in_mempool: true, + block_hash: None, + height: None, + } + } + + pub fn with_error(error_code: i64) -> Self { + Self { + error_code: Some(error_code), + block_hash: None, + height: None, + } + } + + #[allow(dead_code)] + pub fn with_block(block_hash: BlockHash, height: usize) -> Self { + Self { + error_code: None, + block_hash: Some(block_hash), + height: Some(height), } } } @@ -581,10 +573,15 @@ impl BitcoindMock { Err(JsonRpcError::new(JsonRpcErrorCode::ServerError(error))) }); io.add_alias("sendrawtransaction", "error"); - io.add_alias("getrawtransaction", "error"); } else { BitcoindMock::add_sendrawtransaction(&mut io); - BitcoindMock::add_getrawtransaction(&mut io, options.in_mempool); + } + + if let Some(block_hash) = options.block_hash { + BitcoindMock::add_getrawtransaction(&mut io, block_hash.to_string()); + if let Some(height) = options.height { + BitcoindMock::add_getblockheader(&mut io, block_hash.to_string(), height); + } } let server = ServerBuilder::new(io) @@ -594,7 +591,6 @@ impl BitcoindMock { Self { url: format!("http://{}", server.address()), - stopper: BitcoindStopper::new(server.close_handle()), server, } } @@ -605,25 +601,41 @@ impl BitcoindMock { }); } - fn add_getrawtransaction(io: &mut IoHandler, in_mempool: bool) { + fn add_getrawtransaction(io: &mut IoHandler, block_hash: String) { io.add_sync_method("getrawtransaction", move |_params: Params| { - if !in_mempool { - Err(JsonRpcError::new(JsonRpcErrorCode::ServerError(rpc_errors::RPC_INVALID_ADDRESS_OR_KEY as i64))) - } else { - match _params { - Params::Array(x) => match x[1] { - Value::Bool(x) => { - if x { - Ok(serde_json::json!({"hex": TX_HEX, "txid": TXID_HEX, "hash": TXID_HEX, "size": 0, - "vsize": 0, "version": 1, "locktime": 0, "vin": [], "vout": [] })) - } else { - Ok(Value::String(TX_HEX.to_owned())) - } + match _params { + Params::Array(x) => match x[1] { + Value::Bool(x) => { + if x { + Ok(serde_json::json!({"hex": TX_HEX, "txid": TXID_HEX, "hash": TXID_HEX, "size": 0, + "vsize": 0, "version": 1, "locktime": 0, "vin": [], "vout": [], "blockhash": block_hash })) + } else { + Ok(Value::String(TX_HEX.to_owned())) } - _ => panic!("Boolean param not found"), - }, - _ => panic!("No params found"), - } + } + _ => panic!("Boolean param not found"), + }, + _ => panic!("No params found"), + } + }) + } + + fn add_getblockheader(io: &mut IoHandler, block_hash: String, height: usize) { + io.add_sync_method("getblockheader", move |_params: Params| { + match _params { + Params::Array(x) => match x[1] { + Value::Bool(x) => { + if x { + Ok(serde_json::json!({"hash": block_hash, "confirmations": 1, "height": height, "version": 1, + "merkleroot": "4eca41cf0fa551346842eb317564a403e39553444790a65f949f95bc18d24643", "time": 1645719068, "nonce": 2, "bits": "207fffff", + "difficulty": 0.0, "chainwork": "0000000000000000000000000000000000000000000000000000000000001146", "nTx": 1})) + } else { + Ok(Value::String(TX_HEX.to_owned())) + } + } + _ => panic!("Boolean param not found"), + }, + _ => panic!("No params found"), } }) } @@ -633,8 +645,8 @@ impl BitcoindMock { } } -pub(crate) fn start_server(server: Server) { +pub(crate) fn start_server(bitcoind: BitcoindMock) { thread::spawn(move || { - server.wait(); + bitcoind.server.wait(); }); } diff --git a/teos/src/tls.rs b/teos/src/tls.rs deleted file mode 100644 index 33e3354..0000000 --- a/teos/src/tls.rs +++ /dev/null @@ -1,105 +0,0 @@ -/* The following code for generating mTLS certificates is adapted from: - * https://github.com/ElementsProject/lightning/blob/master/plugins/grpc-plugin/src/tls.rs - * - * This file is licensed under the BSD-MIT license, as described here: - * https://github.com/ElementsProject/lightning/blob/master/LICENSE -*/ - -use rcgen::{Certificate, Error as RcgenError, KeyPair}; -use std::convert::TryFrom; -use std::path::Path; - -/// Packs the reasons why generating mtls certificates may fail. -#[derive(Debug)] -pub enum GenCertificateFailure { - RcgenError(RcgenError), - IoError(std::io::Error), -} - -impl From for GenCertificateFailure { - fn from(e: RcgenError) -> Self { - GenCertificateFailure::RcgenError(e) - } -} - -impl From for GenCertificateFailure { - fn from(e: std::io::Error) -> Self { - GenCertificateFailure::IoError(e) - } -} - -/// Just a wrapper around a certificate and an associated keypair. -#[derive(Clone, Debug)] -struct Identity { - pub key: Vec, - pub certificate: Vec, -} - -impl TryFrom<&Identity> for (Certificate, KeyPair) { - type Error = RcgenError; - - 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)) - } -} - -pub fn tls_init( - directory: &Path, -) -> Result<(tonic::transport::Identity, Vec), GenCertificateFailure> { - let ca = generate_or_load_identity("teos Root CA", directory, "ca", None)?; - let server = generate_or_load_identity("teos grpc Server", directory, "server", Some(&ca))?; - let _client = generate_or_load_identity("teos grpc Client", directory, "client", Some(&ca))?; - let server_id = tonic::transport::Identity::from_pem(&server.certificate, &server.key); - - Ok((server_id, ca.certificate)) -} - -/// Generate a given identity -fn generate_or_load_identity( - name: &str, - directory: &Path, - filename: &str, - parent: Option<&Identity>, -) -> Result { - // Just our naming convention here. - let cert_path = directory.join(format!("{filename}.pem")); - let key_path = directory.join(format!("{filename}-key.pem")); - // 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()?; - 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!["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::NoCa; - } - params - .distinguished_name - .push(rcgen::DnType::CommonName, name); - - std::fs::write( - &cert_path, - match parent { - 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() - } - }, - )?; - } - - let key = std::fs::read(&key_path)?; - let certificate = std::fs::read(cert_path)?; - Ok(Identity { certificate, key }) -} diff --git a/teos/src/tx_index.rs b/teos/src/tx_index.rs deleted file mode 100644 index c0b4ee4..0000000 --- a/teos/src/tx_index.rs +++ /dev/null @@ -1,418 +0,0 @@ -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::{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 + Eq { - fn from_txid(txid: Txid) -> Self; -} - -impl Key for Txid { - fn from_txid(txid: Txid) -> Self { - txid - } -} - -impl Key for Locator { - fn from_txid(txid: Txid) -> Self { - Locator::new(txid) - } -} - -pub enum Type { - Transaction, - BlockHash, -} - -pub enum Data { - Transaction(Transaction), - BlockHash(BlockHash), -} - -impl fmt::Display for Data { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Data::Transaction(_) => write!(f, "Transaction"), - Data::BlockHash(_) => write!(f, "BlockHash"), - } - } -} - -/// A trait implemented by types that can be used as value in a [TxIndex]. -pub trait Value { - fn get_type() -> Type; - fn from_data(d: Data) -> Self; -} - -impl Value for BlockHash { - fn get_type() -> Type { - Type::BlockHash - } - - fn from_data(d: Data) -> Self { - match d { - Data::BlockHash(b) => b, - other => panic!("Cannot build a BlockHash from {}", other), - } - } -} - -impl Value for Transaction { - fn get_type() -> Type { - Type::Transaction - } - - fn from_data(d: Data) -> Self { - match d { - Data::Transaction(t) => t, - other => panic!("Cannot build a BlockHash from {}", other), - } - } -} - -/// Data structure used to index locators computed from parsed blocks. -/// -/// Holds up to `size` blocks with their corresponding computed [Locator]s. -#[derive(Debug, PartialEq, Eq)] -pub struct TxIndex { - /// A [K]:[V] map. - index: HashMap, - /// Vector of block hashes covered by the index. - blocks: VecDeque, - /// Map of [BlockHash]:[Vec]. Used to remove data from the index. - tx_in_block: HashMap>, - /// The height of the last block included in the index. - tip: u32, - /// Maximum size of the index. - size: usize, -} - -impl TxIndex -where - K: Key + Copy, - V: Value + Clone, - Self: Sized, -{ - pub fn new(last_n_blocks: &[ValidatedBlock], height: u32) -> Self { - let size = last_n_blocks.len(); - let mut tx_index = Self { - index: HashMap::new(), - blocks: VecDeque::with_capacity(size), - tx_in_block: HashMap::new(), - tip: height, - size, - }; - - for block in last_n_blocks.iter().rev() { - 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.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 - } - - /// Gets an item from the index if present. [None] otherwise. - pub fn get<'a>(&'a self, k: &'a K) -> Option<&V> { - self.index.get(k) - } - - /// Checks if the index if full. - pub fn is_full(&self) -> bool { - self.blocks.len() > self.size - } - - /// Get's the height of a given block based on its position in the block queue. - pub fn get_height(&self, block_hash: &BlockHash) -> Option { - let pos = self.blocks.iter().position(|x| x == block_hash)?; - Some(self.tip as usize + pos + 1 - self.blocks.len()) - } - - /// Updates the index by adding data from a new block. Removes the oldest block if the index is full afterwards. - pub fn update(&mut self, block_header: Header, data: &HashMap) { - self.blocks.push_back(block_header.block_hash()); - - let ks = data - .iter() - .map(|(k, v)| { - self.index.insert(*k, v.clone()); - *k - }) - .collect(); - - self.tx_in_block.insert(block_header.block_hash(), ks); - - if self.is_full() { - // Avoid logging during bootstrap - log::debug!("New block added to index: {}", block_header.block_hash()); - self.tip += 1; - self.remove_oldest_block(); - } - } - - /// Fixes the index by removing disconnected data. - pub fn remove_disconnected_block(&mut self, block_hash: &BlockHash) { - if let Some(ks) = self.tx_in_block.remove(block_hash) { - self.index.retain(|k, _| !ks.contains(k)); - - // Blocks should be disconnected from last backwards. Log if that's not the case so we can revisit this and fix it. - if let Some(ref h) = self.blocks.pop_back() { - if h != block_hash { - log::error!("Disconnected block does not match the oldest block stored in the TxIndex ({block_hash} != {h})"); - } - } - } else { - log::warn!("The index is already empty"); - } - } - - /// Removes the oldest block from the index. - /// This removes data from `self.blocks`, `self.tx_in_block` and `self.index`. - pub fn remove_oldest_block(&mut self) { - let h = self.blocks.pop_front().unwrap(); - let ks = self.tx_in_block.remove(&h).unwrap(); - self.index.retain(|k, _| !ks.contains(k)); - - log::debug!("Oldest block removed from index: {h}"); - } -} - -impl fmt::Display for TxIndex { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "index: {:?}\n\nblocks: {:?}\n\ntx_in_block: {:?}\n\nsize: {}", - self.index, self.blocks, self.tx_in_block, self.size - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::ops::Deref; - - use crate::test_utils::{get_full_block, get_full_blocks, get_last_n_blocks, Blockchain}; - - use bitcoin::hashes::serde_macros::serde_details::SerdeHash; - use bitcoin::Block; - - impl TxIndex - where - K: Key + std::cmp::Eq + Copy, - V: Value + Clone, - Self: Sized, - { - pub fn index_mut(&mut self) -> &mut HashMap { - &mut self.index - } - - pub fn blocks(&self) -> &VecDeque { - &self.blocks - } - - pub fn contains_key(&self, k: &K) -> bool { - self.index.contains_key(k) - } - } - - #[tokio::test] - async fn test_new() { - let height = 10; - let mut chain = Blockchain::default().with_height(height as usize); - let last_six_blocks = get_last_n_blocks(&mut chain, 6).await; - let blocks: Vec = get_full_blocks(&last_six_blocks); - - let cache: TxIndex = TxIndex::new(&last_six_blocks, height); - assert_eq!(blocks.len(), cache.size); - for block in blocks.iter() { - assert!(cache.blocks().contains(&block.block_hash())); - - let mut locators = Vec::new(); - for tx in block.txdata.iter() { - let locator = Locator::new(tx.compute_txid()); - assert!(cache.contains_key(&locator)); - locators.push(locator); - } - - assert_eq!(cache.tx_in_block[&block.block_hash()], locators); - } - } - - #[tokio::test] - async fn test_get_height() { - let cache_size = 10; - let height = 50; - let mut chain = Blockchain::default().with_height_and_txs(height, 42); - let last_n_blocks = get_last_n_blocks(&mut chain, cache_size).await; - - // last_n_blocks is ordered from latest to earliest - let first_block = 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.header.block_hash()).unwrap(), - height - cache_size + 1 - ); - assert_eq!( - 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 - ); - } - - #[tokio::test] - async fn test_get_height_not_found() { - let cache_size = 10; - let height = 50; - let mut chain = Blockchain::default().with_height_and_txs(height, 42); - let cache: TxIndex = TxIndex::new( - &get_last_n_blocks(&mut chain, cache_size).await, - height as u32, - ); - - let fake_hash = &BlockHash::from_slice_delegated(&[0; 32]).unwrap(); - assert!(cache.get_height(fake_hash).is_none()); - } - - #[tokio::test] - async fn test_update() { - let height = 10; - let mut chain = Blockchain::default().with_height(height as usize); - let mut last_n_blocks = get_last_n_blocks(&mut chain, 7).await; - - // Store the last block to use it for an update and the first to check eviction - // Notice that the list of blocks is ordered from last to first. - let last_block = last_n_blocks.remove(0); - let first_block = last_n_blocks.last().unwrap(); - - // 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 full_block = get_full_block(&last_block); - let locator_tx_map = full_block - .txdata - .iter() - .map(|tx| (Locator::new(tx.compute_txid()), tx.clone())) - .collect(); - - let header = full_block.header; - cache.update(header, &locator_tx_map); - - // Check that the new data is in the cache - 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[&block_hash], - locator_tx_map.keys().cloned().collect::>() - ); - - // Check that the data from the first block has been evicted - 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] - async fn test_remove_disconnected_block() { - let cache_size = 6; - let height = cache_size * 2; - let mut chain = Blockchain::default().with_height_and_txs(height, 42); - let mut cache: TxIndex = TxIndex::new( - &get_last_n_blocks(&mut chain, cache_size).await, - height as u32, - ); - - // TxIndex::fix removes the last connected block and removes all the associated data - for i in 0..cache_size { - let header = chain - .at_height(chain.get_block_count() as usize - i) - .deref() - .header; - let locators = cache.tx_in_block.get(&header.block_hash()).unwrap().clone(); - - // Make sure there's data regarding the target block in the cache before fixing it - assert_eq!(cache.blocks().len(), cache.size - i); - assert!(cache.blocks().contains(&header.block_hash())); - assert!(!locators.is_empty()); - for locator in locators.iter() { - assert!(cache.contains_key(locator)); - } - - cache.remove_disconnected_block(&header.block_hash()); - - // Check that the block data is not in the cache anymore - assert_eq!(cache.blocks().len(), cache.size - i - 1); - assert!(!cache.blocks().contains(&header.block_hash())); - assert!(!cache.tx_in_block.contains_key(&header.block_hash())); - for locator in locators.iter() { - assert!(!cache.contains_key(locator)); - } - } - - // At this point the cache should be empty, fixing it further shouldn't do anything - for i in cache_size..cache_size * 2 { - assert!(cache.index.is_empty()); - assert!(cache.blocks().is_empty()); - assert!(cache.tx_in_block.is_empty()); - - let header = chain - .at_height(chain.get_block_count() as usize - i) - .deref() - .header; - cache.remove_disconnected_block(&header.block_hash()); - } - } -} diff --git a/teos/src/watcher.rs b/teos/src/watcher.rs index caf7766..5af641e 100644 --- a/teos/src/watcher.rs +++ b/teos/src/watcher.rs @@ -1,25 +1,156 @@ //! Logic related to the Watcher, the components in charge of watching for breaches on chain. -use std::collections::HashMap; +use log; + +use std::collections::hash_map::Entry; +use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::iter::FromIterator; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{Arc, Mutex}; -use bitcoin::block::Header; +use bitcoin::hash_types::BlockHash; use bitcoin::secp256k1::SecretKey; -use bitcoin::Transaction; +use bitcoin::{Block, BlockHeader, Transaction}; use lightning::chain; use lightning_block_sync::poll::ValidatedBlock; use teos_common::appointment::{Appointment, Locator}; use teos_common::cryptography; use teos_common::receipts::{AppointmentReceipt, RegistrationReceipt}; -use teos_common::{TowerId, UserId}; +use teos_common::UserId; use crate::dbm::DBM; -use crate::extended_appointment::{ExtendedAppointment, UUID}; +use crate::extended_appointment::{AppointmentSummary, ExtendedAppointment, UUID}; use crate::gatekeeper::{Gatekeeper, MaxSlotsReached, UserInfo}; use crate::responder::{ConfirmationStatus, Responder, TransactionTracker}; -use crate::tx_index::TxIndex; + +/// Data structure used to cache locators computed from parsed blocks. +/// +/// Holds up to `size` blocks with their corresponding computed [Locator]s. +#[derive(Debug)] +struct LocatorCache { + /// A [Locator]:[Transaction] map. + cache: HashMap, + /// Vector of block hashes corresponding to the cached blocks. + blocks: Vec, + /// Map of [BlockHash]:[Vec]. Used to remove data from the cache. + tx_in_block: HashMap>, + /// Maximum size of the cache. + size: usize, +} + +impl fmt::Display for LocatorCache { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "cache: {:?}\n\nblocks: {:?}\n\ntx_in_block: {:?}\n\nsize: {}", + self.cache, self.blocks, self.tx_in_block, self.size + ) + } +} + +impl LocatorCache { + /// Creates a new [LocatorCache] instance. + /// The cache is initialized using the provided vector of blocks. + /// The size of the cache is defined as the size of `last_n_blocks`. + /// + /// # Panics + /// + /// Panics if any of the blocks in `last_n_blocks` is unchained. That is, if the given blocks + /// are not linked in strict descending order. + fn new(last_n_blocks: Vec) -> LocatorCache { + let size = last_n_blocks.len(); + let mut cache = LocatorCache { + cache: HashMap::new(), + blocks: Vec::with_capacity(size), + tx_in_block: HashMap::new(), + size, + }; + + for block in last_n_blocks.into_iter().rev() { + if let Some(prev_block_hash) = cache.blocks.last() { + if block.header.prev_blockhash != *prev_block_hash { + panic!("last_n_blocks contains unchained blocks"); + } + }; + + let locator_tx_map = block + .txdata + .iter() + .map(|tx| (Locator::new(tx.txid()), tx.clone())) + .collect(); + + cache.update(block.header, &locator_tx_map); + } + + cache + } + + /// Gets a transaction from the cache if present. [None] otherwise. + fn get_tx(&self, locator: Locator) -> Option<&Transaction> { + self.cache.get(&locator) + } + + /// Checks if the cache if full. + fn is_full(&self) -> bool { + self.blocks.len() > self.size + } + + /// Updates the cache by adding data from a new block. Removes the oldest block if the cache is full afterwards. + fn update( + &mut self, + block_header: BlockHeader, + locator_tx_map: &HashMap, + ) { + self.blocks.push(block_header.block_hash()); + + let locators = locator_tx_map + .iter() + .map(|(l, tx)| { + self.cache.insert(*l, tx.clone()); + *l + }) + .collect(); + + self.tx_in_block.insert(block_header.block_hash(), locators); + + if self.is_full() { + // Avoid logging during bootstrap + log::info!("New block added to cache: {}", block_header.block_hash()); + self.remove_oldest_block(); + } + } + + /// Fixes the [LocatorCache] removing disconnected data. + fn fix(&mut self, header: &BlockHeader) { + if let Some(locators) = self.tx_in_block.remove(&header.block_hash()) { + for locator in locators.iter() { + self.cache.remove(locator); + } + + // Blocks should be disconnected from last backwards. Log if that's not the case so we can revisit this and fix it. + if let Some(h) = self.blocks.pop() { + if h != header.block_hash() { + log::error!("Disconnected block does not match the oldest block stored in the LocatorCache ({} != {})", header.block_hash(), h); + } + } + } else { + log::warn!("The cache is already empty"); + } + } + + /// Removes the oldest block from the cache. + /// This removes data from `self.blocks`, `self.tx_in_block` and `self.cache`. + fn remove_oldest_block(&mut self) { + let oldest_hash = self.blocks.remove(0); + for locator in self.tx_in_block.remove(&oldest_hash).unwrap() { + self.cache.remove(&locator); + } + + log::info!("Oldest block removed from cache: {}", oldest_hash); + } +} /// Structure holding data regarding a breach. /// @@ -79,11 +210,19 @@ 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]. @@ -97,8 +236,12 @@ 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>, + locator_cache: Mutex, /// A [Responder] instance. Data will be passed to it once triggered (if valid). responder: Arc, /// A [Gatekeeper] instance. Data regarding users is requested to it. @@ -108,7 +251,7 @@ pub struct Watcher { /// The tower signing key. Used to sign messages going to users. signing_key: SecretKey, /// The tower identifier. - pub tower_id: TowerId, + pub tower_id: UserId, /// A [DBM] (database manager) instance. Used to persist appointment data into disk. dbm: Arc>, } @@ -118,14 +261,28 @@ impl Watcher { pub fn new( gatekeeper: Arc, responder: Arc, - last_n_blocks: &[ValidatedBlock], + last_n_blocks: Vec, last_known_block_height: u32, signing_key: SecretKey, - tower_id: TowerId, + tower_id: UserId, dbm: Arc>, ) -> Self { + let mut appointments = HashMap::new(); + let mut locator_uuid_map: HashMap> = HashMap::new(); + for (uuid, appointment) in dbm.lock().unwrap().load_all_appointments() { + 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 { - locator_cache: Mutex::new(TxIndex::new(last_n_blocks, last_known_block_height)), + appointments: Mutex::new(appointments), + locator_uuid_map: Mutex::new(locator_uuid_map), + locator_cache: Mutex::new(LocatorCache::new(last_n_blocks)), responder, gatekeeper, last_known_block_height: AtomicU32::new(last_known_block_height), @@ -137,7 +294,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.get_appointments_count() == 0 + self.appointments.lock().unwrap().is_empty() } /// Registers a new user within the [Watcher]. This request is passed to the [Gatekeeper], who is in @@ -157,7 +314,8 @@ 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 [ExtendedAppointment] (constructed from the [Appointment]) will be persisted on disk. + /// 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. /// 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( @@ -167,7 +325,7 @@ impl Watcher { ) -> Result<(AppointmentReceipt, u32, u32), AddAppointmentFailure> { let user_id = self .gatekeeper - .authenticate_user(&appointment.to_vec(), &user_signature) + .authenticate_user(&appointment.serialize(), &user_signature) .map_err(|_| AddAppointmentFailure::AuthenticationFailure)?; let (has_subscription_expired, expiry) = @@ -184,15 +342,13 @@ impl Watcher { self.last_known_block_height.load(Ordering::Acquire), ); - let uuid = extended_appointment.uuid(); + let uuid = UUID::new(extended_appointment.locator(), user_id); if self.responder.has_tracker(uuid) { - log::info!("Tracker for {uuid} already found in Responder"); + log::info!("Tracker for {} already found in Responder", uuid); 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) @@ -206,7 +362,7 @@ impl Watcher { .locator_cache .lock() .unwrap() - .get(&extended_appointment.locator()) + .get_tx(extended_appointment.locator()) { // Appointments that were triggered in blocks held in the cache Some(dispute_tx) => { @@ -227,23 +383,55 @@ impl Watcher { Ok((receipt, available_slots, expiry)) } - /// Stores an appointment in the database (or updates it if it already exists). + /// 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`. fn store_appointment( &self, uuid: UUID, appointment: &ExtendedAppointment, ) -> StoredAppointment { - 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 { - dbm.store_appointment(uuid, appointment).unwrap(); + 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 + } 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 {}: {}", + appointment.locator(), + uuid + ); + self.dbm + .lock() + .unwrap() + .store_appointment(uuid, appointment) + .unwrap(); + StoredAppointment::Collision + } else { + log::debug!("Update received for {}, locator map not modified", uuid); + self.dbm + .lock() + .unwrap() + .update_appointment(uuid, appointment); + StoredAppointment::Update + } } } @@ -262,7 +450,7 @@ impl Watcher { "Trigger for locator {} found in cache", appointment.locator() ); - match cryptography::decrypt(appointment.encrypted_blob(), &dispute_tx.compute_txid()) { + match cryptography::decrypt(appointment.encrypted_blob(), &dispute_tx.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. @@ -270,9 +458,6 @@ 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( @@ -280,8 +465,11 @@ impl Watcher { Breach::new(dispute_tx.clone(), penalty_tx), user_id, ) { - log::warn!("Appointment bounced in the Responder. Reason: {reason:?}"); - self.gatekeeper.delete_appointments(vec![uuid], false); + // 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); TriggeredAppointment::Rejected } else { log::info!("Appointment went straight to the Responder"); @@ -315,7 +503,7 @@ impl Watcher { locator: Locator, user_signature: &str, ) -> Result { - let message = format!("get appointment {locator}"); + let message = format!("get appointment {}", locator); let user_id = self .gatekeeper @@ -330,17 +518,25 @@ impl Watcher { } let uuid = UUID::new(locator, user_id); - 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 - }) + + 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({ + log::info!("Cannot find {}", locator); + GetAppointmentFailure::NotFound + }) + } } /// Gets a map of breaches provided a map between locators and transactions. @@ -352,14 +548,20 @@ impl Watcher { &self, locator_tx_map: HashMap, ) -> HashMap { - let breaches: HashMap = self - .dbm + let monitored_locators: HashSet = self + .locator_uuid_map .lock() .unwrap() - .batch_check_locators_exist(locator_tx_map.keys().collect()) - .iter() - .map(|locator| (*locator, locator_tx_map[locator].clone())) + .keys() + .cloned() .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") @@ -370,41 +572,117 @@ impl Watcher { breaches } - /// Responds to breaches. + /// Filters a map of breaches between those that are valid and those that are not. /// - /// 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(); + /// 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(); + // 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() { - // 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, - ) { - invalid_breaches.push(uuid); - } + 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())); } - Err(_) => { - invalid_breaches.push(uuid); + None => { + match cryptography::decrypt( + appointment.encrypted_blob(), + &dispute_tx.txid(), + ) { + 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.is_empty()).then_some(invalid_breaches) + (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 {} without breach. Deleting appointment", + uuid + ), + DeletionReason::Invalid => log::info!( + "{} cannot be completed, it contains invalid data. Deleting appointment", + uuid + ), + DeletionReason::Accepted => { + log::info!("{} accepted by the Responder. Deleting appointment", uuid) + } + }; + 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); + } + } + 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, + ) { + self.delete_appointments_from_memory(uuids, reason); + self.dbm + .lock() + .unwrap() + .batch_remove_appointments(uuids, updated_users); } /// Ges the number of users currently registered with the tower. @@ -412,9 +690,9 @@ impl Watcher { self.gatekeeper.get_registered_users_count() } - /// Gets the total number of appointments excluding trackers. + /// Gets the total number of appointments stored in the [Watcher]. pub(crate) fn get_appointments_count(&self) -> usize { - self.dbm.lock().unwrap().get_appointments_count() + self.appointments.lock().unwrap().len() } /// Gets the total number of trackers in the [Responder]. @@ -424,28 +702,12 @@ impl Watcher { /// Gets all the appointments stored in the [Watcher] (from the database). pub(crate) fn get_all_watcher_appointments(&self) -> HashMap { - self.dbm.lock().unwrap().load_appointments(None) - } - - /// Gets all the appointments matching a specific locator from the [Watcher] (from the database). - pub(crate) fn get_watcher_appointments_with_locator( - &self, - locator: Locator, - ) -> HashMap { - self.dbm.lock().unwrap().load_appointments(Some(locator)) + self.dbm.lock().unwrap().load_all_appointments() } /// Gets all the trackers stored in the [Responder] (from the database). pub(crate) fn get_all_responder_trackers(&self) -> HashMap { - self.dbm.lock().unwrap().load_trackers(None) - } - - /// Gets all the trackers matching s specific locator from the [Responder] (from the database). - pub(crate) fn get_responder_trackers_with_locator( - &self, - locator: Locator, - ) -> HashMap { - self.dbm.lock().unwrap().load_trackers(Some(locator)) + self.dbm.lock().unwrap().load_all_trackers() } /// Gets the list of all registered user ids. @@ -454,7 +716,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<(UserInfo, Vec)> { + pub(crate) fn get_user_info(&self, user_id: UserId) -> Option { self.gatekeeper.get_user_info(user_id) } @@ -477,7 +739,30 @@ impl Watcher { return Err(GetSubscriptionInfoFailure::SubscriptionExpired(expiry)); } - let (subscription_info, locators) = self.gatekeeper.get_user_info(user_id).unwrap(); + 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) + } + } + } + } + Ok((subscription_info, locators)) } } @@ -495,27 +780,71 @@ impl chain::Listen for Watcher { /// /// This also takes care of updating the [LocatorCache] and removing outdated data from the [Watcher] when /// told by the [Gatekeeper]. - fn filtered_block_connected( - &self, - header: &Header, - txdata: &chain::transaction::TransactionData, - height: u32, - ) { - log::info!("New block received: {}", header.block_hash()); + fn block_connected(&self, block: &Block, height: u32) { + log::info!("New block received: {}", block.header.block_hash()); - let locator_tx_map = txdata + let locator_tx_map = block + .txdata .iter() - .map(|(_, tx)| (Locator::new(tx.compute_txid()), (*tx).clone())) + .map(|tx| (Locator::new(tx.txid()), tx.clone())) .collect(); self.locator_cache .lock() .unwrap() - .update(*header, &locator_tx_map); + .update(block.header, &locator_tx_map); - // 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); + 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"); + } } // Update last known block @@ -526,12 +855,9 @@ 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: &Header, height: u32) { + fn block_disconnected(&self, header: &BlockHeader, height: u32) { log::warn!("Block disconnected: {}", header.block_hash()); - self.locator_cache - .lock() - .unwrap() - .remove_disconnected_block(&header.block_hash()); + self.locator_cache.lock().unwrap().fix(header); self.last_known_block_height .store(height - 1, Ordering::Release); } @@ -540,58 +866,50 @@ 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}; - use crate::dbm::DBM; + use crate::dbm::{Error as DBError, DBM}; use crate::responder::ConfirmationStatus; use crate::rpc_errors; use crate::test_utils::{ create_carrier, create_responder, create_watcher, generate_dummy_appointment, - generate_dummy_appointment_with_user, get_random_tx, BitcoindMock, BitcoindStopper, - Blockchain, MockOptions, MockedServerQuery, DURATION, EXPIRY_DELTA, SLOTS, START_HEIGHT, + generate_dummy_appointment_with_user, generate_uuid, get_last_n_blocks, get_random_breach, + get_random_tx, store_appointment_and_fks_to_db, BitcoindMock, Blockchain, MockOptions, + MockedServerQuery, DURATION, EXPIRY_DELTA, SLOTS, START_HEIGHT, }; - use teos_common::cryptography::get_random_keypair; + use teos_common::cryptography::{get_random_bytes, 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 { - // 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() + *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) } } impl Eq for Watcher {} impl Watcher { - 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) -> TransactionTracker { + pub(crate) fn add_random_tracker_to_responder(&self, uuid: UUID) { // The confirmation status can be whatever here. Using the most common. self.responder - .add_random_tracker(ConfirmationStatus::ConfirmedIn(100)) + .add_random_tracker(uuid, ConfirmationStatus::ConfirmedIn(100)); } } - async fn init_watcher(chain: &mut Blockchain) -> (Watcher, BitcoindStopper) { + async fn init_watcher(chain: &mut Blockchain) -> Watcher { let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); init_watcher_with_db(chain, dbm).await } - async fn init_watcher_with_db( - chain: &mut Blockchain, - dbm: Arc>, - ) -> (Watcher, BitcoindStopper) { - let bitcoind_mock = BitcoindMock::new(MockOptions::default()); + async fn init_watcher_with_db(chain: &mut Blockchain, dbm: Arc>) -> Watcher { + let bitcoind_mock = BitcoindMock::new(MockOptions::empty()); let gk = Arc::new(Gatekeeper::new( chain.get_block_count(), @@ -600,7 +918,7 @@ mod tests { EXPIRY_DELTA, dbm.clone(), )); - let responder = create_responder(chain, gk.clone(), dbm.clone(), bitcoind_mock.url()).await; + let responder = create_responder(chain.tip(), gk.clone(), dbm.clone(), bitcoind_mock.url()); create_watcher( chain, Arc::new(responder), @@ -617,15 +935,131 @@ mod tests { expiry: u32, receipt: AppointmentReceipt, expected_user_signature: &str, - tower_id: TowerId, + tower_id: UserId, ) { assert_eq!(slots, expected_slots); assert_eq!(expiry, START_HEIGHT as u32 + DURATION); assert_eq!(receipt.start_block(), START_HEIGHT as u32); assert_eq!(receipt.user_signature(), expected_user_signature); let recovered_pk = - cryptography::recover_pk(&receipt.to_vec(), &receipt.signature().unwrap()).unwrap(); - assert_eq!(TowerId(recovered_pk), tower_id); + cryptography::recover_pk(&receipt.serialize(), &receipt.signature().unwrap()).unwrap(); + assert_eq!(UserId(recovered_pk), tower_id); + } + + #[tokio::test] + async fn test_cache_new() { + let mut chain = Blockchain::default().with_height(10); + let last_six_blocks = get_last_n_blocks(&mut chain, 6).await; + let blocks: Vec = last_six_blocks + .iter() + .map(|block| block.deref().clone()) + .collect(); + + let cache = LocatorCache::new(last_six_blocks); + assert_eq!(blocks.len(), cache.size); + for block in blocks.iter() { + assert!(cache.blocks.contains(&block.block_hash())); + + let mut locators = Vec::new(); + for tx in block.txdata.iter() { + let locator = Locator::new(tx.txid()); + assert!(cache.cache.contains_key(&locator)); + locators.push(locator); + } + + assert_eq!(cache.tx_in_block[&block.block_hash()], locators); + } + } + + #[tokio::test] + async fn test_cache_update() { + let mut chain = Blockchain::default().with_height(10); + let mut last_n_blocks = get_last_n_blocks(&mut chain, 7).await; + + // Safe the last block to use it for an update and the first to check eviction + // Notice that the list of blocks is ordered from last to first. + let last_block = last_n_blocks.remove(0); + let first_block = last_n_blocks.last().unwrap().deref().clone(); + + // Init the cache with the 6 block before the last + let mut cache = LocatorCache::new(last_n_blocks); + + // Update the cache with the last block + let locator_tx_map = last_block + .txdata + .iter() + .map(|tx| (Locator::new(tx.txid()), tx.clone())) + .collect(); + + cache.update(last_block.deref().header, &locator_tx_map); + + // Check that the new data is in the cache + assert!(cache.blocks.contains(&last_block.block_hash())); + for (locator, _) in locator_tx_map.iter() { + assert!(cache.cache.contains_key(locator)); + } + assert_eq!( + cache.tx_in_block[&last_block.block_hash()], + locator_tx_map.keys().cloned().collect::>() + ); + + // Check that the data from the first block has been evicted + assert!(!cache.blocks.contains(&first_block.block_hash())); + for tx in first_block.txdata.iter() { + assert!(!cache.cache.contains_key(&Locator::new(tx.txid()))); + } + assert!(!cache.tx_in_block.contains_key(&first_block.block_hash())); + } + + #[tokio::test] + async fn test_cache_fix() { + let cache_size = 6; + let mut chain = Blockchain::default().with_height_and_txs(cache_size * 2, 42); + + let last_n_blocks = get_last_n_blocks(&mut chain, cache_size).await; + + // Init the cache with the 6 block before the last + let mut cache = LocatorCache::new(last_n_blocks); + + // LocatorCache::fix removes the last connected block and removes all the associated data + for i in 0..cache_size { + let header = chain + .at_height(chain.get_block_count() as usize - i) + .deref() + .header; + let locators = cache.tx_in_block.get(&header.block_hash()).unwrap().clone(); + + // Make sure there's data regarding the target block in the cache before fixing it + assert_eq!(cache.blocks.len(), cache.size - i); + assert!(cache.blocks.contains(&header.block_hash())); + assert!(!locators.is_empty()); + for locator in locators.iter() { + assert!(cache.cache.contains_key(locator)); + } + + cache.fix(&header); + + // Check that the block data is not in the cache anymore + assert_eq!(cache.blocks.len(), cache.size - i - 1); + assert!(!cache.blocks.contains(&header.block_hash())); + assert!(cache.tx_in_block.get(&header.block_hash()).is_none()); + for locator in locators.iter() { + assert!(!cache.cache.contains_key(locator)); + } + } + + // At this point the cache should be empty, fixing it further shouldn't do anything + for i in cache_size..cache_size * 2 { + assert!(cache.cache.is_empty()); + assert!(cache.blocks.is_empty()); + assert!(cache.tx_in_block.is_empty()); + + let header = chain + .at_height(chain.get_block_count() as usize - i) + .deref() + .header; + cache.fix(&header); + } } #[tokio::test] @@ -633,25 +1067,25 @@ mod tests { // A fresh watcher has no associated data let mut chain = Blockchain::default().with_height(START_HEIGHT); let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); - let (watcher, _s) = init_watcher_with_db(&mut chain, dbm.clone()).await; + let watcher = init_watcher_with_db(&mut chain, dbm.clone()).await; assert!(watcher.is_fresh()); 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 appointments to the system and create a new Watcher reusing the same db + // 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 _ in 0..10 { - let appointment = generate_dummy_appointment(None).inner; - let user_sig = cryptography::sign(&appointment.to_vec(), &user_sk); + let user_sig = cryptography::sign(&appointment.serialize(), &user_sk).unwrap(); watcher .add_appointment(appointment.clone(), user_sig.clone()) .unwrap(); } // Create a new Responder reusing the same DB and check that the data is loaded - let (another_w, _as) = init_watcher_with_db(&mut chain, dbm).await; + let another_w = init_watcher_with_db(&mut chain, dbm).await; assert!(!another_w.is_fresh()); assert_eq!(watcher, another_w); } @@ -662,7 +1096,7 @@ mod tests { // Not testing the update / rejection logic, since that's already covered in the Gatekeeper, just that the data makes // sense and the signature verifies. let mut chain = Blockchain::default().with_height(START_HEIGHT); - let (watcher, _s) = init_watcher(&mut chain).await; + let watcher = init_watcher(&mut chain).await; let tower_pk = watcher.tower_id.0; let (_, user_pk) = get_random_keypair(); @@ -677,7 +1111,7 @@ mod tests { ); assert!(cryptography::verify( - &receipt.to_vec(), + &receipt.serialize(), &receipt.signature().unwrap(), &tower_pk )); @@ -687,18 +1121,19 @@ mod tests { async fn test_add_appointment() { let mut chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 10); let tip_txs = chain.blocks.last().unwrap().txdata.clone(); - let (watcher, _s) = init_watcher(&mut chain).await; + let watcher = init_watcher(&mut chain).await; // add_appointment should add a given appointment to the Watcher given the following logic: // - if the appointment does not exist for a given user, add the appointment // - 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 - let tower_id = TowerId(PublicKey::from_secret_key( + let tower_id: UserId = UserId(PublicKey::from_secret_key( &Secp256k1::new(), &watcher.signing_key, )); @@ -706,10 +1141,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.serialize(), &user_sk).unwrap(); let (receipt, slots, expiry) = watcher .add_appointment(appointment.clone(), user_sig.clone()) .unwrap(); @@ -722,31 +1157,37 @@ mod tests { let user2_id = UserId(user2_pk); watcher.register(user2_id).unwrap(); - let user2_sig = cryptography::sign(&appointment.to_vec(), &user2_sk); + let user2_sig = cryptography::sign(&appointment.serialize(), &user2_sk).unwrap(); 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 - assert_eq!(watcher.get_appointments_count(), 2); - assert_eq!(watcher.responder.get_trackers_count(), 0); + // 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 { .. }) + )); + } // If an appointment is already in the Responder, it should bounce - 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 + let (uuid, triggered_appointment) = generate_dummy_appointment_with_user(user_id, None); + let signature = + cryptography::sign(&triggered_appointment.inner.serialize(), &user_sk).unwrap(); + watcher .add_appointment(triggered_appointment.inner.clone(), signature.clone()) .unwrap(); - 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()); + let breach = get_random_breach(); watcher.responder.add_tracker( uuid, breach, @@ -759,65 +1200,85 @@ 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.compute_txid())); - let user_sig = cryptography::sign(&appointment_in_cache.inner.to_vec(), &user_sk); + generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.txid())); + let user_sig = + cryptography::sign(&appointment_in_cache.inner.serialize(), &user_sk).unwrap(); let (receipt, slots, expiry) = watcher - .add_appointment(appointment_in_cache.inner, user_sig.clone()) + .add_appointment(appointment_in_cache.inner.clone(), user_sig.clone()) .unwrap(); - // The appointment should have been accepted, slots should have been decreased, and a new tracker should be found in the Responder + // 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 assert_appointment_added(slots, SLOTS - 3, expiry, receipt, &user_sig, tower_id); - assert_eq!(watcher.get_appointments_count(), 2); - assert_eq!(watcher.responder.get_trackers_count(), 2); - // Data should be in the database + assert_eq!(watcher.appointments.lock().unwrap().len(), 3); + assert!(!watcher + .locator_uuid_map + .lock() + .unwrap() + .contains_key(&appointment_in_cache.locator())); 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.compute_txid())); + generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.txid())); invalid_appointment.inner.encrypted_blob.reverse(); - let user_sig = cryptography::sign(&invalid_appointment.inner.to_vec(), &user_sk); + let user_sig = + cryptography::sign(&invalid_appointment.inner.serialize(), &user_sk).unwrap(); let (receipt, slots, expiry) = watcher - .add_appointment(invalid_appointment.inner, user_sig.clone()) + .add_appointment(invalid_appointment.inner.clone(), user_sig.clone()) .unwrap(); assert_appointment_added(slots, SLOTS - 4, expiry, receipt, &user_sig, tower_id); - assert_eq!(watcher.get_appointments_count(), 2); - assert_eq!(watcher.responder.get_trackers_count(), 2); + assert_eq!(watcher.appointments.lock().unwrap().len(), 3); + // Data should not be in the database - assert!(!watcher.responder.has_tracker(uuid)); - assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(matches!( + watcher.dbm.lock().unwrap().load_appointment(uuid), + Err(DBError::NotFound) + )); + assert!(matches!( + watcher.dbm.lock().unwrap().load_tracker(uuid), + Err(DBError::NotFound) + )); // Transaction rejected // Update the Responder with a new Carrier - let (carrier, _as) = create_carrier( + *watcher.responder.get_carrier().lock().unwrap() = create_carrier( MockedServerQuery::Error(rpc_errors::RPC_VERIFY_ERROR as i64), chain.tip().deref().height, ); - *watcher.responder.get_carrier().lock().unwrap() = carrier; let dispute_tx = &tip_txs[tip_txs.len() - 2]; - let (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 invalid_appointment = generate_dummy_appointment(Some(&dispute_tx.txid())).inner; + let user_sig = cryptography::sign(&invalid_appointment.serialize(), &user_sk).unwrap(); let (receipt, slots, expiry) = watcher - .add_appointment(invalid_appointment.inner, user_sig.clone()) + .add_appointment(invalid_appointment, user_sig.clone()) .unwrap(); - 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); + assert_appointment_added(slots, SLOTS - 4, expiry, receipt, &user_sig, tower_id); + assert_eq!(watcher.appointments.lock().unwrap().len(), 3); + // Data should not be in the database - assert!(!watcher.responder.has_tracker(uuid)); - assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(matches!( + watcher.dbm.lock().unwrap().load_appointment(uuid), + Err(DBError::NotFound) + )); // FAIL cases (non-registered, subscription expired and not enough slots) @@ -826,11 +1287,14 @@ mod tests { let user3_sig = String::from_utf8((0..65).collect()).unwrap(); assert!(matches!( - watcher.add_appointment(appointment, user3_sig), + watcher.add_appointment(appointment.clone(), user3_sig), Err(AddAppointmentFailure::AuthenticationFailure) )); // Data should not be in the database - assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(matches!( + watcher.dbm.lock().unwrap().load_appointment(uuid), + Err(DBError::NotFound) + )); // 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. @@ -843,45 +1307,52 @@ mod tests { .unwrap() .available_slots = 0; - let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); - let signature = cryptography::sign(&appointment.inner.to_vec(), &user_sk); + 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.serialize(), &user_sk).unwrap(); assert!(matches!( - watcher.add_appointment(appointment.inner, signature), + watcher.add_appointment(new_appointment, new_app_sig), Err(AddAppointmentFailure::NotEnoughSlots) )); // Data should not be in the database - assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(matches!( + watcher.dbm.lock().unwrap().load_appointment(uuid), + Err(DBError::NotFound) + )); // If the user subscription has expired, the appointment should be rejected. watcher .gatekeeper - .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); + .get_registered_users() + .lock() + .unwrap() + .get_mut(&user2_id) + .unwrap() + .subscription_expiry = START_HEIGHT as u32; assert!(matches!( - watcher.add_appointment(appointment.inner, signature), + watcher.add_appointment(appointment, user2_sig), Err(AddAppointmentFailure::SubscriptionExpired { .. }) )); // Data should not be in the database - assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(matches!( + watcher.dbm.lock().unwrap().load_appointment(uuid), + Err(DBError::NotFound) + )); } #[tokio::test] async fn test_store_appointment() { let mut chain = Blockchain::default().with_height(START_HEIGHT); - let (watcher, _s) = init_watcher(&mut chain).await; + let watcher = init_watcher(&mut chain).await; // Register the user 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, Some(&dispute_txid)); + let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None); // Storing a new appointment should return New assert_eq!( @@ -889,29 +1360,53 @@ mod tests { StoredAppointment::New, ); assert_eq!( - watcher.get_all_watcher_appointments(), - HashMap::from_iter([(uuid, appointment)]) + *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 an appointment with the same UUID should be seen as an updated - // 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); + // The appointment data here does not matter much, just the UUID and the locator since they are tied to each other. assert_eq!( watcher.store_appointment(uuid, &appointment), StoredAppointment::Update, ); assert_eq!( - watcher.get_all_watcher_appointments(), - HashMap::from_iter([(uuid, appointment)]) + *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]))]) ); } #[tokio::test] async fn test_store_triggered_appointment() { let mut chain = Blockchain::default().with_height(START_HEIGHT); - let (watcher, _s) = init_watcher(&mut chain).await; + let watcher = init_watcher(&mut chain).await; // Register the user let (_, user_pk) = get_random_keypair(); @@ -920,7 +1415,7 @@ mod tests { let dispute_tx = get_random_tx(); let (uuid, appointment) = - generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.compute_txid())); + generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.txid())); // Valid triggered appointments should be accepted by the Responder assert_eq!( @@ -929,48 +1424,55 @@ mod tests { ); // In this case the appointment is kept in the Responder and, therefore, in the database assert!(watcher.responder.has_tracker(uuid)); - assert!(watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(matches!( + watcher.dbm.lock().unwrap().load_appointment(uuid), + Ok(ExtendedAppointment { .. }) + )); // A properly formatted but invalid transaction should be rejected by the Responder // Update the Responder with a new Carrier that will reject the transaction - let (carrier, _as) = create_carrier( + *watcher.responder.get_carrier().lock().unwrap() = create_carrier( MockedServerQuery::Error(rpc_errors::RPC_VERIFY_ERROR as i64), chain.tip().deref().height, ); - *watcher.responder.get_carrier().lock().unwrap() = carrier; let dispute_tx = get_random_tx(); let (uuid, appointment) = - generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.compute_txid())); + generate_dummy_appointment_with_user(user_id, Some(&dispute_tx.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!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(matches!( + watcher.dbm.lock().unwrap().load_appointment(uuid), + Err { .. } + )); // 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, appointment) = generate_dummy_appointment_with_user(user_id, None); + let uuid = generate_uuid(); assert_eq!( - watcher.store_triggered_appointment(uuid, &appointment, user_id, &dispute_tx), + watcher.store_triggered_appointment(uuid, &appointment, user_id, &get_random_tx()), TriggeredAppointment::Invalid, ); // The appointment is not kept anywhere assert!(!watcher.responder.has_tracker(uuid)); - assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(matches!( + watcher.dbm.lock().unwrap().load_appointment(uuid), + Err { .. } + )); } #[tokio::test] async fn test_get_appointment() { let mut chain = Blockchain::default().with_height(START_HEIGHT); - let (watcher, _s) = init_watcher(&mut chain).await; + let watcher = init_watcher(&mut chain).await; - let dispute_tx = get_random_tx(); - let appointment = generate_dummy_appointment(Some(&dispute_tx.compute_txid())).inner; + let appointment = generate_dummy_appointment(None).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), @@ -984,12 +1486,12 @@ mod tests { watcher .add_appointment( appointment.clone(), - cryptography::sign(&appointment.to_vec(), &user_sk), + cryptography::sign(&appointment.serialize(), &user_sk).unwrap(), ) .unwrap(); let message = format!("get appointment {}", appointment.locator); - let signature = cryptography::sign(message.as_bytes(), &user_sk); + let signature = cryptography::sign(message.as_bytes(), &user_sk).unwrap(); let info = watcher .get_appointment(appointment.locator, &signature) .unwrap(); @@ -1003,19 +1505,32 @@ 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. + // Remove the data from the Watcher memory first (data is kept in the db tho) 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 = 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 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 tracker_message = format!("get appointment {}", appointment.locator); - let tracker_signature = cryptography::sign(tracker_message.as_bytes(), &user_sk); + let tracker_signature = cryptography::sign(tracker_message.as_bytes(), &user_sk).unwrap(); let info = watcher .get_appointment(appointment.locator, &tracker_signature) .unwrap(); @@ -1027,13 +1542,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); + let signature2 = cryptography::sign(message.as_bytes(), &user2_sk).unwrap(); assert!(matches!( watcher.get_appointment(appointment.locator, &signature2), Err(GetAppointmentFailure::NotFound { .. }) @@ -1042,7 +1557,12 @@ mod tests { // If the user subscription has expired, the request will fail watcher .gatekeeper - .add_outdated_user(user_id, START_HEIGHT as u32); + .get_registered_users() + .lock() + .unwrap() + .get_mut(&user_id) + .unwrap() + .subscription_expiry = START_HEIGHT as u32; assert!(matches!( watcher.get_appointment(appointment.locator, &signature), @@ -1053,174 +1573,284 @@ mod tests { #[tokio::test] async fn test_get_breaches() { let mut chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 10); - let (watcher, _s) = init_watcher(&mut chain).await; + let txs = chain.blocks.last().unwrap().txdata.clone(); + let watcher = init_watcher(&mut chain).await; // Let's create some locators based on the transactions in the last block - 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(); + let mut locator_tx_map = HashMap::new(); + for tx in txs { + locator_tx_map.insert(Locator::new(tx.txid()), tx.clone()); + } // Add some of them to the Watcher - let mut breaches = HashMap::new(); - for (i, (l, tx)) in locator_tx_map.iter().enumerate() { - // Track some of the these transactions. + for (i, locator) in locator_tx_map.keys().enumerate() { if i % 2 == 0 { - 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()); + watcher + .locator_uuid_map + .lock() + .unwrap() + .insert(*locator, HashSet::from_iter(vec![generate_uuid()])); } } // Check that breaches are correctly detected - assert_eq!(watcher.get_breaches(locator_tx_map), breaches); - } - - #[tokio::test] - 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 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(); - } - - assert!(watcher.handle_breaches(breaches).is_none()) - } - - #[tokio::test] - 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; - - // 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 = 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 - appointment.encrypted_blob.reverse(); - rejected.insert(uuid); - }; - let signature = cryptography::sign(&appointment.to_vec(), &user_sk); - watcher.add_appointment(appointment, signature).unwrap(); - } - - assert_eq!( - rejected, - HashSet::from_iter(watcher.handle_breaches(breaches).unwrap()) + 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)) ); } #[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; - - // 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; + async fn test_filter_breaches() { + let mut chain = Blockchain::default().with_height_and_txs(START_HEIGHT, 12); + let txs = chain.blocks.last().unwrap().txdata.clone(); + let watcher = init_watcher(&mut chain).await; // Let'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); + let mut locator_tx_map = HashMap::new(); + for tx in txs { + locator_tx_map.insert(Locator::new(tx.txid()), tx.clone()); } - assert_eq!( - uuids, - HashSet::from_iter(watcher.handle_breaches(breaches).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 { .. })); } #[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] - async fn test_filtered_block_connected() { + async fn test_delete_appointments_from_memory() { let mut chain = Blockchain::default().with_height(START_HEIGHT); - let (watcher, _s) = init_watcher(&mut chain).await; + let watcher = init_watcher(&mut chain).await; + + // Add some appointments both to memory and to the database + let mut to_be_deleted = HashMap::new(); + + 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])); + + 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 = 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 + 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(i, 42)); + } + } + + // 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, + ); + + // 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) + )); + + 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 { .. }) + )); + } + } + + // 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 + ); + } + } + + #[tokio::test] + async fn test_block_connected() { + let mut chain = Blockchain::default().with_height(START_HEIGHT); + let watcher = init_watcher(&mut chain).await; // block_connected for the Watcher is used to keep track of what new transactions has been mined whose may be potential // channel breaches. @@ -1231,7 +1861,7 @@ mod tests { watcher.last_known_block_height.load(Ordering::Relaxed), chain.get_block_count() ); - watcher.block_connected(&chain.generate(None), chain.get_block_count()); + watcher.block_connected(&chain.generate(None), chain.get_block_count() as u32); assert_eq!( watcher.last_known_block_height.load(Ordering::Relaxed), chain.get_block_count() @@ -1240,11 +1870,9 @@ 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 (decryption error or rejection by responder). + // - Delete invalid appointments. // - Delete appointments that have been outdated (i.e. have expired without a trigger) - // - Delete invalid appointments also from the Gatekeeper - // - // We will also test that appointments for outdated users are removed by the GK. + // - Delete invalid appointments also from the Gatekeeper (not outdated tough, the GK will take care of those via it's own Listen) // 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(); @@ -1254,118 +1882,194 @@ mod tests { watcher.register(user_id).unwrap(); watcher.register(user2_id).unwrap(); - let appointment = generate_dummy_appointment(None).inner; - let uuid1 = UUID::new(appointment.locator, user_id); - let uuid2 = UUID::new(appointment.locator, user2_id); + let appointment = generate_dummy_appointment(None); + let uuid1 = UUID::new(appointment.locator(), user_id); + let uuid2 = UUID::new(appointment.locator(), user2_id); - let user_sig = cryptography::sign(&appointment.to_vec(), &user_sk); + let user_sig = cryptography::sign(&appointment.inner.serialize(), &user_sk).unwrap(); watcher - .add_appointment(appointment.clone(), user_sig) + .add_appointment(appointment.inner.clone(), user_sig) + .unwrap(); + let user2_sig = cryptography::sign(&appointment.inner.serialize(), &user2_sk).unwrap(); + watcher + .add_appointment(appointment.inner.clone(), user2_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()); + .get_registered_users() + .lock() + .unwrap() + .get_mut(&user_id) + .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.dbm.lock().unwrap().appointment_exists(uuid)); + 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) + ); - // 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()); + watcher.block_connected(&chain.generate(None), 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() - .contains_key(&user_id)); - assert!(watcher.dbm.lock().unwrap().appointment_exists(uuid2)); - assert!(watcher - .gatekeeper - .get_registered_users() - .lock() - .unwrap() - .contains_key(&user2_id)); + 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 { .. }) + )); // Check triggers. Add a new appointment and trigger it with valid data. let dispute_tx = get_random_tx(); - 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); + let appointment = generate_dummy_appointment(Some(&dispute_tx.txid())); + let sig = cryptography::sign(&appointment.inner.serialize(), &user2_sk).unwrap(); + let uuid = UUID::new(appointment.locator(), user2_id); watcher.add_appointment(appointment.inner, sig).unwrap(); - assert!(watcher.dbm.lock().unwrap().appointment_exists(uuid)); + assert!(watcher.appointments.lock().unwrap().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()); + 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) + ); // Data should have been kept in the database - 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)); + assert!(matches!( + watcher.dbm.lock().unwrap().load_appointment(uuid), + Ok(ExtendedAppointment { .. }) + )); + assert!(matches!( + watcher.dbm.lock().unwrap().load_tracker(uuid), + Ok(TransactionTracker { .. }) + )); // Check triggering with a valid formatted transaction but that is rejected by the Responder. let dispute_tx = get_random_tx(); - 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); + let appointment = generate_dummy_appointment(Some(&dispute_tx.txid())); + let sig = cryptography::sign(&appointment.inner.serialize(), &user2_sk).unwrap(); + let uuid = UUID::new(appointment.locator(), user2_id); watcher.add_appointment(appointment.inner, sig).unwrap(); // Set the carrier response - // Both non-decryptable blobs and blobs with invalid transactions will yield an invalid trigger. - let (carrier, _s) = create_carrier( + *watcher.responder.get_carrier().lock().unwrap() = create_carrier( MockedServerQuery::Error(rpc_errors::RPC_VERIFY_ERROR as i64), chain.tip().deref().height, ); - *watcher.responder.get_carrier().lock().unwrap() = carrier; - let block = chain.generate(Some(vec![dispute_tx])); + 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.serialize(), &user2_sk).unwrap(); + let uuid = UUID::new(appointment.locator(), user2_id); watcher - .gatekeeper - .block_connected(&block, chain.get_block_count()); - watcher.block_connected(&block, chain.get_block_count()); + .add_appointment(appointment.inner.clone(), sig) + .unwrap(); - // Data should have been wiped from the database - assert!(!watcher.responder.has_tracker(uuid)); - assert!(!watcher.dbm.lock().unwrap().appointment_exists(uuid)); + 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) + )); } #[tokio::test] async fn test_block_disconnected() { let mut chain = Blockchain::default().with_height(START_HEIGHT); let start_height = START_HEIGHT as u32; - let (watcher, _s) = init_watcher(&mut chain).await; + let watcher = init_watcher(&mut chain).await; // block_disconnected for the Watcher fixes the locator cache by removing the disconnected block // and updates the last_known_block_height to the previous block height @@ -1374,7 +2078,7 @@ mod tests { .locator_cache .lock() .unwrap() - .blocks() + .blocks .contains(&last_block_header.block_hash())); watcher.block_disconnected(&last_block_header, start_height); @@ -1387,7 +2091,7 @@ mod tests { .locator_cache .lock() .unwrap() - .blocks() + .blocks .contains(&last_block_header.block_hash())); } } diff --git a/watchtower-plugin/Cargo.toml b/watchtower-plugin/Cargo.toml deleted file mode 100755 index d216835..0000000 --- a/watchtower-plugin/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "watchtower-plugin" -version = "0.2.0" -authors = ["Sergi Delgado Segura "] -license = "MIT" -edition = "2021" - -[[bin]] -name = "watchtower-client" -path = "src/main.rs" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -# General -backoff = { version = "0.4.0", features = ["tokio"] } -hex = { version = "0.4.3", features = [ "serde" ] } -home = "0.5.3" -reqwest = { version = "0.11", features = [ "blocking", "json", "socks" ] } -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.11", features = [ "tls", "transport" ] } -tokio = { version = "1.5", features = [ "rt-multi-thread", "fs" ] } - -# Bitcoin and Lightning -bitcoin = "0.32.0" -cln-plugin = "0.3.0" - -# Local -teos-common = { path = "../teos-common" } - -[dev-dependencies] -mockito = "0.32.4" -tempdir = "0.3.7" diff --git a/watchtower-plugin/README.md b/watchtower-plugin/README.md deleted file mode 100644 index 0f9ec7e..0000000 --- a/watchtower-plugin/README.md +++ /dev/null @@ -1,300 +0,0 @@ -# Watchtower client - -This is a watchtower client plugin to interact with an [Eye of Satoshi tower](https://github.com/talaia-labs/rust-teos), -and eventually with any [BOLT13](https://github.com/sr-gi/bolt13/blob/master/13-watchtowers.md) compliant watchtower. - -The plugin manages all the client-side logic to send appointment to a number of registered towers every time a new -commitment transaction is generated. It also keeps a summary of the messages sent to the towers and their responses. - -The plugin has the following methods: - -- `registertower `: registers the user id (compressed public key) with a given tower. -- `gettowerinfo `: gets all the locally stored data about a given tower. -- `retrytower `: tries to send pending appointment to a (previously) unreachable tower. -- `abandontower `: deletes all data associated with a given tower. -- `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. -- `getappointmentreceipt `: pulls a given appointment receipt from the local database. -- `getregistrationreceipt `: pulls the latest registration receipt from the local database. - -The plugin also has an implicit method to send appointments to the registered towers for every new commitment transaction. - -# Installing the plugin and linking it to CLN - -The first step to add the plugin to CLN is installing it. To do so you need to run (from the `rust-teos` folder): - -``` -cargo install --locked --path watchtower-plugin -``` - -That will generate a binary called `watchtower-client`. That's the binary we need to link to CLN. - -You can link the plugin either via cmd or by placing it in the plugins folder. - -### Linking the plugin via cmd - -To link the plugin via cmd simply run `lightningd` specifying the plugin to link, that is: - -``` -lightningd --plugin=watchtower-client -``` - -Notice that you'll need to do this every time you restart your node. - -### Linking the plugin via the plugins folder - -You can also add a plugin by adding it to the plugins folder so you don't have to link it every single time. In order to do so you need to go to the CLN user directory in your machine (that's usually `~/.lightning`). Once there, you need to create a folder called `plugins` if it does not exist: - -``` -cd ~/.lightning -mkdir plugins && cd plugins -``` - -Now you need to place the watchtower-client into this folder. To do so we will create a symbolic link to it. First, check where the binary is placed (this is usually placed in your user's home). - -``` -whereis watchtower-client -> watchtower-client: /.cargo/bin/watchtower-client -``` - -Notice that here `` will be the path to your user's home directory, for instance `/home/sergi/`. - -Now create a symbolic link to it (make sure to replace the path for your's!): - -``` -ln -s /.cargo/bin/watchtower-client . -``` - -If you check the folder you'll see that now there's a link called `watchtower-client`: - -``` -ls -> watchtower-client -``` - -You can now turn on your lightning node and the plugin will be automatically linked. - -### Check that the client is properly linked - -We can check the plugin was properly linked by running: - -``` -lightning-cli plugin list -``` - -That will return a list of all linked plugins, which should include the `watchtower-client`: - -``` -[ - ... - { - "name": "~/.lightning/plugins/watchtower-client", - "active": true - } -] -``` - -# Config file, data folder and first bootstrap - -The plugin, by default, creates a data folder under the user's home folder (`~/.watchtower`), where all the plugin's data is stored. The data folder can be modified by setting the ENV variable `TOWERS_DATA_DIR`. - -On first bootstrap, the plugin generates a key pair that is used as the user identifier. All requests from the user are signed using the secret key, so the tower can authenticate the user after the registration process (`registertower`). - -All the appointments generated by the tower, as well as all the registered towers' data, are stored on a `SQLite3` database under the data dir (that's `~/.watchtower/watchtowers_db.sql3` for the default data dir). - -# Core Lightning (CLN) config - -Config options can be setup directly in the [CLN config file](https://github.com/ElementsProject/lightning#configuration-file). The currently available options are: - -- `watchtower-port`: default tower API port. -- `watchtower-max-retry-time`: for how long (in seconds) a retry strategy will try to reach a temporary unreachable tower before giving up (default: 1 hour). -- `watchtower-auto-retry-delay`: how long (in seconds) the client will wait before auto-retrying a failed tower (default: 8 hours). -- `proxy`: Set a socks v5 proxy IP address and port. Notice this is necessary if you want to connect to a tower through Tor! (default: no proxy). -- `always-use-proxy`: Use the proxy always (default: false). - -Notice `proxy` and `always-use-proxy` are general CLN options that are honored by the plugin, so if set the plugin will use Tor to communicate with the tower. - -# Getting started - -## Registering with a tower - -Once the plugin is loaded in your node, the first step is to register your node with an active tower. You can do so by running: - -``` -lightning-cli registertower tower_id [host, port] -``` - -Where `tower_id` represents the target tower public key. As a convenience, `tower_id` may be of the form `tower_id@host` or `id@host:port`. In this case, the host and port parameters must be omitted. Port defaults to `9814` and can be changed in the config file. - -### Example - -``` -lightning-cli registertower 02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4 -``` - -If the tower is online, you should get back a response similar to this: - -``` -{ - "user_id": "032fd79e4052531955cf3782b09b495a75919317573ba2fb4dca199652595ced2a", - "available_slots": 10000, - "subscription_expiry": 4712 -} -``` - -Where `available_slots` is the amount of free slots the user has available in the tower, `user_id` is the user's public key and `subscription_expiry` is the block height when the subscription expires. Generally speaking, a slot fits an appointment, so in this example the user can send **10000** appointments in roughly **one month**. - -Notice that, ideally, the client and the tower have to agree on the **subscription details** (`available_slots` and `subscription_expiry`). Currently, those depend only on the tower, since it is offering the service for free. However, in the current state, hitting `registertower` again will add another `10000` slots and reset the time to `current_height + roughtly_one_mont_in_blocks`. - -## Sending data to the tower -Once your node is registered with at least one tower it will start sending appointments to the tower for every commitment transaction update on any of your channels. In the current version of the plugin, everything is sent to every registered tower (**full replication**). There is nothing to be done here, under normal conditions, the plugin takes care of it. - -## Checking the state of the towers - -To find out more information about registered towers, you can use `list_towers` and `gettowerinfo`: - -``` -lightning-cli listtowers -``` -``` -{ - "02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4": { - "net_addr": "http://localhost:9814", - "available_slots": 9996, - "subscription_expiry": 4712, - "status": "reachable", - "pending_appointments": [], - "invalid_appointments": [] - } -} -``` - -The overview contains the `id` and network address of the tower (`netaddr`), as well as the current `status` and two list of appointments: **pending** and **invalid**. - -The tower has 5 different states: - -- `reachable`: the tower is reachable at the given network address. -- `temporarily unreachable`: the tower is temporarily unreachable, meaning that one of the last requests sent to it has failed. -- `unreachable`: the tower has been unreachable for a while. -- `misbehaving`: the tower has sent us incorrect data. -- `subscription error`: the subscription with the tower has expired or run out of slots. - -The main difference between `temporarily unreachable` and `unreachable` is the amount of time that has passed since we last received a response. If a tower is temporarily unreachable, a backoff strategy is triggered and all the appointments that cannot be delivered are stored under `pending_appointments`. If the tower comes back online within the retry strategy, every pending appointment is sent through and the tower is flagged back as `reachable`. However, if the backoff strategy ends up giving up, the tower is flagged as `unreachable`. - -If the client receives data from a tower that is not properly signed, the tower is flagged as `misbehaving` and it is abandoned, meaning that no more appointments are sent to it. This state should never be reached by honest towers. - -A `subscription error` means that the subscription needs to be renewed (hit `registertower` again). - -Regarding `pending_appointments` and `invalid_appointments` they store the data that is pending to be sent to the tower (for unreachable towers) and the appointments that have been rejected by the tower for being invalid, respectively. The latter should never get populated for honest clients. - -`gettowerinfo` provides more detailed information about the tower: - -**Usage** - -``` -lightning-cli gettowerinfo tower_id -``` - -**Call** - -``` -lightning-cli gettowerinfo 02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4 -``` - -**Return** - -``` -{ - "net_addr": "http://localhost:9814", - "available_slots": 9996, - "subscription_expiry": 4712, - "status": "reachable", - "appointments": { - "b851b8ec05f5809b9a710f7d9d24db6c": "rbxrs8ncqgzyrxkw5h95a64tbeyhmx6wopdtqndktkko3mq8q3tkczjyk19epd713it8warbpnxgk8py6utq87dt16f3qk6ehkjw5c7q", - "10c6f7787fc33d6298fa89fc41f6a0eb": "dhrtt91bbswmmu41nu4quszt7bsxzpfyx84ycfc1yjt73rs8eqpqg3fwqq8q9tff8aqorohueo3bcgqrww1ocef38hdfuhna44ikjife", - "52dc9bd565bdfe227111927e3964d70b": "d9495n3giiof4rq5aqzh4a6fezftnhofwdi1gb7q5mciyq9besdh4xixczitpgo5dxzdnyzzdy4b9i7hd1zcojdgw833975dn8azfc7x", - "e2824d355f711806d38671c19b91110d": "rbxhpeztw74dspxsr3tk7jdekw7cbkt88kfmda4guf5xkmh1tcmeauqf3s15168y8eo438nbpath58qrxsh9usskzmxk8suf1h19meae" - }, - "pending_appointments": { - "062dc0f28ce5b31e6902c87ff1de15ee": { - "encrypted_blob": "e91bf1a1ab097f71976f240fb2d0c036f5b2188f14089dd1960e041b0a4d31a2bcbf9d6bec064a1d81471bdacf1f4d3b7c8d5df280d86a44504a5ee2ebf309adadc4976cc48cef7b94c9a8f17a16f0dcddfd6d0d105621bc519c0f20b46a8335a3a091bf6bfcc813bd4e34e644822bddda81b2a829d8a3b522b4c9b3f4465a6e416ae9ca8c808637cbc51e8d73dfe80cad3a6cc8c5ca018dd8a4cf2edbc02fd5f6cee0aef5ed5411731ef89061272712180c04150652f5bbb1b540ccc72547fe4ca5e92819c3bbb2feeccd7ce8f7b6568dd7f725fefdd64684f63d59e5d719b24a11272c64818b6319c19a261ee9c8a1674eb2e7c7367797893ac8", - "to_self_delay": 42 - }, - "ad229060698d4bc2b910a30933b1b50a": { - "encrypted_blob": "5881dce52efc18b698adc4f93b4ba275eb73271645b471227680ddb889ab60870972c4d44278dc55da4502021d9af67fd4e40803a2c9a6b4d2fe1d1f89b93373407302b67d12bb6c90b6e72b073f1c6bb3c69d57e635bfd5ff2b9648812364821b30bd95b3e8b3b2a888da8225e3d4d5cd2cd1cf2705d022b908b6b2d71c155ea50c38e2b3fb45a615c7bc1d61607a9240999c1bf174d6153b4ca7d086586614c99a45d7195c589fda8101ee8801e28b7ccad7c2b5fbda38cfe7b5e8ec13c23b8fa3cc3e6791ea9675f312cd59278ea0434538d15600b7fc905cf5a8371fc93d1e834e16d5c6399127b71c8f5c9ebfc11c5c8d3f72ce92e278f163", - "to_self_delay": 42 - } - }, - "invalid_appointments": {} -} -``` - -Notice that there are is a new field in this report: `appointments`. - -`appointments` contains a collection of `locator:tower_signature` pairs of all the appointments sent and accepted by the tower. - -The report may also contain a `misbehaving_proof` field if the tower has misbehaved (this is not the case for this example). The proof would look as follows: - -``` - "misbehaving_proof": { - "locator": "3ebd6c5a4d5ec18c815ad9fcda9aac75", - "appointment_receipt": { - "user_signature": "d7efykp63dy69jrtc3r65pssbdhp4335etq3jap1zqk135qmrtyhr8ghbdhw8y8f7nsjgmm9eoyhsfj6yugzq1bu657frmwwrudr9gpt", - "start_block": 391, - "signature": "rd41nsmhtjsawhc9pta1p5na7kmsyk48xttjy4bt3tbkbajboyzfq6mpamkjixs1w7qotocwjg3sxnbzg6uduec4cnahhkmctgddjn8w" - }, - "recovered_id": "02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4" - } -``` - -Finally, notice how `pending_appointments` now contains all the data about the pending appointments (**the full appointment**). The same applies to `invalid_appointments`. - -## Manually retrying a tower -If a tower has been flagged as **unreachable** (after the default backoff has failed) or there has been a **subscription error**, the tower won't be tried again until the user manually requests so. This can be managed with the `retrytower` command: - -**Usage** - -``` -lightning-cli retrytower tower_id -``` -**Call** - -``` -lightning-cli retrytower 02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4 -``` -**Return** - -``` -"Retrying 02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4" -``` - -Notice that this only works if the tower is **unreachable**. A tower cannot be retried if it is already being retried (**temporarily unreachable**). - -## Query data from a tower -Data can be queried from a tower to check, for instance, that the tower is keeping it or that it is correct. This can be done using the `getappointment` command: - -**Usage** - -``` -lightning-cli getappointment tower_id locator -``` -**Call** - -``` -lightning-cli getappointment 02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4 b851b8ec05f5809b9a710f7d9d24db6c -``` - -**Return** - -``` -{ - "appointment": { - "locator": "b851b8ec05f5809b9a710f7d9d24db6c", - "encrypted_blob": "017044dd0686e89bd3cf69777f1fdcb63d13eafa35e1946a0ac1324247ed793f11e27b3ee599bb1676cc98862c1f07d8e5bd29ed51c94c4ea2721a2b6f205f11cbdb1478da413ced585fe5069c6f438e977d325499bdedb985c055eaff00466209007587f20d09d153b537b0b1b6f5b8151384a1ad9f94dfffd5d5f6c2d484bad7d007976fdcaff173b18dbc4e1e24ca2ae29f8ab7e6933468c179f3857c813441e303b2e9e9b7625b19d8460d368f66cf5a7a2f54139ae0a0c9f0ef0c56183734e5dd51289ecb4f046d97e02895373c97e242c71f910c3ed1fc1b32eda4a3c28c73ad7e5fef624094fadb0753c03f8c9a4189a427e721f3ddfc0a", - "to_self_delay": 42 - }, - "status": "being_watched" -} -``` \ No newline at end of file diff --git a/watchtower-plugin/src/constants.rs b/watchtower-plugin/src/constants.rs deleted file mode 100644 index 90c50d8..0000000 --- a/watchtower-plugin/src/constants.rs +++ /dev/null @@ -1,52 +0,0 @@ -// Collection of ENV variable names and values -pub const TOWERS_DATA_DIR: &str = "TOWERS_DATA_DIR"; -pub const DEFAULT_TOWERS_DATA_DIR: &str = ".watchtower"; - -/// Collections of plugin option names, default values and descriptions - -pub const WT_PORT: &str = "watchtower-port"; -pub const DEFAULT_WT_PORT: i64 = 9814; -pub const WT_PORT_DESC: &str = "tower API port"; -pub const WT_MAX_RETRY_TIME: &str = "watchtower-max-retry-time"; -pub const DEFAULT_WT_MAX_RETRY_TIME: i64 = 3600; -pub const WT_MAX_RETRY_TIME_DESC: &str = "for how long (in seconds) a retry strategy will try to reach a temporary unreachable tower before giving up. Defaults to 1 hour"; -pub const WT_AUTO_RETRY_DELAY: &str = "watchtower-auto-retry-delay"; -pub const DEFAULT_WT_AUTO_RETRY_DELAY: i64 = 28800; -pub const WT_AUTO_RETRY_DELAY_DESC: &str = "how long (in seconds) a retrier will wait before auto-retrying a failed tower. Defaults to once every 8 hours"; -pub const DEV_WT_MAX_RETRY_INTERVAL: &str = "dev-watchtower-max-retry-interval"; -pub const DEFAULT_DEV_WT_MAX_RETRY_INTERVAL: i64 = 900; -pub const DEV_WT_MAX_RETRY_INTERVAL_DESC: &str = - "maximum length (in seconds) for a retry interval. Defaults to 15 min"; - -/// Collections of rpc method names and descriptions - -pub const RPC_REGISTER_TOWER: &str = "registertower"; -pub const RPC_REGISTER_TOWER_DESC: &str = - "Registers the client public key (user id) with the tower"; -pub const RPC_GET_REGISTRATION_RECEIPT: &str = "getregistrationreceipt"; -pub const RPC_GET_REGISTRATION_RECEIPT_DESC: &str = - "Gets the latest registration receipt given a tower id"; -pub const RPC_GET_APPOINTMENT: &str = "getappointment"; -pub const RPC_GET_APPOINTMENT_DESC: &str = - "Gets appointment data from the tower given a tower id and a locator"; -pub const RPC_GET_APPOINTMENT_RECEIPT: &str = "getappointmentreceipt"; -pub const RPC_GET_APPOINTMENT_RECEIPT_DESC: &str = - "Gets a (local) appointment receipt given a tower id and a locator"; -pub const RPC_GET_SUBSCRIPTION_INFO: &str = "getsubscriptioninfo"; -pub const RPC_GET_SUBSCRIPTION_INFO_DESC: &str = - "Gets the subscription information directly from the tower"; -pub const RPC_LIST_TOWERS: &str = "listtowers"; -pub const RPC_LIST_TOWERS_DESC: &str = "Lists all registered towers"; -pub const RPC_GET_TOWER_INFO: &str = "gettowerinfo"; -pub const RPC_GET_TOWER_INFO_DESC: &str = "Shows the info about a tower given a tower id"; -pub const RPC_RETRY_TOWER: &str = "retrytower"; -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 - -pub const HOOK_COMMITMENT_REVOCATION: &str = "commitment_revocation"; diff --git a/watchtower-plugin/src/convert.rs b/watchtower-plugin/src/convert.rs deleted file mode 100644 index 77ca95a..0000000 --- a/watchtower-plugin/src/convert.rs +++ /dev/null @@ -1,562 +0,0 @@ -use std::fmt; -use std::{convert::TryFrom, str::FromStr}; - -use hex::FromHex; -use serde::{Deserialize, Serialize}; -use serde_json::json; - -use bitcoin::{Transaction, Txid}; - -use teos_common::appointment::Locator; -use teos_common::TowerId; - -/// Errors related to the `registertower` command. -#[derive(Debug)] -pub enum RegisterError { - InvalidId(String), - InvalidHost(String), - InvalidPort(String), - InvalidFormat(String), -} - -impl std::fmt::Display for RegisterError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - RegisterError::InvalidId(x) => write!(f, "{x}"), - RegisterError::InvalidHost(x) => write!(f, "{x}"), - RegisterError::InvalidPort(x) => write!(f, "{x}"), - RegisterError::InvalidFormat(x) => write!(f, "{x}"), - } - } -} - -/// Parameters related to the `registertower` command. -#[derive(Debug, Serialize)] -pub struct RegisterParams { - pub tower_id: TowerId, - pub host: Option, - pub port: Option, -} - -impl RegisterParams { - fn new(tower_id: &str, host: Option<&str>, port: Option) -> Result { - let mut params = RegisterParams::from_id(tower_id)?; - - if host.is_some() { - params = params.with_host(host.unwrap())? - } - - if port.is_some() { - params = params.with_port(port.unwrap())? - } - - Ok(params) - } - - fn from_id(tower_id: &str) -> Result { - Ok(Self { - tower_id: TowerId::from_str(tower_id) - .map_err(|_| RegisterError::InvalidId("Invalid tower id".to_owned()))?, - host: None, - port: None, - }) - } - - fn with_host(self, host: &str) -> Result { - if host.is_empty() { - Err(RegisterError::InvalidHost("hostname is empty".to_owned())) - } else if host.contains(' ') { - Err(RegisterError::InvalidHost( - "hostname contains white spaces".to_owned(), - )) - } else { - Ok(Self { - host: Some(String::from(host)), - ..self - }) - } - } - - fn with_port(self, port: u64) -> Result { - if port > u16::MAX as u64 { - Err(RegisterError::InvalidPort(format!( - "port must be a 16-byte integer. Received: {port}" - ))) - } else { - Ok(Self { - port: Some(port as u16), - ..self - }) - } - } -} - -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) => { - let s = s.trim(); - let mut v = s.split('@'); - let tower_id = v.next().unwrap(); - - match v.next() { - Some(x) => { - let mut v = x.split(':'); - let host = v.next(); - let port = if let Some(p) = v.next() { - p.parse() - .map(Some) - .map_err(|_| RegisterError::InvalidPort(format!("Port is not a number: {p}")))? - } else { - None - }; - - RegisterParams::new(tower_id, host, port) - } - None => RegisterParams::from_id(tower_id), - } - }, - serde_json::Value::Array(mut a) => { - let param_count = a.len(); - - match param_count { - 1 => RegisterParams::try_from(a.pop().unwrap()), - 2 | 3 => { - let tower_id = a.get(0).unwrap().as_str().ok_or_else(|| RegisterError::InvalidId("tower_id must be a string".to_string()))?; - let host = Some(a.get(1).unwrap().as_str().ok_or_else(|| RegisterError::InvalidHost("host must be a string".to_string()))?); - let port = if let Some(p) = a.get(2) { - Some(p.as_u64().ok_or_else(|| RegisterError::InvalidPort(format!("port must be a number. Received: {p}")))?) - } else { - None - }; - - RegisterParams::new(tower_id, host, port) - } - _ => Err(RegisterError::InvalidFormat(format!("Unexpected request format. The request needs 1-3 parameters. Received: {param_count}"))), - } - }, - serde_json::Value::Object(mut m) => { - let allowed_keys = ["tower_id", "host", "port"]; - let param_count = m.len(); - - if m.is_empty() || param_count > allowed_keys.len() { - Err(RegisterError::InvalidFormat(format!("Unexpected request format. The request needs 1-3 parameters. Received: {param_count}"))) - } else if !m.contains_key(allowed_keys[0]){ - Err(RegisterError::InvalidId(format!("{} is mandatory", allowed_keys[0]))) - } else if !m.iter().all(|(k, _)| allowed_keys.contains(&k.as_str())) { - Err(RegisterError::InvalidFormat("Invalid named parameter found in request".to_owned())) - } else { - let mut params = Vec::with_capacity(allowed_keys.len()); - for k in allowed_keys { - if let Some(v) = m.remove(k) { - params.push(v); - } - } - - RegisterParams::try_from(json!(params)) - } - }, - _ => Err(RegisterError::InvalidFormat( - format!("Unexpected request format. Expected: 'tower_id[@host][:port]' or 'tower_id [host] [port]'. Received: '{value}'"), - )), - } - } -} - -/// Errors related to the `getappointment` command. -#[derive(Debug)] -pub enum GetAppointmentError { - InvalidId(String), - InvalidLocator(String), - InvalidFormat(String), -} - -impl std::fmt::Display for GetAppointmentError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - GetAppointmentError::InvalidId(x) => write!(f, "{x}"), - GetAppointmentError::InvalidLocator(x) => write!(f, "{x}"), - GetAppointmentError::InvalidFormat(x) => write!(f, "{x}"), - } - } -} - -/// Parameters related to the `getappointment` command. -#[derive(Debug)] -pub struct GetAppointmentParams { - pub tower_id: TowerId, - pub locator: Locator, -} - -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) => { - let param_count = a.len(); - if param_count != 2 { - Err(GetAppointmentError::InvalidFormat(format!( - "Unexpected request format. The request needs 2 parameter. Received: {param_count}" - ))) - } else { - let tower_id = if let Some(s) = a.get(0).unwrap().as_str() { - TowerId::from_str(s).map_err(|_| { - GetAppointmentError::InvalidId("Invalid tower id".to_owned()) - }) - } else { - Err(GetAppointmentError::InvalidId( - "tower_id must be a hex encoded string".to_owned(), - )) - }?; - - let locator = if let Some(s) = a.get(1).unwrap().as_str() { - Locator::from_hex(s).map_err(|_| { - GetAppointmentError::InvalidLocator("Invalid locator".to_owned()) - }) - } else { - Err(GetAppointmentError::InvalidLocator( - "locator must be a hex encoded string".to_owned(), - )) - }?; - - Ok(Self { tower_id, locator }) - } - } - serde_json::Value::Object(mut m) => { - let allowed_keys = ["tower_id", "locator"]; - - if m.len() > allowed_keys.len() { - return Err(GetAppointmentError::InvalidFormat( - "Invalid named argument found in request".to_owned(), - )); - } - - // DISCUSS: There may be a more idiomatic way of doing this - for k in allowed_keys.iter() { - if !m.contains_key(*k) { - return Err(GetAppointmentError::InvalidFormat(format!( - "{k} is mandatory" - ))); - } - } - - let mut params = Vec::with_capacity(allowed_keys.len()); - for k in allowed_keys { - if let Some(v) = m.remove(k) { - params.push(v); - } - } - GetAppointmentParams::try_from(json!(params)) - } - _ => Err(GetAppointmentError::InvalidFormat(format!( - "Unexpected request format. Expected: tower_id locator. Received: '{value}'" - ))), - } - } -} - -/// Data associated with a commitment revocation. Represents the data sent by CoreLN through the `commitment_revocation` hook. -#[derive(Debug, Serialize, Deserialize)] -pub struct CommitmentRevocation { - pub channel_id: String, - #[serde(rename(deserialize = "commitnum"))] - pub commit_num: u32, - pub commitment_txid: Txid, - #[serde(deserialize_with = "crate::ser::deserialize_tx")] - pub penalty_tx: Transaction, -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - use std::collections::HashMap; - - const VALID_ID: &str = "020dea894c967319407265764aba31bdef75d463f96800f34dd6df61380d82dfc0"; - - mod register_command { - use super::*; - - #[test] - fn test_from_id() { - // The tower id should be a valid id, otherwise the params construction will fail - let params = RegisterParams::from_id(VALID_ID).unwrap(); - assert!(params.host.is_none()); - assert!(params.port.is_none()); - - // Any incorrectly formatted id will make it fail - assert!(matches!( - RegisterParams::from_id(""), - Err(RegisterError::InvalidId(..)) - )); - } - - #[test] - fn test_with_host() { - // Any properly formatted host should work - let params = RegisterParams::from_id(VALID_ID).unwrap(); - let host = "myhost"; - assert_eq!(params.with_host(host).unwrap().host, Some(host.to_owned())); - - // Host must not be empty not have spaces - assert!(matches!( - RegisterParams::from_id(VALID_ID).unwrap().with_host(""), - Err(RegisterError::InvalidHost(..)) - )); - assert!(matches!( - RegisterParams::from_id(VALID_ID) - .unwrap() - .with_host("myhost "), - Err(RegisterError::InvalidHost(..)) - )); - } - - #[test] - fn test_with_port() { - let mut params = RegisterParams::from_id(VALID_ID).unwrap(); - - // Any 16-bytes value will do for the port - let port = 6677; - params = params.with_port(port).unwrap(); - assert_eq!(params.port, Some(port as u16)); - - // Going over u16::MAX will make this fail - let port = u16::MAX as u64 + 1; - assert!(matches!( - params.with_port(port), - Err(RegisterError::InvalidPort(..)) - )); - } - - #[test] - fn test_try_from_json_string() { - let ok = [ - format!("{VALID_ID}@host:80"), - format!("{VALID_ID}@host"), - VALID_ID.to_string(), - ]; - let wrong_id = ["", "id@host:80", "@host:80", "@:80"]; - let wrong_host = [ - format!("{VALID_ID}@"), - format!("{VALID_ID}@ "), - format!("{VALID_ID}@ host"), - format!("{VALID_ID}@:80"), - ]; - let wrong_port = [format!("{VALID_ID}@host:"), format!("{VALID_ID}@host:port")]; - - for s in ok { - let v = serde_json::Value::Array(vec![serde_json::Value::String(s.to_string())]); - let p = RegisterParams::try_from(v); - assert!(matches!(p, Ok(..))); - } - - for s in wrong_id { - let v = serde_json::Value::Array(vec![serde_json::Value::String(s.to_string())]); - let p = RegisterParams::try_from(v); - assert!(matches!(p, Err(RegisterError::InvalidId(..)))); - } - - for s in wrong_host { - let v = serde_json::Value::Array(vec![serde_json::Value::String(s.to_string())]); - let p = RegisterParams::try_from(v); - assert!(matches!(p, Err(RegisterError::InvalidHost(..)))); - } - - for s in wrong_port { - let v = serde_json::Value::Array(vec![serde_json::Value::String(s.to_string())]); - let p = RegisterParams::try_from(v); - assert!(matches!(p, Err(RegisterError::InvalidPort(..)))); - } - } - - #[test] - fn test_try_from_json_array() { - let id = json!(VALID_ID); - let number_id = json!(0); - - let host = json!("host"); - let number_host = json!(1); - - let port = json!(80); - let string_port = json!("80"); - - for v in [vec![&id, &host, &port], vec![&id, &host], vec![&id]] { - let p = RegisterParams::try_from(json!(v)); - assert!(matches!(p, Ok(..))); - } - - // Wrong id - let p = RegisterParams::try_from(json!(vec![&number_id, &host, &port])); - assert!(matches!(p, Err(RegisterError::InvalidId(..)))); - - // Wrong host - let p = RegisterParams::try_from(json!(vec![&id, &number_host, &port])); - assert!(matches!(p, Err(RegisterError::InvalidHost(..)))); - - // Wrong port - let p = RegisterParams::try_from(json!(vec![&id, &host, &string_port])); - assert!(matches!(p, Err(RegisterError::InvalidPort(..)))); - - // Wrong param count (params should be 1-3) - let p = RegisterParams::try_from(json!(vec![&id, &host, &port, &id])); - assert!(matches!(p, Err(RegisterError::InvalidFormat(..)))); - } - - #[test] - fn test_try_from_json_dict() { - let id = json!(VALID_ID); - let host = json!("host"); - let port = json!(80); - - for v in [ - HashMap::from([("tower_id", &id), ("host", &host), ("port", &port)]), - HashMap::from([("tower_id", &id), ("host", &host)]), - HashMap::from([("tower_id", &id)]), - ] { - let p = RegisterParams::try_from(json!(v)); - assert!(matches!(p, Ok(..))); - } - - // Id key missing - let p = - RegisterParams::try_from(json!(HashMap::from([("host", &host), ("port", &port)]))); - assert!(matches!(p, Err(RegisterError::InvalidId(..)))); - - // Wrong id key - let p = RegisterParams::try_from(json!(HashMap::from([ - ("wrong_tower_id", &id), - ("tower_id", &id), - ("host", &host), - ("port", &port) - ]))); - assert!(matches!(p, Err(RegisterError::InvalidFormat(..)))); - - // Wrong host key - let p = RegisterParams::try_from(json!(HashMap::from([ - ("tower_id", &id), - ("wrong_host", &host), - ("port", &port) - ]))); - assert!(matches!(p, Err(RegisterError::InvalidFormat(..)))); - - // Wrong port key - let p = RegisterParams::try_from(json!(HashMap::from([ - ("tower_id", &id), - ("host", &host), - ("wrong_port", &port) - ]))); - assert!(matches!(p, Err(RegisterError::InvalidFormat(..)))); - - // Wrong param count (params should be 1-3) - let p = RegisterParams::try_from(json!(HashMap::from([ - ("tower_id", &id), - ("host", &host), - ("port", &port), - ("another_param", &json!(0)) - ]))); - assert!(matches!(p, Err(RegisterError::InvalidFormat(..)))); - } - - #[test] - fn test_try_from_other_json() { - // Unexpected json object (it must be either String or Array) - let p = RegisterParams::try_from(json!(true)); - assert!(matches!(p, Err(RegisterError::InvalidFormat(..)))); - } - } - - mod get_appointment_command { - use super::*; - - #[test] - fn test_try_from_array() { - let id = json!(VALID_ID); - let wrong_id = - json!("050dea894c967319407265764aba31bdef75d463f96800f34dd6df61380d82dfc0"); - let number_id = json!(0); - - let locator = json!("c69517f00d9482e6b1c41639f9bdfd5c"); - let wrong_locator = - json!("c69517f00d9482e6b1c41639f9bdfd5cc69517f00d9482e6b1c41639f9bdfd5c"); - let number_locator = json!(1); - - // Valid params - let p = GetAppointmentParams::try_from(json!(vec![&id, &locator])); - assert!(matches!(p, Ok(..))); - - // Wrong params - // Id is a hex string but the format is wrong (wrong prefix) - let p = GetAppointmentParams::try_from(json!(vec![&wrong_id, &locator])); - assert!(matches!(p, Err(GetAppointmentError::InvalidId(..)))); - // Ud is not a hex string - let p = GetAppointmentParams::try_from(json!(vec![&number_id, &wrong_locator])); - assert!(matches!(p, Err(GetAppointmentError::InvalidId(..)))); - - // Locator is a hex string but not properly formatted (wrong length) - let p = GetAppointmentParams::try_from(json!(vec![&id, &wrong_locator])); - assert!(matches!(p, Err(GetAppointmentError::InvalidLocator(..)))); - // Locator is not a hex string - let p = GetAppointmentParams::try_from(json!(vec![&id, &number_locator])); - assert!(matches!(p, Err(GetAppointmentError::InvalidLocator(..)))); - } - - #[test] - fn test_try_from_dict() { - let id = json!(VALID_ID); - let locator = json!("c69517f00d9482e6b1c41639f9bdfd5c"); - - // Valid params - let p = GetAppointmentParams::try_from(json!(HashMap::from([ - ("tower_id", &id), - ("locator", &locator) - ]))); - assert!(matches!(p, Ok(..))); - - // Wrong keys - let p = GetAppointmentParams::try_from(json!(HashMap::from([ - ("wrong_tower_id", &id), - ("locator", &locator) - ]))); - assert!(matches!(p, Err(GetAppointmentError::InvalidFormat(..)))); - - let p = GetAppointmentParams::try_from(json!(HashMap::from([ - ("tower_id", &id), - ("wrong_locator", &locator) - ]))); - assert!(matches!(p, Err(GetAppointmentError::InvalidFormat(..)))); - - // Too many parameters - let p = GetAppointmentParams::try_from(json!(HashMap::from([ - ("tower_id", &id), - ("locator", &locator), - ("another_param", &json!(0)) - ]))); - assert!(matches!(p, Err(GetAppointmentError::InvalidFormat(..)))); - } - - #[test] - fn test_try_from_other_json() { - // Unexpected json object (it must be either String or Array) - let p = RegisterParams::try_from(json!(true)); - assert!(matches!(p, Err(RegisterError::InvalidFormat(..)))); - } - - #[test] - fn test_wrong_param_count() { - // The param count for get_appointment must be 2. - let params_vec = [vec![], vec![1], vec![1, 2, 3]]; - - for params in params_vec { - let p = GetAppointmentParams::try_from(json!(params)); - assert!(matches!(p, Err(..))); - } - } - } -} diff --git a/watchtower-plugin/src/dbm.rs b/watchtower-plugin/src/dbm.rs deleted file mode 100755 index bf271bb..0000000 --- a/watchtower-plugin/src/dbm.rs +++ /dev/null @@ -1,1339 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::iter::FromIterator; -use std::path::PathBuf; -use std::str::FromStr; - -use rusqlite::{params, Connection, Error as SqliteError}; - -use bitcoin::secp256k1::SecretKey; - -use teos_common::appointment::{Appointment, Locator}; -use teos_common::dbm::{DatabaseConnection, DatabaseManager, Error}; -use teos_common::receipts::{AppointmentReceipt, RegistrationReceipt}; -use teos_common::{TowerId, UserId}; - -use crate::{AppointmentStatus, MisbehaviorProof, TowerInfo, TowerStatus, TowerSummary}; - -const TABLES: [&str; 8] = [ - "CREATE TABLE IF NOT EXISTS towers ( - tower_id INT PRIMARY KEY, - net_addr TEXT NOT NULL, - available_slots INT NOT NULL -)", - "CREATE TABLE IF NOT EXISTS appointments ( - locator INT PRIMARY KEY, - encrypted_blob BLOB, - to_self_delay INT -)", - "CREATE TABLE IF NOT EXISTS pending_appointments ( - locator INT NOT NULL, - tower_id INT NOT NULL, - PRIMARY KEY (locator, tower_id), - FOREIGN KEY(locator) - REFERENCES appointments(locator) - ON DELETE CASCADE - FOREIGN KEY(tower_id) - REFERENCES towers(tower_id) - ON DELETE CASCADE -)", - "CREATE TABLE IF NOT EXISTS invalid_appointments ( - locator INT NOT NULL, - tower_id INT NOT NULL, - PRIMARY KEY (locator, tower_id), - FOREIGN KEY(locator) - REFERENCES appointments(locator) - ON DELETE CASCADE - FOREIGN KEY(tower_id) - REFERENCES towers(tower_id) - ON DELETE CASCADE -)", - "CREATE TABLE IF NOT EXISTS registration_receipts ( - tower_id INT NOT NULL, - available_slots INT NOT NULL, - subscription_start INT NOT NULL, - subscription_expiry INT NOT NULL, - signature BLOB NOT NULL, - PRIMARY KEY (tower_id, subscription_expiry), - FOREIGN KEY(tower_id) - REFERENCES towers(tower_id) - ON DELETE CASCADE -)", - "CREATE TABLE IF NOT EXISTS appointment_receipts ( - locator INT NOT NULL, - tower_id INT NOT NULL, - start_block INT NOT NULL, - user_signature BLOB NOT NULL, - tower_signature BLOB NOT NULL, - PRIMARY KEY (locator, tower_id), - FOREIGN KEY(tower_id) - REFERENCES towers(tower_id) - ON DELETE CASCADE -)", - "CREATE TABLE IF NOT EXISTS misbehaving_proofs ( - tower_id INT PRIMARY KEY, - locator INT NOT NULL, - recovered_id INT NOT NULL, - FOREIGN KEY(locator, tower_id) - REFERENCES appointment_receipts(locator, tower_id) - ON DELETE CASCADE -)", - "CREATE TABLE IF NOT EXISTS keys ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - key INT NOT NULL -)", -]; - -/// Component in charge of interacting with the underlying database. -/// -/// Currently works for `SQLite`. `PostgreSQL` should also be added in the future. -#[derive(Debug)] -pub struct DBM { - /// The underlying database connection. - connection: Connection, -} - -impl DatabaseConnection for DBM { - fn get_connection(&self) -> &Connection { - &self.connection - } - - fn get_mut_connection(&mut self) -> &mut Connection { - &mut self.connection - } -} - -impl DBM { - /// Creates a new [DBM] instance. - pub fn new(db_path: &PathBuf) -> Result { - let connection = Connection::open(db_path)?; - connection.execute("PRAGMA foreign_keys=1;", [])?; - let mut dbm = Self { connection }; - dbm.create_tables(Vec::from_iter(TABLES))?; - - Ok(dbm) - } - - /// Stores the client secret key into the database. - /// - /// When a new key is generated, old keys are not overwritten but are not retrievable from the API either. - pub fn store_client_key(&self, sk: &SecretKey) -> Result<(), Error> { - let query = "INSERT INTO keys (key) VALUES (?)"; - self.store_data(query, params![sk.display_secret().to_string()]) - } - - /// Loads the last known client secret key from the database. - /// - /// 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) -> Option { - let mut stmt = self - .connection - .prepare( - "SELECT key FROM keys WHERE id = (SELECT seq FROM sqlite_sequence WHERE name=(?))", - ) - .unwrap(); - - stmt.query_row(["keys"], |row| { - let sk: String = row.get(0).unwrap(); - Ok(SecretKey::from_str(&sk).unwrap()) - }) - .ok() - } - - /// Stores a tower record into the database alongside the corresponding registration receipt. - /// - /// This function MUST be guarded against inserting duplicate (tower_id, subscription_expiry) pairs. - /// This is currently done in WTClient::add_update_tower. - pub fn store_tower_record( - &mut self, - tower_id: TowerId, - net_addr: &str, - receipt: &RegistrationReceipt, - ) -> Result<(), Error> { - let tx = self.get_mut_connection().transaction().unwrap(); - tx.execute( - "INSERT INTO towers (tower_id, net_addr, available_slots) - VALUES (?1, ?2, ?3) - ON CONFLICT (tower_id) DO UPDATE SET net_addr = ?2, available_slots = ?3", - params![tower_id.to_vec(), net_addr, receipt.available_slots()], - ) - .map_err(Error::Unknown)?; - tx.execute( - "INSERT INTO registration_receipts (tower_id, available_slots, subscription_start, subscription_expiry, signature) - VALUES (?1, ?2, ?3, ?4, ?5)", - params![tower_id.to_vec(), receipt.available_slots(), receipt.subscription_start(), receipt.subscription_expiry(), receipt.signature()]).map_err( Error::Unknown)?; - - tx.commit().map_err(Error::Unknown) - } - - /// Loads a tower record from the database. - /// - /// 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) -> Option { - let mut stmt = self - .connection - .prepare("SELECT t.net_addr, t.available_slots, r.subscription_start, r.subscription_expiry - FROM towers as t, registration_receipts as r - WHERE t.tower_id = r.tower_id AND t.tower_id = ?1 AND r.subscription_expiry = (SELECT MAX(subscription_expiry) - FROM registration_receipts - WHERE tower_id = ?1)") - .unwrap(); - - let mut tower = stmt - .query_row([tower_id.to_vec()], |row| { - let net_addr: String = row.get(0).unwrap(); - let available_slots: u32 = row.get(1).unwrap(); - let subscription_start: u32 = row.get(2).unwrap(); - let subscription_expiry: u32 = row.get(3).unwrap(); - Ok(TowerInfo::new( - net_addr, - available_slots, - subscription_start, - subscription_expiry, - self.load_appointment_receipts(tower_id), - self.load_appointments(tower_id, AppointmentStatus::Pending), - self.load_appointments(tower_id, AppointmentStatus::Invalid), - )) - }) - .ok()?; - - 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; - } - - Some(tower) - } - - /// Loads the latest registration receipt for a given tower. - /// - /// Latests is determined by the one with the `subscription_expiry` further into the future. - pub fn load_registration_receipt( - &self, - tower_id: TowerId, - user_id: UserId, - ) -> Option { - let mut stmt = self - .connection - .prepare( - "SELECT available_slots, subscription_start, subscription_expiry, signature - FROM registration_receipts - WHERE tower_id = ?1 AND subscription_expiry = (SELECT MAX(subscription_expiry) - FROM registration_receipts - WHERE tower_id = ?1)", - ) - .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, - )) - }) - .ok() - } - - /// Removes a tower record from the database. - /// - /// This triggers a cascade deletion of all related data, such as appointments, appointment receipts, etc. As long as there is a single - /// reference to them. - pub fn remove_tower_record(&self, tower_id: TowerId) -> Result<(), Error> { - let query = "DELETE FROM towers WHERE tower_id=?"; - self.remove_data(query, params![tower_id.to_vec()]) - } - - /// Loads all tower records from the database. - pub fn load_towers(&self) -> HashMap { - let mut towers = HashMap::new(); - let mut stmt = self - .connection - .prepare("SELECT tw.tower_id, tw.net_addr, tw.available_slots, rr.subscription_start, rr.subscription_expiry - FROM towers AS tw - JOIN registration_receipts AS rr - JOIN (SELECT tower_id, MAX(subscription_expiry) AS max_se - FROM registration_receipts - GROUP BY tower_id) AS max_rrs ON (tw.tower_id = rr.tower_id) - AND (rr.tower_id = max_rrs.tower_id) - AND (rr.subscription_expiry = max_rrs.max_se)") - .unwrap(); - let mut rows = stmt.query([]).unwrap(); - - while let Ok(Some(row)) = rows.next() { - let raw_towerid: Vec = row.get(0).unwrap(); - let tower_id = TowerId::from_slice(&raw_towerid).unwrap(); - let net_addr: String = row.get(1).unwrap(); - let available_slots: u32 = row.get(2).unwrap(); - let start: u32 = row.get(3).unwrap(); - let expiry: u32 = row.get(4).unwrap(); - - let mut tower = TowerSummary::with_appointments( - net_addr, - available_slots, - start, - expiry, - self.load_appointment_locators(tower_id, AppointmentStatus::Pending), - self.load_appointment_locators(tower_id, AppointmentStatus::Invalid), - ); - - if self.exists_misbehaving_proof(tower_id) { - tower.status = TowerStatus::Misbehaving; - } else if !tower.pending_appointments.is_empty() { - // TODO: We could set the status to SubscriptionError here if we checked the state of the subscription - // (using available_slots and expiry). This will be possible once we implement cln rpc queries (which are - // already viable since cln-plugin = "0.1.1"). - tower.status = TowerStatus::TemporaryUnreachable; - } - - towers.insert(tower_id, tower); - } - - towers - } - - /// Stores an appointments receipt into the database representing an appointment accepted by a given tower. - pub fn store_appointment_receipt( - &mut self, - tower_id: TowerId, - locator: Locator, - available_slots: u32, - receipt: &AppointmentReceipt, - ) -> Result<(), SqliteError> { - let tx = self.get_mut_connection().transaction().unwrap(); - tx.execute( - "INSERT INTO appointment_receipts (locator, tower_id, start_block, user_signature, tower_signature) - VALUES (?1, ?2, ?3, ?4, ?5)", - params![ - locator.to_vec(), - tower_id.to_vec(), - receipt.start_block(), - receipt.user_signature(), - receipt.signature() - ], - )?; - tx.execute( - "UPDATE towers SET available_slots=?1 WHERE tower_id=?2", - params![available_slots, tower_id.to_vec()], - )?; - tx.commit() - } - - /// Loads a given appointment receipt of a given tower from the database. - pub fn load_appointment_receipt( - &self, - tower_id: TowerId, - locator: Locator, - ) -> Option { - let mut stmt = self - .connection - .prepare("SELECT start_block, user_signature, tower_signature FROM appointment_receipts WHERE tower_id = ?1 and locator = ?2") - .unwrap(); - - stmt.query_row(params![tower_id.to_vec(), locator.to_vec()], |row| { - let start_block = row.get::<_, u32>(0).unwrap(); - let user_sig = row.get::<_, String>(1).unwrap(); - let tower_sig = row.get::<_, String>(2).unwrap(); - - Ok(AppointmentReceipt::with_signature( - user_sig, - start_block, - tower_sig, - )) - }) - .ok() - } - - /// Loads the appointment receipts associated to a given tower. - /// - /// TODO: Currently this is only loading a summary of the receipt, if we need to really load all the information - /// for any reason this method may need to be renamed. - pub fn load_appointment_receipts(&self, tower_id: TowerId) -> HashMap { - let mut receipts = HashMap::new(); - let mut stmt = self - .connection - .prepare("SELECT locator, tower_signature FROM appointment_receipts WHERE tower_id = ?") - .unwrap(); - let mut rows = stmt.query([tower_id.to_vec()]).unwrap(); - - while let Ok(Some(row)) = rows.next() { - let locator = Locator::from_slice(&row.get::<_, Vec>(0).unwrap()).unwrap(); - let signature = row.get::<_, String>(1).unwrap(); - - receipts.insert(locator, signature); - } - - receipts - } - - /// Loads a collection of locators from the database entry associated to a given tower. - /// - /// The loaded locators can be loaded either from appointment_receipts, pending_appointments or invalid_appointments - /// depending on `status`. - pub fn load_appointment_locators( - &self, - tower_id: TowerId, - status: AppointmentStatus, - ) -> HashSet { - let status = match status { - AppointmentStatus::Accepted => "appointment_receipts", - AppointmentStatus::Pending => "pending_appointments", - AppointmentStatus::Invalid => "invalid_appointments", - }; - let mut appointments = HashSet::new(); - // TODO: Can this be prepared instead of formatted (using ?1 seems to fail)? - let mut stmt = self - .connection - .prepare(&format!("SELECT locator FROM {status} WHERE tower_id = ?")) - .unwrap(); - - let mut rows = stmt.query(params![tower_id.to_vec()]).unwrap(); - while let Ok(Some(inner_row)) = rows.next() { - appointments - .insert(Locator::from_slice(&inner_row.get::<_, Vec>(0).unwrap()).unwrap()); - } - - appointments - } - - /// Loads an appointment from the database. - pub fn load_appointment(&self, locator: Locator) -> Option { - let mut stmt = self - .connection - .prepare("SELECT encrypted_blob, to_self_delay FROM appointments WHERE locator = ?") - .unwrap(); - - stmt.query_row(params![locator.to_vec()], |row| { - let encrypted_blob = row.get::<_, Vec>(0).unwrap(); - let to_self_delay = row.get::<_, u32>(1).unwrap(); - - Ok(Appointment::new(locator, encrypted_blob, to_self_delay)) - }) - .ok() - } - - /// Stores an appointment into the database. - /// - /// Appointments are only stored as a whole when they are pending or invalid. - /// Accepted appointments are simplified in the form of an appointment receipt. - fn store_appointment( - tx: &rusqlite::Transaction, - appointment: &Appointment, - ) -> Result { - tx.execute( - "INSERT INTO appointments (locator, encrypted_blob, to_self_delay) VALUES (?1, ?2, ?3)", - params![ - appointment.locator.to_vec(), - appointment.encrypted_blob, - appointment.to_self_delay - ], - ) - } - - /// Stores a pending appointment into the database. - /// - /// A pending appointment is an appointment that was sent to a tower when it was unreachable. - /// This data is stored so it can be resent once the tower comes back online. - /// Internally calls [Self::store_appointment]. - pub fn store_pending_appointment( - &mut self, - tower_id: TowerId, - appointment: &Appointment, - ) -> Result<(), SqliteError> { - let tx = self.get_mut_connection().transaction().unwrap(); - - // If the appointment already exists (because it was added by another tower as either pending or invalid) we simply - // ignore the error. - Self::store_appointment(&tx, appointment).ok(); - tx.execute( - "INSERT INTO pending_appointments (locator, tower_id) VALUES (?1, ?2)", - params![appointment.locator.to_vec(), tower_id.to_vec(),], - )?; - - tx.commit() - } - - /// Removes a pending appointment from the database. - /// - /// If the pending appointment is the only instance of the appointment, the appointment will also be deleted form the appointments table. - pub fn delete_pending_appointment( - &mut self, - tower_id: TowerId, - locator: Locator, - ) -> Result<(), SqliteError> { - // We will delete data from pending_appointments or from appointments depending on whether the later has a single reference - // to it or not. If that's the case, deleting the entry from appointments will trigger a cascade deletion of the entry in pending. - // If there are other references, this will be deleted when removing the last one. - let count = { - let mut stmt = self - .connection - .prepare("SELECT COUNT(*) FROM pending_appointments WHERE locator=?") - .unwrap(); - let pending = stmt - .query_row(params![locator.to_vec()], |row| row.get::<_, u32>(0)) - .unwrap(); - - let mut stmt = self - .connection - .prepare("SELECT COUNT(*) FROM invalid_appointments WHERE locator=?") - .unwrap(); - let invalid = stmt - .query_row(params![locator.to_vec()], |row| row.get::<_, u32>(0)) - .unwrap_or(0); - - pending + invalid - }; - - let tx = self.get_mut_connection().transaction().unwrap(); - if count == 1 { - tx.execute( - "DELETE FROM appointments WHERE locator=?", - params![locator.to_vec()], - )?; - } else { - tx.execute( - "DELETE FROM pending_appointments WHERE locator=?1 AND tower_id=?2", - params![locator.to_vec(), tower_id.to_vec()], - )?; - }; - tx.commit() - } - - /// Stores an invalid appointment into the database. - /// - /// An invalid appointment is an appointment that was rejected by the tower. - /// Storing this data may allow us to see what was the issue and send the data later on. - /// Internally calls [Self::store_appointment]. - pub fn store_invalid_appointment( - &mut self, - tower_id: TowerId, - appointment: &Appointment, - ) -> Result<(), SqliteError> { - let tx = self.get_mut_connection().transaction().unwrap(); - - // If the appointment already exists (because it was added by another tower as either pending or invalid) we simply - // ignore the error. - Self::store_appointment(&tx, appointment).ok(); - tx.execute( - "INSERT INTO invalid_appointments (locator, tower_id) VALUES (?1, ?2)", - params![appointment.locator.to_vec(), tower_id.to_vec(),], - )?; - - tx.commit() - } - - /// Loads non finalized appointments from the database for a given tower based on a status flag. - /// - /// This is meant to be used only for pending and invalid appointments, if the method is called for - /// accepted appointment, an empty collection will be returned. - pub fn load_appointments( - &self, - tower_id: TowerId, - status: AppointmentStatus, - ) -> Vec { - let table = match status { - AppointmentStatus::Accepted => return Vec::new(), - AppointmentStatus::Pending => "pending_appointments", - AppointmentStatus::Invalid => "invalid_appointments", - }; - - let mut appointments = Vec::new(); - let mut stmt = self - .connection - .prepare(&format!("SELECT a.locator, a.encrypted_blob, a.to_self_delay FROM appointments as a, {table} as t WHERE a.locator = t.locator AND t.tower_id = ?")) - .unwrap(); - let mut rows = stmt.query([tower_id.to_vec()]).unwrap(); - - while let Ok(Some(row)) = rows.next() { - let locator = Locator::from_slice(&row.get::<_, Vec>(0).unwrap()).unwrap(); - let encrypted_blob = row.get::<_, Vec>(1).unwrap(); - let to_self_delay = row.get::<_, u32>(2).unwrap(); - - appointments.push(Appointment::new(locator, encrypted_blob, to_self_delay)); - } - - appointments - } - - /// Stores a misbehaving proof into the database. - /// - /// A misbehaving proof is proof that the tower has signed an appointment using a key different - /// than the one advertised to the user when they registered. - pub fn store_misbehaving_proof( - &mut self, - tower_id: TowerId, - proof: &MisbehaviorProof, - ) -> Result<(), SqliteError> { - let tx = self.get_mut_connection().transaction().unwrap(); - tx.execute( - "INSERT INTO appointment_receipts (tower_id, locator, start_block, user_signature, tower_signature) - VALUES (?1, ?2, ?3, ?4, ?5)", - params![ - tower_id.to_vec(), - proof.locator.to_vec(), - proof.appointment_receipt.start_block(), - proof.appointment_receipt.user_signature(), - proof.appointment_receipt.signature() - ], - )?; - tx.execute( - "INSERT INTO misbehaving_proofs (tower_id, locator, recovered_id) VALUES (?1, ?2, ?3)", - params![ - tower_id.to_vec(), - proof.locator.to_vec(), - proof.recovered_id.to_vec() - ], - )?; - - tx.commit() - } - - /// Loads the misbehaving proof for a given tower from the database (if found). - 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 = ?") - .unwrap(); - - misbehaving_stmt - .query_row([tower_id.to_vec()], |row| { - let locator = Locator::from_slice(&row.get::<_, Vec>(0).unwrap()).unwrap(); - let recovered_id = TowerId::from_slice(&row.get::<_, Vec>(1).unwrap()).unwrap(); - Ok((locator, recovered_id)) - }) - .map(|(locator, recovered_id)| { - let mut receipt_stmt = self - .connection - .prepare( - "SELECT start_block, user_signature, tower_signature - FROM appointment_receipts - WHERE locator = ?1 AND tower_id = ?2", - ) - .unwrap(); - let receipt = receipt_stmt - .query_row([locator.to_vec(), tower_id.to_vec()], |row| { - let start_block = row.get::<_, u32>(0).unwrap(); - let user_signature = row.get::<_, String>(1).unwrap(); - let tower_signature = row.get::<_, String>(2).unwrap(); - Ok(AppointmentReceipt::with_signature( - user_signature, - start_block, - tower_signature, - )) - }) - .unwrap(); - MisbehaviorProof::new(locator, receipt, recovered_id) - }) - .ok() - } - - /// Checks whether a misbehaving proof exists for a given tower. - fn exists_misbehaving_proof(&self, tower_id: TowerId) -> bool { - let mut misbehaving_stmt = self - .connection - .prepare("SELECT tower_id FROM misbehaving_proofs WHERE tower_id = ?") - .unwrap(); - misbehaving_stmt.exists([tower_id.to_vec()]).unwrap() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use teos_common::cryptography::get_random_keypair; - use teos_common::test_utils::{ - generate_random_appointment, get_random_registration_receipt, get_random_user_id, - get_registration_receipt_from_previous, - }; - - impl DBM { - pub(crate) fn in_memory() -> Result { - let connection = Connection::open_in_memory()?; - connection.execute("PRAGMA foreign_keys=1;", [])?; - let mut dbm = Self { connection }; - dbm.create_tables(Vec::from_iter(TABLES))?; - - Ok(dbm) - } - - pub(crate) fn appointment_exists(&self, locator: Locator) -> bool { - let mut stmt = self - .connection - .prepare("SELECT * FROM appointments WHERE locator=? ") - .unwrap(); - stmt.exists(params![locator.to_vec()]).unwrap() - } - - pub(crate) fn appointment_receipt_exists( - &self, - locator: Locator, - tower_id: TowerId, - ) -> bool { - let mut stmt = self - .connection - .prepare("SELECT * FROM appointment_receipts WHERE locator=?1 AND tower_id=?2 ") - .unwrap(); - stmt.exists(params![locator.to_vec(), tower_id.to_vec()]) - .unwrap() - } - } - - #[test] - fn test_create_tables() { - let connection = Connection::open_in_memory().unwrap(); - let mut dbm = DBM { connection }; - dbm.create_tables(Vec::from_iter(TABLES)).unwrap(); - } - - #[test] - fn test_store_load_tower_record() { - let mut dbm = DBM::in_memory().unwrap(); - - // In order to add a tower record we need to associated registration receipt. - let tower_id = get_random_user_id(); - let net_addr = "talaia.watch"; - - let receipt = get_random_registration_receipt(); - let tower_info = TowerInfo::new( - net_addr.to_owned(), - receipt.available_slots(), - receipt.subscription_start(), - receipt.subscription_expiry(), - HashMap::new(), - Vec::new(), - Vec::new(), - ); - - // Check the loaded data matches the in memory data - dbm.store_tower_record(tower_id, net_addr, &receipt) - .unwrap(); - assert_eq!(dbm.load_tower_record(tower_id).unwrap(), tower_info); - } - - #[test] - fn test_load_registration_receipt() { - let mut dbm = DBM::in_memory().unwrap(); - - // Registration receipts are stored alongside tower records when the register command is called - let tower_id = get_random_user_id(); - let net_addr = "talaia.watch"; - let receipt = get_random_registration_receipt(); - - // Check the receipt was stored - dbm.store_tower_record(tower_id, net_addr, &receipt) - .unwrap(); - assert_eq!( - dbm.load_registration_receipt(tower_id, receipt.user_id()) - .unwrap(), - receipt - ); - - // Add another receipt for the same tower with a higher expiry and check this last one is loaded - let middle_receipt = get_registration_receipt_from_previous(&receipt); - let latest_receipt = get_registration_receipt_from_previous(&middle_receipt); - - dbm.store_tower_record(tower_id, net_addr, &latest_receipt) - .unwrap(); - assert_eq!( - dbm.load_registration_receipt(tower_id, latest_receipt.user_id()) - .unwrap(), - latest_receipt - ); - - // Add a final one with a lower expiry and check the last is still loaded - dbm.store_tower_record(tower_id, net_addr, &middle_receipt) - .unwrap(); - assert_eq!( - dbm.load_registration_receipt(tower_id, latest_receipt.user_id()) - .unwrap(), - latest_receipt - ); - } - - #[test] - fn test_load_same_registration_receipt() { - let mut dbm = DBM::in_memory().unwrap(); - - // Registration receipts are stored alongside tower records when the register command is called - let tower_id = get_random_user_id(); - let net_addr = "talaia.watch"; - let receipt = get_random_registration_receipt(); - - // Store it once - dbm.store_tower_record(tower_id, net_addr, &receipt) - .unwrap(); - assert_eq!( - dbm.load_registration_receipt(tower_id, receipt.user_id()) - .unwrap(), - receipt - ); - - // Store the same again, this should fail due to UNIQUE PK constrains. - // Notice store_tower_record is guarded against this by WTClient::add_update_tower though. - assert!(matches!( - dbm.store_tower_record(tower_id, net_addr, &receipt), - Err { .. } - )); - } - - #[test] - fn test_load_nonexistent_tower_record() { - let dbm = DBM::in_memory().unwrap(); - - // If the tower does not exists, `load_tower` will fail. - let tower_id = get_random_user_id(); - assert!(dbm.load_tower_record(tower_id).is_none()); - } - - #[test] - fn test_store_load_towers() { - let mut dbm = DBM::in_memory().unwrap(); - let mut towers = HashMap::new(); - - // In order to add a tower record we need to associated registration receipt. - for _ in 0..10 { - let tower_id = get_random_user_id(); - let net_addr = "talaia.watch"; - let mut receipt = get_random_registration_receipt(); - dbm.store_tower_record(tower_id, net_addr, &receipt) - .unwrap(); - - // Add not only one registration receipt to test if the tower retrieves the one with furthest expiry date. - for _ in 0..10 { - receipt = get_registration_receipt_from_previous(&receipt); - dbm.store_tower_record(tower_id, net_addr, &receipt) - .unwrap(); - } - - towers.insert( - tower_id, - TowerSummary::new( - net_addr.to_owned(), - receipt.available_slots(), - receipt.subscription_start(), - receipt.subscription_expiry(), - ), - ); - } - - assert_eq!(dbm.load_towers(), towers); - } - - #[test] - fn test_load_towers_empty() { - // If there are no towers in the database, `load_towers` should return an empty map. - let dbm = DBM::in_memory().unwrap(); - assert_eq!(dbm.load_towers(), HashMap::new()); - } - - #[test] - fn test_remove_tower_record() { - let mut dbm = DBM::in_memory().unwrap(); - - let tower_id = get_random_user_id(); - let net_addr = "talaia.watch"; - let receipt = get_random_registration_receipt(); - dbm.store_tower_record(tower_id, net_addr, &receipt) - .unwrap(); - - assert!(matches!(dbm.remove_tower_record(tower_id), Ok(()))); - } - - #[test] - fn test_remove_tower_record_inexistent() { - let dbm = DBM::in_memory().unwrap(); - - assert!(matches!( - dbm.remove_tower_record(get_random_user_id()), - Err(Error::NotFound) - )); - } - - #[test] - fn test_store_load_appointment_receipts() { - let mut dbm = DBM::in_memory().unwrap(); - - // In order to add a tower record we need to associated registration receipt. - let tower_id = get_random_user_id(); - let net_addr = "talaia.watch"; - - let receipt = get_random_registration_receipt(); - let mut tower_summary = TowerSummary::new( - net_addr.to_owned(), - receipt.available_slots(), - receipt.subscription_start(), - receipt.subscription_expiry(), - ); - dbm.store_tower_record(tower_id, net_addr, &receipt) - .unwrap(); - - // Add some appointment receipts and check they match - let mut receipts = HashMap::new(); - for _ in 0..5 { - let appointment = generate_random_appointment(None); - let user_signature = "user_signature"; - let appointment_receipt = AppointmentReceipt::with_signature( - user_signature.to_owned(), - 42, - "tower_signature".to_owned(), - ); - - tower_summary.available_slots -= 1; - - dbm.store_appointment_receipt( - tower_id, - appointment.locator, - tower_summary.available_slots, - &appointment_receipt, - ) - .unwrap(); - receipts.insert( - appointment.locator, - appointment_receipt.signature().unwrap(), - ); - } - - assert_eq!(dbm.load_appointment_receipts(tower_id), receipts); - } - - #[test] - fn test_load_appointment_receipt() { - let mut dbm = DBM::in_memory().unwrap(); - let tower_id = get_random_user_id(); - let appointment = generate_random_appointment(None); - - // If there is no appointment receipt for the given (locator, tower_id) pair, Error::NotFound is returned - // Try first with both being unknown - assert!(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"; - let receipt = get_random_registration_receipt(); - dbm.store_tower_record(tower_id, net_addr, &receipt) - .unwrap(); - - assert!(dbm - .load_appointment_receipt(tower_id, appointment.locator) - .is_none()); - - // Add both - let tower_summary = TowerSummary::new( - net_addr.to_owned(), - receipt.available_slots(), - receipt.subscription_start(), - receipt.subscription_expiry(), - ); - let appointment_receipt = AppointmentReceipt::with_signature( - "user_signature".to_owned(), - 42, - "tower_signature".to_owned(), - ); - dbm.store_appointment_receipt( - tower_id, - appointment.locator, - tower_summary.available_slots, - &appointment_receipt, - ) - .unwrap(); - - assert_eq!( - dbm.load_appointment_receipt(tower_id, appointment.locator) - .unwrap(), - appointment_receipt - ); - } - - #[test] - fn test_load_appointment_locators() { - // `load_appointment_locators` is used to load locators from either `appointment_receipts`, `pending_appointments` or `invalid_appointments` - let mut dbm = DBM::in_memory().unwrap(); - - // We first need to add a tower record to the database so we can add some associated data. - let tower_id = get_random_user_id(); - let net_addr = "talaia.watch"; - - let receipt = get_random_registration_receipt(); - let tower_summary = TowerSummary::new( - net_addr.to_owned(), - receipt.available_slots(), - receipt.subscription_start(), - receipt.subscription_expiry(), - ); - dbm.store_tower_record(tower_id, net_addr, &receipt) - .unwrap(); - - // Create all types of appointments and store them in the db. - let user_signature = "user_signature"; - let mut receipts = HashSet::new(); - let mut pending_appointments = HashSet::new(); - let mut invalid_appointments = HashSet::new(); - for _ in 0..5 { - let appointment = generate_random_appointment(None); - let appointment_receipt = AppointmentReceipt::with_signature( - user_signature.to_owned(), - 42, - "tower_signature".to_owned(), - ); - let pending_appointment = generate_random_appointment(None); - let invalid_appointment = generate_random_appointment(None); - - dbm.store_appointment_receipt( - tower_id, - appointment.locator, - tower_summary.available_slots, - &appointment_receipt, - ) - .unwrap(); - dbm.store_pending_appointment(tower_id, &pending_appointment) - .unwrap(); - dbm.store_invalid_appointment(tower_id, &invalid_appointment) - .unwrap(); - - receipts.insert(appointment.locator); - pending_appointments.insert(pending_appointment.locator); - invalid_appointments.insert(invalid_appointment.locator); - } - - // Pull data from the db and check it matches the expected data - assert_eq!( - dbm.load_appointment_locators(tower_id, AppointmentStatus::Accepted), - receipts - ); - assert_eq!( - dbm.load_appointment_locators(tower_id, AppointmentStatus::Pending), - pending_appointments - ); - assert_eq!( - dbm.load_appointment_locators(tower_id, AppointmentStatus::Invalid), - invalid_appointments - ); - } - - #[test] - fn test_store_load_appointment() { - let mut dbm = DBM::in_memory().unwrap(); - - let appointment = generate_random_appointment(None); - let tx = dbm.get_mut_connection().transaction().unwrap(); - DBM::store_appointment(&tx, &appointment).unwrap(); - tx.commit().unwrap(); - - let loaded_appointment = dbm.load_appointment(appointment.locator); - assert_eq!(appointment, loaded_appointment.unwrap()); - } - - #[test] - fn test_store_load_appointment_inexistent() { - let dbm = DBM::in_memory().unwrap(); - - let locator = generate_random_appointment(None).locator; - let loaded_appointment = dbm.load_appointment(locator); - assert!(loaded_appointment.is_none()); - } - - #[test] - fn test_store_pending_appointment() { - let mut dbm = DBM::in_memory().unwrap(); - - // In order to add a tower record we need to associated registration receipt. - let tower_id = get_random_user_id(); - let net_addr = "talaia.watch"; - - let receipt = get_random_registration_receipt(); - let mut tower_summary = TowerSummary::new( - net_addr.to_owned(), - receipt.available_slots(), - receipt.subscription_start(), - receipt.subscription_expiry(), - ) - .with_status(TowerStatus::TemporaryUnreachable); - dbm.store_tower_record(tower_id, net_addr, &receipt) - .unwrap(); - - // Add some pending appointments and check they match - for _ in 0..5 { - let appointment = generate_random_appointment(None); - - tower_summary - .pending_appointments - .insert(appointment.locator); - - dbm.store_pending_appointment(tower_id, &appointment) - .unwrap(); - assert_eq!( - TowerSummary::from(dbm.load_tower_record(tower_id).unwrap()), - tower_summary - ); - } - } - - #[test] - fn test_store_pending_appointment_twice() { - let mut dbm = DBM::in_memory().unwrap(); - - // In order to add a tower record we need to associated registration receipt. - let tower_id_1 = get_random_user_id(); - let tower_id_2 = get_random_user_id(); - let net_addr = "talaia.watch"; - - let receipt = get_random_registration_receipt(); - dbm.store_tower_record(tower_id_1, net_addr, &receipt) - .unwrap(); - dbm.store_tower_record(tower_id_2, net_addr, &receipt) - .unwrap(); - - // If the same appointment is stored twice (by different towers) it should go through - // Since the appointment data will be stored only once and this will create two references - let appointment = generate_random_appointment(None); - dbm.store_pending_appointment(tower_id_1, &appointment) - .unwrap(); - dbm.store_pending_appointment(tower_id_2, &appointment) - .unwrap(); - - // If this is called twice with for the same tower it will fail, since two identical references - // can not exist. This is intended behavior and should not happen - assert!(dbm - .store_pending_appointment(tower_id_2, &appointment) - .is_err()); - } - - #[test] - fn test_delete_pending_appointment() { - let mut dbm = DBM::in_memory().unwrap(); - - // In order to add a tower record we need to associated registration receipt. - let tower_id = get_random_user_id(); - let net_addr = "talaia.watch"; - - let receipt = get_random_registration_receipt(); - dbm.store_tower_record(tower_id, net_addr, &receipt) - .unwrap(); - - // Add a single one, remove it later - let appointment = generate_random_appointment(None); - dbm.store_pending_appointment(tower_id, &appointment) - .unwrap(); - assert!(dbm - .delete_pending_appointment(tower_id, appointment.locator) - .is_ok()); - - // The appointment should be completely gone - assert!(!dbm - .load_appointment_locators(tower_id, AppointmentStatus::Pending) - .contains(&appointment.locator)); - assert!(!dbm.appointment_exists(appointment.locator)); - - // Try again with more than one reference - let another_tower_id = get_random_user_id(); - dbm.store_tower_record(another_tower_id, net_addr, &receipt) - .unwrap(); - - // Add two - dbm.store_pending_appointment(tower_id, &appointment) - .unwrap(); - dbm.store_pending_appointment(another_tower_id, &appointment) - .unwrap(); - // Delete one - assert!(dbm - .delete_pending_appointment(tower_id, appointment.locator) - .is_ok()); - // Check - assert!(!dbm - .load_appointment_locators(tower_id, AppointmentStatus::Pending) - .contains(&appointment.locator)); - assert!(dbm - .load_appointment_locators(another_tower_id, AppointmentStatus::Pending) - .contains(&appointment.locator)); - assert!(dbm.appointment_exists(appointment.locator)); - - // Add an invalid reference and check again - dbm.store_invalid_appointment(tower_id, &appointment) - .unwrap(); - assert!(dbm - .delete_pending_appointment(another_tower_id, appointment.locator) - .is_ok()); - assert!(!dbm - .load_appointment_locators(another_tower_id, AppointmentStatus::Pending) - .contains(&appointment.locator)); - assert!(dbm - .load_appointment_locators(tower_id, AppointmentStatus::Invalid) - .contains(&appointment.locator)); - assert!(dbm.appointment_exists(appointment.locator)); - } - - #[test] - fn test_store_invalid_appointment() { - let mut dbm = DBM::in_memory().unwrap(); - - // In order to add a tower record we need to associated registration receipt. - let tower_id = get_random_user_id(); - let net_addr = "talaia.watch"; - - let receipt = get_random_registration_receipt(); - let mut tower_summary = TowerSummary::new( - net_addr.to_owned(), - receipt.available_slots(), - receipt.subscription_start(), - receipt.subscription_expiry(), - ); - dbm.store_tower_record(tower_id, net_addr, &receipt) - .unwrap(); - - // Add some invalid appointments and check they match - for _ in 0..5 { - let appointment = generate_random_appointment(None); - - tower_summary - .invalid_appointments - .insert(appointment.locator); - - dbm.store_invalid_appointment(tower_id, &appointment) - .unwrap(); - assert_eq!( - TowerSummary::from(dbm.load_tower_record(tower_id).unwrap()), - tower_summary - ); - } - } - - #[test] - fn test_store_invalid_appointment_twice() { - let mut dbm = DBM::in_memory().unwrap(); - - // In order to add a tower record we need to associated registration receipt. - let tower_id_1 = get_random_user_id(); - let tower_id_2 = get_random_user_id(); - let net_addr = "talaia.watch"; - - let receipt = get_random_registration_receipt(); - dbm.store_tower_record(tower_id_1, net_addr, &receipt) - .unwrap(); - dbm.store_tower_record(tower_id_2, net_addr, &receipt) - .unwrap(); - - // Same as with pending appointments. Two references from different towers is allowed - let appointment = generate_random_appointment(None); - dbm.store_invalid_appointment(tower_id_1, &appointment) - .unwrap(); - dbm.store_invalid_appointment(tower_id_2, &appointment) - .unwrap(); - - // Two references from the same tower is not. - assert!(dbm - .store_invalid_appointment(tower_id_2, &appointment) - .is_err()); - } - - #[test] - fn test_store_load_misbehaving_proof() { - let mut dbm = DBM::in_memory().unwrap(); - - // In order to add a tower record we need to associated registration receipt. - let tower_id = get_random_user_id(); - let net_addr = "talaia.watch"; - - let receipt = get_random_registration_receipt(); - let tower_summary = TowerSummary::new( - net_addr.to_owned(), - receipt.available_slots(), - receipt.subscription_start(), - receipt.subscription_expiry(), - ); - dbm.store_tower_record(tower_id, net_addr, &receipt) - .unwrap(); - assert_eq!( - TowerSummary::from(dbm.load_tower_record(tower_id).unwrap()), - tower_summary - ); - - // Store a misbehaving proof and load it back - let appointment = generate_random_appointment(None); - let appointment_receipt = AppointmentReceipt::with_signature( - "user_signature".to_owned(), - 42, - "tower_signature".to_owned(), - ); - - let proof = MisbehaviorProof::new( - appointment.locator, - appointment_receipt, - get_random_user_id(), - ); - - dbm.store_misbehaving_proof(tower_id, &proof).unwrap(); - assert_eq!(dbm.load_misbehaving_proof(tower_id).unwrap(), proof); - } - - #[test] - fn test_store_load_non_existing_misbehaving_proof() { - let dbm = DBM::in_memory().unwrap(); - assert!(dbm.load_misbehaving_proof(get_random_user_id()).is_none()); - } - - #[test] - fn test_store_exists_misbehaving_proof() { - let mut dbm = DBM::in_memory().unwrap(); - - // In order to add a tower record we need to associated registration receipt. - let tower_id = get_random_user_id(); - let net_addr = "talaia.watch"; - - let receipt = get_random_registration_receipt(); - let tower_summary = TowerSummary::new( - net_addr.to_owned(), - receipt.available_slots(), - receipt.subscription_start(), - receipt.subscription_expiry(), - ); - dbm.store_tower_record(tower_id, net_addr, &receipt) - .unwrap(); - assert_eq!( - TowerSummary::from(dbm.load_tower_record(tower_id).unwrap()), - tower_summary - ); - - // // Store a misbehaving proof check - let appointment = generate_random_appointment(None); - let appointment_receipt = AppointmentReceipt::with_signature( - "user_signature".to_owned(), - 42, - "tower_signature".to_owned(), - ); - - let proof = MisbehaviorProof::new( - appointment.locator, - appointment_receipt, - get_random_user_id(), - ); - - dbm.store_misbehaving_proof(tower_id, &proof).unwrap(); - assert!(dbm.exists_misbehaving_proof(tower_id)); - } - - #[test] - fn test_exists_misbehaving_proof_false() { - let dbm = DBM::in_memory().unwrap(); - assert!(!dbm.exists_misbehaving_proof(get_random_user_id())); - } - - #[test] - fn test_store_load_client_key() { - let dbm = DBM::in_memory().unwrap(); - - assert!(dbm.load_client_key().is_none()); - for _ in 0..7 { - let sk = get_random_keypair().0; - dbm.store_client_key(&sk).unwrap(); - assert_eq!(dbm.load_client_key().unwrap(), sk); - } - } -} diff --git a/watchtower-plugin/src/lib.rs b/watchtower-plugin/src/lib.rs deleted file mode 100755 index 8f41658..0000000 --- a/watchtower-plugin/src/lib.rs +++ /dev/null @@ -1,524 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::fmt; - -use serde::Serialize; - -use teos_common::appointment::{Appointment, Locator}; -use teos_common::net::NetAddr; -use teos_common::receipts::AppointmentReceipt; -use teos_common::TowerId; - -pub mod constants; -pub mod convert; -pub mod dbm; -pub mod net; -pub mod retrier; -mod ser; -pub mod wt_client; - -#[cfg(test)] -mod test_utils; - -/// The status the tower can be found at. -#[derive(Clone, Serialize, PartialEq, Eq, Copy, Debug)] -#[serde(rename_all = "snake_case")] -pub enum TowerStatus { - Reachable, - TemporaryUnreachable, - Unreachable, - SubscriptionError, - Misbehaving, -} - -/// The status an appointment can be at. -pub enum AppointmentStatus { - Accepted, - Pending, - Invalid, -} - -/// Errors related to updating a subscription -#[derive(Debug, PartialEq, Eq)] -pub enum SubscriptionError { - Expiry, - Slots, -} - -impl SubscriptionError { - /// Whether the error is related to the expiry time or not. - pub fn is_expiry(&self) -> bool { - *self == SubscriptionError::Expiry - } -} - -impl fmt::Display for TowerStatus { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - TowerStatus::Reachable => "reachable", - TowerStatus::TemporaryUnreachable => "temporary unreachable", - TowerStatus::Unreachable => "unreachable", - TowerStatus::SubscriptionError => "subscription error", - TowerStatus::Misbehaving => "misbehaving", - } - ) - } -} - -impl TowerStatus { - /// Whether the tower is reachable or not. - pub fn is_reachable(&self) -> bool { - *self == TowerStatus::Reachable - } - - /// Whether the tower is unreachable or not. - pub fn is_temporary_unreachable(&self) -> bool { - *self == TowerStatus::TemporaryUnreachable - } - - /// Whether the tower is unreachable or not. - pub fn is_unreachable(&self) -> bool { - *self == TowerStatus::Unreachable - } - - /// Whether the tower is misbehaving or not. - pub fn is_misbehaving(&self) -> bool { - *self == TowerStatus::Misbehaving - } - - /// Whether there is a subscription issue with the tower. - pub fn is_subscription_error(&self) -> bool { - *self == TowerStatus::SubscriptionError - } - - /// Whether the tower can be manually retried - pub fn is_retryable(&self) -> bool { - self.is_unreachable() || self.is_subscription_error() - } -} - -/// Summarized data associated with a given tower. -#[derive(Clone, Serialize, Debug, PartialEq, Eq)] -pub struct TowerSummary { - #[serde(flatten)] - pub net_addr: NetAddr, - pub available_slots: u32, - subscription_start: u32, - pub subscription_expiry: u32, - pub status: TowerStatus, - #[serde(serialize_with = "teos_common::ser::serialize_locators")] - pub pending_appointments: HashSet, - #[serde(serialize_with = "teos_common::ser::serialize_locators")] - pub invalid_appointments: HashSet, -} - -impl TowerSummary { - /// Creates a new [TowerSummary] instance. - pub fn new( - net_addr: String, - available_slots: u32, - subscription_start: u32, - subscription_expiry: u32, - ) -> Self { - Self { - net_addr: NetAddr::new(net_addr), - available_slots, - subscription_start, - subscription_expiry, - status: TowerStatus::Reachable, - pending_appointments: HashSet::new(), - invalid_appointments: HashSet::new(), - } - } - - /// Creates a new instance with some associated appointment data. - pub fn with_appointments( - net_addr: String, - available_slots: u32, - subscription_start: u32, - subscription_expiry: u32, - pending_appointments: HashSet, - invalid_appointments: HashSet, - ) -> Self { - Self { - net_addr: NetAddr::new(net_addr), - available_slots, - subscription_start, - subscription_expiry, - status: TowerStatus::Reachable, - pending_appointments, - invalid_appointments, - } - } - - /// Creates a new instance using the existing info but updating the status. - pub fn with_status(mut self, status: TowerStatus) -> Self { - self.status = status; - self - } - - /// Updates the main information about the summary while preserving the appointment maps. - pub fn udpate( - &mut self, - net_addr: String, - available_slots: u32, - subscription_start: u32, - subscription_expiry: u32, - ) { - self.net_addr = NetAddr::new(net_addr); - self.available_slots = available_slots; - self.subscription_start = subscription_start; - self.subscription_expiry = subscription_expiry; - } -} - -impl From for TowerSummary { - fn from(info: TowerInfo) -> Self { - TowerSummary::with_appointments( - info.net_addr, - info.available_slots, - info.subscription_start, - info.subscription_expiry, - info.pending_appointments - .iter() - .map(|a| a.locator) - .collect(), - info.invalid_appointments - .iter() - .map(|a| a.locator) - .collect(), - ) - .with_status(info.status) - } -} - -/// Summarized data associated with a given tower. -#[derive(Clone, Serialize, Debug, PartialEq, Eq)] -pub struct TowerInfo { - pub net_addr: String, - pub available_slots: u32, - pub subscription_start: u32, - pub subscription_expiry: u32, - pub status: TowerStatus, - #[serde(serialize_with = "crate::ser::serialize_receipts")] - pub appointments: HashMap, - #[serde(serialize_with = "crate::ser::serialize_appointments")] - pub pending_appointments: Vec, - #[serde(serialize_with = "crate::ser::serialize_appointments")] - pub invalid_appointments: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub misbehaving_proof: Option, -} - -impl TowerInfo { - /// Creates a new [TowerInfo] instance. - pub fn new( - net_addr: String, - available_slots: u32, - subscription_start: u32, - subscription_expiry: u32, - appointments: HashMap, - pending_appointments: Vec, - invalid_appointments: Vec, - ) -> Self { - Self { - net_addr, - available_slots, - subscription_start, - subscription_expiry, - status: TowerStatus::Reachable, - appointments, - pending_appointments, - invalid_appointments, - misbehaving_proof: None, - } - } - - /// Creates a new instance using the existing info but updating the status. - pub fn with_status(mut self, status: TowerStatus) -> Self { - self.status = status; - self - } - - /// Sets the misbehaving proof of a tower. - pub fn set_misbehaving_proof(&mut self, proof: MisbehaviorProof) { - self.misbehaving_proof = Some(proof); - } -} - -/// A misbehaving proof. Contains proof of a tower replying with a public key different from the advertised one. -#[derive(Clone, Serialize, Debug, PartialEq, Eq)] -pub struct MisbehaviorProof { - #[serde(with = "hex::serde")] - pub locator: Locator, - pub appointment_receipt: AppointmentReceipt, - pub recovered_id: TowerId, -} - -impl MisbehaviorProof { - /// Creates a new [MisbehavingProof] instance. - pub fn new( - locator: Locator, - appointment_receipt: AppointmentReceipt, - recovered_id: TowerId, - ) -> Self { - Self { - locator, - appointment_receipt, - recovered_id, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - const STATUSES: [TowerStatus; 5] = [ - TowerStatus::Reachable, - TowerStatus::TemporaryUnreachable, - TowerStatus::Unreachable, - TowerStatus::SubscriptionError, - TowerStatus::Misbehaving, - ]; - - const AVAILABLE_SLOTS: u32 = 21; - const SUBSCRIPTION_START: u32 = 100; - const SUBSCRIPTION_EXPIRY: u32 = SUBSCRIPTION_START + 42; - - mod tower_status { - use super::*; - use TowerStatus::*; - - #[test] - fn test_is_reachable() { - for status in STATUSES { - if status == Reachable { - assert!(status.is_reachable()) - } else { - assert!(!status.is_reachable()); - } - } - } - - #[test] - fn test_is_temporary_reachable() { - for status in STATUSES { - if status == TemporaryUnreachable { - assert!(status.is_temporary_unreachable()) - } else { - assert!(!status.is_temporary_unreachable()); - } - } - } - - #[test] - fn test_is_unreachable() { - for status in STATUSES { - if status == Unreachable { - assert!(status.is_unreachable()) - } else { - assert!(!status.is_unreachable()); - } - } - } - - #[test] - fn test_is_misbehaving() { - for status in STATUSES { - if status == Misbehaving { - assert!(status.is_misbehaving()) - } else { - assert!(!status.is_misbehaving()); - } - } - } - - #[test] - fn test_is_subscription_error() { - for status in STATUSES { - if status == SubscriptionError { - assert!(status.is_subscription_error()) - } else { - assert!(!status.is_subscription_error()); - } - } - } - - #[test] - fn test_is_retryable() { - for status in STATUSES { - if status == Unreachable || status == SubscriptionError { - assert!(status.is_retryable()) - } else { - assert!(!status.is_retryable()); - } - } - } - } - - mod tower_summary { - use super::*; - - use std::iter::FromIterator; - - use teos_common::test_utils::generate_random_appointment; - - impl TowerSummary { - pub fn set_net_addr(&mut self, net_addr: String) { - self.net_addr = NetAddr::new(net_addr); - } - } - - #[test] - fn test_new() { - let net_addr: String = "addr".to_owned(); - - let tower_summary = TowerSummary::new( - net_addr.clone(), - AVAILABLE_SLOTS, - SUBSCRIPTION_START, - SUBSCRIPTION_EXPIRY, - ); - assert_eq!( - tower_summary, - TowerSummary { - net_addr: NetAddr::new(net_addr), - available_slots: AVAILABLE_SLOTS, - subscription_start: SUBSCRIPTION_START, - subscription_expiry: SUBSCRIPTION_EXPIRY, - status: TowerStatus::Reachable, - pending_appointments: HashSet::new(), - invalid_appointments: HashSet::new(), - }, - ); - } - - #[test] - fn test_with_appointments() { - let net_addr: String = "addr".to_owned(); - - let pending_appointments = - HashSet::from_iter([generate_random_appointment(None).locator]); - let invalid_appointments = - HashSet::from_iter([generate_random_appointment(None).locator]); - - let tower_summary = TowerSummary::with_appointments( - net_addr.clone(), - AVAILABLE_SLOTS, - SUBSCRIPTION_START, - SUBSCRIPTION_EXPIRY, - pending_appointments.clone(), - invalid_appointments.clone(), - ); - assert_eq!( - tower_summary, - TowerSummary { - net_addr: NetAddr::new(net_addr), - available_slots: AVAILABLE_SLOTS, - subscription_start: SUBSCRIPTION_START, - subscription_expiry: SUBSCRIPTION_EXPIRY, - status: TowerStatus::Reachable, - pending_appointments, - invalid_appointments, - }, - ); - } - - #[test] - fn test_with_status() { - let mut tower_summary = TowerSummary::new( - "addr".to_owned(), - AVAILABLE_SLOTS, - SUBSCRIPTION_START, - SUBSCRIPTION_EXPIRY, - ); - - let unreachable_tower = tower_summary.clone().with_status(TowerStatus::Unreachable); - tower_summary.status = TowerStatus::Unreachable; - assert_eq!(unreachable_tower, tower_summary); - } - } - - mod tower_info { - use super::*; - - use teos_common::test_utils::{generate_random_appointment, get_random_user_id}; - - impl TowerInfo { - pub fn empty( - net_addr: String, - available_slots: u32, - subscription_start: u32, - subscription_expiry: u32, - ) -> Self { - TowerInfo::new( - net_addr, - available_slots, - subscription_start, - subscription_expiry, - HashMap::new(), - Vec::new(), - Vec::new(), - ) - } - } - - #[test] - fn test_new() { - let tower_info = TowerInfo::new( - "addr".to_owned(), - AVAILABLE_SLOTS, - SUBSCRIPTION_START, - SUBSCRIPTION_EXPIRY, - HashMap::new(), - Vec::new(), - Vec::new(), - ); - - assert!(tower_info.status.is_reachable()); - assert!(tower_info.misbehaving_proof.is_none()); - } - - #[test] - fn test_with_status() { - let mut tower_info = TowerInfo::empty( - "addr".to_owned(), - AVAILABLE_SLOTS, - SUBSCRIPTION_START, - SUBSCRIPTION_EXPIRY, - ); - - let unreachable_tower = tower_info.clone().with_status(TowerStatus::Unreachable); - tower_info.status = TowerStatus::Unreachable; - assert_eq!(unreachable_tower, tower_info); - } - - #[test] - fn test_set_misbehaving_proof() { - let mut tower_info = TowerInfo::empty( - "addr".to_owned(), - AVAILABLE_SLOTS, - SUBSCRIPTION_START, - SUBSCRIPTION_EXPIRY, - ); - assert_eq!(tower_info.misbehaving_proof, None); - - let appointment_receipt = AppointmentReceipt::with_signature( - "user_signature".to_owned(), - SUBSCRIPTION_START + 1, - "tower_signature".to_owned(), - ); - let proof = MisbehaviorProof::new( - generate_random_appointment(None).locator, - appointment_receipt, - get_random_user_id(), - ); - - tower_info.set_misbehaving_proof(proof.clone()); - assert_eq!(tower_info.misbehaving_proof, Some(proof)); - } - } -} diff --git a/watchtower-plugin/src/main.rs b/watchtower-plugin/src/main.rs deleted file mode 100755 index a344084..0000000 --- a/watchtower-plugin/src/main.rs +++ /dev/null @@ -1,670 +0,0 @@ -use std::convert::TryFrom; -use std::env; -use std::path::PathBuf; -use std::sync::{Arc, Mutex, MutexGuard}; - -use home::home_dir; -use serde_json::json; -use tokio::io::{stdin, stdout}; -use tokio::sync::mpsc::unbounded_channel; - -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}; -use teos_common::net::http::Endpoint; -use teos_common::net::NetAddr; -use teos_common::protos as common_msgs; -use teos_common::TowerId; -use teos_common::{cryptography, errors}; - -use watchtower_plugin::convert::{CommitmentRevocation, GetAppointmentParams, RegisterParams}; -use watchtower_plugin::net::http::{ - 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), - RequestError::DeserializeError(e) => anyhow!(e), - RequestError::Unexpected(e) => anyhow!(e), - }; - log::info!("{e}"); - e -} - -/// Sends fresh data to a retrier as long as is does not exist, or it does and its running. -fn send_to_retrier(state: &MutexGuard, tower_id: TowerId, locator: Locator) { - if if let Some(status) = state.get_retrier_status(&tower_id) { - // A retrier in the retriers map can only be running or idle - status.is_running() - } else { - true - } { - state - .unreachable_towers - .send((tower_id, RevocationData::Fresh(locator))) - .unwrap(); - } else { - log::debug!("Not sending data to idle retrier ({tower_id}, {locator})") - } -} - -/// Registers the client to a given tower. -/// -/// Accepted tower_id formats: -/// - tower_id@host:port -/// - tower_id host port -/// - tower_id@host (will default port to DEFAULT_PORT) -/// - tower_id host (will default port to DEFAULT_PORT) -async fn register( - plugin: Plugin>>, - v: serde_json::Value, -) -> Result { - let params = RegisterParams::try_from(v).map_err(|x| anyhow!(x))?; - let mut host = params.host.unwrap_or_else(|| "localhost".to_owned()); - let tower_id = params.tower_id; - let user_id = plugin.state().lock().unwrap().user_id; - - // TODO: The user should pick the start_time or, at least, check the returned start time against it's known block height. - // Otherwise the tower could just generate a subscription starting far in the future. For this we need to access lightning RPC - // 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(&WT_PORT_CONFG).unwrap()) - .map_err(|_| anyhow!("{} out of range", constants::WT_PORT))?, - ); - - let tower_net_addr = { - if !host.starts_with("http://") && !host.starts_with("https://") { - host = format!("http://{host}") - } - NetAddr::new(format!("{host}:{port}")) - }; - - let proxy = plugin.state().lock().unwrap().proxy.clone(); - - let receipt = http::register(tower_id, user_id, &tower_net_addr, &proxy) - .await - .map_err(|e| { - let mut state = plugin.state().lock().unwrap(); - if e.is_connection() && state.towers.contains_key(&tower_id) { - state.set_tower_status(tower_id, TowerStatus::TemporaryUnreachable); - } - to_cln_error(e) - })?; - - if !receipt.verify(&tower_id) { - return Err(anyhow!( - "Registration receipt contains bad signature. Are you using the right tower_id?" - )); - } - - plugin - .state() - .lock() - .unwrap() - .add_update_tower(tower_id, tower_net_addr.net_addr(), &receipt).map_err(|e| { - if e.is_expiry() { - anyhow!("Registration receipt contains a subscription expiry that is not higher than the one we are currently registered for") - } else { - anyhow!("Registration receipt does not contain more slots than the ones we are currently registered for") - } - })?; - - log::info!( - "Registration succeeded. Available slots: {}. Subscription period (block height range): ({}-{})", - receipt.available_slots(), - receipt.subscription_start(), - receipt.subscription_expiry() - ); - - Ok(json!(receipt)) -} - -/// Gets the latest registration receipt from the client to a given tower (if it exists). -/// -/// This is pulled from the database -async fn get_registration_receipt( - plugin: Plugin>>, - v: serde_json::Value, -) -> Result { - let tower_id = TowerId::try_from(v).map_err(|x| anyhow!(x))?; - let state = plugin.state().lock().unwrap(); - - 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. -async fn get_subscription_info( - plugin: Plugin>>, - v: serde_json::Value, -) -> Result { - let tower_id = TowerId::try_from(v).map_err(|x| anyhow!(x))?; - - let (user_sk, tower_net_addr, proxy) = { - let state = plugin.state().lock().unwrap(); - if let Some(info) = state.towers.get(&tower_id) { - Ok((state.user_sk, info.net_addr.clone(), state.proxy.clone())) - } else { - Err(anyhow!("Unknown tower id: {tower_id}")) - } - }?; - - let signature = cryptography::sign("get subscription info".as_bytes(), &user_sk); - - let response: common_msgs::GetSubscriptionInfoResponse = process_post_response( - post_request( - &tower_net_addr, - Endpoint::GetSubscriptionInfo, - &common_msgs::GetSubscriptionInfoRequest { signature }, - &proxy, - ) - .await, - ) - .await - .map_err(|e| { - if e.is_connection() { - plugin - .state() - .lock() - .unwrap() - .set_tower_status(tower_id, TowerStatus::TemporaryUnreachable); - } - to_cln_error(e) - })?; - - Ok(json!(response)) -} - -/// Gets information about an appointment from the tower. -async fn get_appointment( - plugin: Plugin>>, - v: serde_json::Value, -) -> Result { - let params = GetAppointmentParams::try_from(v).map_err(|x| anyhow!(x))?; - - let (user_sk, tower_net_addr, proxy) = { - let state = plugin.state().lock().unwrap(); - if let Some(info) = state.towers.get(¶ms.tower_id) { - Ok((state.user_sk, info.net_addr.clone(), state.proxy.clone())) - } else { - Err(anyhow!("Unknown tower id: {}", params.tower_id)) - } - }?; - - let signature = cryptography::sign( - format!("get appointment {}", params.locator).as_bytes(), - &user_sk, - ); - - let response: ApiResponse = process_post_response( - post_request( - &tower_net_addr, - Endpoint::GetAppointment, - &common_msgs::GetAppointmentRequest { - locator: params.locator.to_vec(), - signature, - }, - &proxy, - ) - .await, - ) - .await - .map_err(|e| { - if e.is_connection() { - plugin - .state() - .lock() - .unwrap() - .set_tower_status(params.tower_id, TowerStatus::TemporaryUnreachable); - } - to_cln_error(e) - })?; - - Ok(json!(response)) -} - -/// Gets an appointment receipt from the client given a tower_id and a locator (if it exists). -/// -/// This is pulled from the database -async fn get_appointment_receipt( - plugin: Plugin>>, - v: serde_json::Value, -) -> Result { - let params = GetAppointmentParams::try_from(v).map_err(|x| anyhow!(x))?; - let state = plugin.state().lock().unwrap(); - - 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. -/// -/// The given information comes from memory, so it is summarized. -async fn list_towers( - plugin: Plugin>>, - _: serde_json::Value, -) -> Result { - Ok(json!(plugin.state().lock().unwrap().towers)) -} - -/// Gets information about a given tower. -/// -/// Data comes from disk (DB), so all stored data is provided. -async fn get_tower_info( - plugin: Plugin>>, - v: serde_json::Value, -) -> Result { - let state = plugin.state().lock().unwrap(); - let tower_id = TowerId::try_from(v).map_err(|e| anyhow!(e))?; - - 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. -/// -/// Only works if the tower is unreachable or there's been a subscription error (and the tower is not already being retried). -async fn retry_tower( - plugin: Plugin>>, - v: serde_json::Value, -) -> Result { - let tower_id = TowerId::try_from(v).map_err(|e| anyhow!(e))?; - let state = plugin.state().lock().unwrap(); - if let Some(tower_status) = state.get_tower_status(&tower_id) { - if let Some(retrier_status) = state.retriers.get(&tower_id) { - if retrier_status.is_idle() { - // We don't send any associated data in this case given the idle retrier already has it all. - state - .unreachable_towers - .send((tower_id, RevocationData::None)) - .map_err(|e| anyhow!(e))?; - } else { - // Status can only be running or idle for data in the retriers map. - return Err(anyhow!("{tower_id} is already being retried")); - } - } else if tower_status.is_retryable() { - // We do send associated data here given there is no retrier associated to this tower. - state - .unreachable_towers - .send(( - tower_id, - RevocationData::Stale( - state - .towers - .get(&tower_id) - .unwrap() - .pending_appointments - .iter() - .cloned() - .collect(), - ), - )) - .map_err(|e| anyhow!(e))?; - } else { - return Err(anyhow!( - "Tower status must be unreachable or have a subscription issue to manually retry", - )); - } - } else { - return Err(anyhow!("Unknown tower {tower_id}")); - } - Ok(json!(format!("Retrying {tower_id}"))) -} - -/// Forgets about a tower wiping out all local data associated to it. -async fn abandon_tower( - plugin: Plugin>>, - v: serde_json::Value, -) -> Result { - let tower_id = TowerId::try_from(v).map_err(|e| anyhow!(e))?; - let mut state = plugin.state().lock().unwrap(); - if state.towers.contains_key(&tower_id) { - state.remove_tower(tower_id).unwrap(); - Ok(json!(format!("{tower_id} successfully abandoned"))) - } else { - Err(anyhow!("Unknown tower {tower_id}")) - } -} - -/// Sends an appointment to all registered towers for every new commitment transaction. -/// -/// The appointment is built using the data provided by the backend (dispute txid and penalty transaction). -async fn on_commitment_revocation( - plugin: Plugin>>, - v: serde_json::Value, -) -> Result { - let commitment_revocation = serde_json::from_value::(v) - .map_err(|e| anyhow!("Cannot decode commitment_revocation data. Error: {e}"))?; - log::debug!( - "New commitment revocation received for channel {}. Commit number {}", - commitment_revocation.channel_id, - commitment_revocation.commit_num - ); - - // TODO: For now, to_self_delay is hardcoded to 42. Revisit and define it better / remove it when / if needed - let locator = Locator::new(commitment_revocation.commitment_txid); - let appointment = Appointment::new( - locator, - cryptography::encrypt( - &commitment_revocation.penalty_tx, - &commitment_revocation.commitment_txid, - ) - .unwrap(), - 42, - ); - let signature = cryptography::sign( - &appointment.to_vec(), - &plugin.state().lock().unwrap().user_sk, - ); - - // 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. - let towers = plugin - .state() - .lock() - .unwrap() - .towers - .iter() - .map(|(id, info)| (*id, info.net_addr.clone(), info.status)) - .collect::>(); - - let proxy = plugin.state().lock().unwrap().proxy.clone(); - - for (tower_id, net_addr, status) in towers { - if status.is_reachable() { - match http::add_appointment(tower_id, &net_addr, &proxy, &appointment, &signature).await - { - Ok((slots, receipt)) => { - plugin - .state() - .lock() - .unwrap() - .add_appointment_receipt(tower_id, locator, slots, &receipt); - log::debug!("Response verified and data stored in the database"); - } - Err(e) => match e { - AddAppointmentError::RequestError(e) => { - if e.is_connection() { - log::warn!( - "{tower_id} cannot be reached. Adding {} to pending appointments", - appointment.locator - ); - let mut state = plugin.state().lock().unwrap(); - state.set_tower_status(tower_id, TowerStatus::TemporaryUnreachable); - state.add_pending_appointment(tower_id, &appointment); - send_to_retrier(&state, tower_id, appointment.locator); - } - } - AddAppointmentError::ApiError(e) => match e.error_code { - errors::INVALID_SIGNATURE_OR_SUBSCRIPTION_ERROR => { - log::warn!( - "There is a subscription issue with {tower_id}. Adding {} to pending", - appointment.locator - ); - let mut state = plugin.state().lock().unwrap(); - state.set_tower_status(tower_id, TowerStatus::SubscriptionError); - state.add_pending_appointment(tower_id, &appointment); - send_to_retrier(&state, tower_id, appointment.locator); - } - - _ => { - log::warn!( - "{tower_id} rejected the appointment. Error: {}, error_code: {}", - e.error, - e.error_code - ); - plugin - .state() - .lock() - .unwrap() - .add_invalid_appointment(tower_id, &appointment); - } - }, - AddAppointmentError::SignatureError(proof) => { - log::warn!("Cannot recover known tower_id from the appointment receipt. Flagging tower as misbehaving"); - plugin - .state() - .lock() - .unwrap() - .flag_misbehaving_tower(tower_id, proof) - } - }, - }; - } else if status.is_misbehaving() { - log::warn!("{tower_id} is misbehaving. Not sending any further appointments",); - } else { - if status.is_subscription_error() { - log::warn!( - "There is a subscription issue with {tower_id}. Adding {} to pending", - appointment.locator - ); - } else { - log::warn!( - "{tower_id} is {status}. Adding {} to pending", - appointment.locator, - ); - } - - let mut state = plugin.state().lock().unwrap(); - state.add_pending_appointment(tower_id, &appointment); - - if !status.is_unreachable() { - send_to_retrier(&state, tower_id, appointment.locator); - } - } - } - - // FIXME: Ask cdecker: Do hooks need to return something? - Ok(json!(r#" {"result": continue}"#)) -} - -#[tokio::main] -async fn main() -> Result<(), Error> { - let data_dir = match env::var(constants::TOWERS_DATA_DIR) { - Ok(v) => PathBuf::from(v), - Err(_) => home_dir().unwrap().join(constants::DEFAULT_TOWERS_DATA_DIR), - }; - - let builder = Builder::new(stdin(), stdout()) - .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, - register, - ) - .rpcmethod( - constants::RPC_GET_REGISTRATION_RECEIPT, - constants::RPC_GET_REGISTRATION_RECEIPT_DESC, - get_registration_receipt, - ) - .rpcmethod( - constants::RPC_GET_APPOINTMENT, - constants::RPC_GET_APPOINTMENT_DESC, - get_appointment, - ) - .rpcmethod( - constants::RPC_GET_APPOINTMENT_RECEIPT, - constants::RPC_GET_APPOINTMENT_RECEIPT_DESC, - get_appointment_receipt, - ) - .rpcmethod( - constants::RPC_GET_SUBSCRIPTION_INFO, - constants::RPC_GET_SUBSCRIPTION_INFO_DESC, - get_subscription_info, - ) - .rpcmethod( - constants::RPC_LIST_TOWERS, - constants::RPC_LIST_TOWERS_DESC, - list_towers, - ) - .rpcmethod( - constants::RPC_GET_TOWER_INFO, - 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, - retry_tower, - ) - .rpcmethod( - constants::RPC_ABANDON_TOWER, - constants::RPC_ABANDON_TOWER_DESC, - abandon_tower, - ) - .hook( - constants::HOOK_COMMITMENT_REVOCATION, - on_commitment_revocation, - ); - - // We're unwrapping here given it does not seem we actually have anything to check at the moment. - // Change this so the plugin can be disabled soon if this happens not to be the case. - let midstate = if let Some(midstate) = builder.configure().await? { - midstate - } else { - return Ok(()); - }; - - let (tx, rx) = unbounded_channel(); - let wt_client = Arc::new(Mutex::new( - WTClient::with_proxy( - data_dir, - tx, - midstate.configuration().proxy.map(|proxy| { - // We don't need to inform `always-use-proxy` needing `proxy` to work. This is done by CLN already when needed. - ProxyInfo::new( - proxy, - midstate.configuration().always_use_proxy.unwrap_or(false), - ) - }), - ) - .await, - )); - - 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(&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(&DEV_WT_MAX_RETRY_INTERVAL_CONFIG).unwrap(), - ) - .inspect_err(|_| { - log::error!("{} out of range", constants::DEV_WT_MAX_RETRY_INTERVAL); - })?; - - let plugin = midstate.start(wt_client.clone()).await?; - tokio::spawn(async move { - RetryManager::new( - wt_client, - rx, - max_elapsed_time, - auto_retry_delay, - max_interval_time, - ) - .manage_retry() - .await - }); - plugin.join().await -} diff --git a/watchtower-plugin/src/net/http.rs b/watchtower-plugin/src/net/http.rs deleted file mode 100644 index 1a93633..0000000 --- a/watchtower-plugin/src/net/http.rs +++ /dev/null @@ -1,687 +0,0 @@ -use reqwest::{Method, Response}; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; - -use teos_common::appointment::Appointment; -use teos_common::cryptography; -use teos_common::net::http::Endpoint; -use teos_common::net::NetAddr; -use teos_common::protos as common_msgs; -use teos_common::receipts::{AppointmentReceipt, RegistrationReceipt}; -use teos_common::{TowerId, UserId}; - -use crate::net::ProxyInfo; -use crate::MisbehaviorProof; - -/// Represents a generic api response. -#[derive(Serialize, Deserialize, Debug)] -#[serde(untagged)] -pub enum ApiResponse { - Response(T), - Error(ApiError), -} - -/// API errors that can be received when interacting with the tower. Error codes match `teos_common::errors`. -#[derive(Serialize, Deserialize, Debug)] -pub struct ApiError { - pub error: String, - pub error_code: u8, -} - -/// Errors related to requests sent to the tower. -#[derive(Debug, PartialEq, Eq)] -pub enum RequestError { - ConnectionError(String), - DeserializeError(String), - Unexpected(String), -} - -impl RequestError { - pub fn is_connection(&self) -> bool { - matches!(self, RequestError::ConnectionError(_)) - } -} - -/// Errors related to the `add_appointment` requests to the tower. -#[derive(Debug)] -pub enum AddAppointmentError { - RequestError(RequestError), - ApiError(ApiError), - SignatureError(MisbehaviorProof), -} - -impl From for AddAppointmentError { - fn from(r: RequestError) -> Self { - AddAppointmentError::RequestError(r) - } -} - -/// Handles the logic of interacting with the `register` endpoint of the tower. -pub async fn register( - tower_id: TowerId, - user_id: UserId, - tower_net_addr: &NetAddr, - proxy: &Option, -) -> Result { - log::info!("Registering in the Eye of Satoshi (tower_id={tower_id})"); - process_post_response( - post_request( - tower_net_addr, - Endpoint::Register, - &common_msgs::RegisterRequest { - user_id: user_id.to_vec(), - }, - proxy, - ) - .await, - ) - .await - .map(|r: common_msgs::RegisterResponse| { - RegistrationReceipt::with_signature( - user_id, - r.available_slots, - r.subscription_start, - r.subscription_expiry, - r.subscription_signature, - ) - }) -} - -/// Encapsulates the logging and response parsing of sending and appointment to the tower. -pub async fn add_appointment( - tower_id: TowerId, - tower_net_addr: &NetAddr, - proxy: &Option, - appointment: &Appointment, - signature: &str, -) -> Result<(u32, AppointmentReceipt), AddAppointmentError> { - log::debug!( - "Sending appointment {} to tower {tower_id}", - appointment.locator - ); - let (response, receipt) = - send_appointment(tower_id, tower_net_addr, proxy, appointment, signature).await?; - log::debug!("Appointment accepted and signed by {tower_id}"); - log::debug!("Remaining slots: {}", response.available_slots); - log::debug!("Start block: {}", response.start_block); - - Ok((response.available_slots, receipt)) -} - -/// Handles the logic of interacting with the `add_appointment` endpoint of the tower. -pub async fn send_appointment( - tower_id: TowerId, - tower_net_addr: &NetAddr, - proxy: &Option, - appointment: &Appointment, - signature: &str, -) -> Result<(common_msgs::AddAppointmentResponse, AppointmentReceipt), AddAppointmentError> { - let request_data = common_msgs::AddAppointmentRequest { - appointment: Some(appointment.clone().into()), - signature: signature.to_owned(), - }; - - match process_post_response( - post_request( - tower_net_addr, - Endpoint::AddAppointment, - &request_data, - proxy, - ) - .await, - ) - .await? - { - ApiResponse::Response::(r) => { - let receipt = AppointmentReceipt::with_signature( - signature.to_owned(), - r.start_block, - r.signature.clone(), - ); - let recovered_id = TowerId( - cryptography::recover_pk(&receipt.to_vec(), &receipt.signature().unwrap()).unwrap(), - ); - if recovered_id == tower_id { - Ok((r, receipt)) - } else { - Err(AddAppointmentError::SignatureError(MisbehaviorProof::new( - appointment.locator, - receipt, - recovered_id, - ))) - } - } - ApiResponse::Error(e) => Err(AddAppointmentError::ApiError(e)), - } -} - -/// A generic function to send a request to a tower. -async fn request( - tower_net_addr: &NetAddr, - endpoint: Endpoint, - proxy: &Option, - method: Method, - data: Option, -) -> Result { - let client = if let Some(proxy) = proxy { - if proxy.always_use || tower_net_addr.is_onion() { - reqwest::Client::builder() - .proxy( - reqwest::Proxy::http(proxy.get_socks_addr()) - .map_err(|e| RequestError::ConnectionError(format!("{e}")))?, - ) - .build() - .map_err(|e| RequestError::ConnectionError(format!("{e}")))? - } else { - reqwest::Client::new() - } - } else { - // If there is no proxy we only build the client as long as the address is not onion - if tower_net_addr.is_onion() { - return Err(RequestError::ConnectionError( - "Cannot connect to an onion address without a proxy".to_owned(), - )); - } - reqwest::Client::new() - }; - - 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. -pub async fn process_post_response( - post_request: Result, -) -> Result { - // TODO: Check if this can be switched for a map. Not sure how to handle async with maps - match post_request { - Ok(r) => r.json().await.map_err(|e| { - RequestError::DeserializeError(format!("Unexpected response body. Error: {e}")) - }), - Err(e) => Err(e), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - use crate::test_utils::get_dummy_add_appointment_response; - use teos_common::test_utils::{ - generate_random_appointment, get_random_appointment_receipt, - get_random_registration_receipt, get_random_user_id, - }; - - mod request_error { - use super::*; - - #[test] - fn test_is_connection() { - let error_message = "error_msg"; - for error in [ - RequestError::ConnectionError(error_message.to_owned()), - RequestError::DeserializeError(error_message.to_owned()), - RequestError::Unexpected(error_message.to_owned()), - ] { - if error == RequestError::ConnectionError(error_message.to_owned()) { - assert!(error.is_connection()) - } else { - assert!(!error.is_connection()) - } - } - } - } - - #[tokio::test] - async fn test_register() { - let (tower_sk, tower_pk) = cryptography::get_random_keypair(); - let mut registration_receipt = get_random_registration_receipt(); - registration_receipt.sign(&tower_sk); - - 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.url()), - &None, - ) - .await - .unwrap(); - - api_mock.assert_async().await; - assert_eq!(receipt, registration_receipt); - } - - #[tokio::test] - async fn test_register_connection_error() { - let error = register( - get_random_user_id(), - get_random_user_id(), - &NetAddr::new("http://server_addr".to_owned()), - &None, - ) - .await - .unwrap_err(); - - assert!(matches!(error, RequestError::ConnectionError { .. })) - } - - #[tokio::test] - async fn test_register_deserialize_error() { - 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.url()), - &None, - ) - .await - .unwrap_err(); - - api_mock.assert_async().await; - assert!(matches!(error, RequestError::DeserializeError { .. })) - } - - #[tokio::test] - async fn test_add_appointment() { - // `add_appointment` is basically a pass trough function for `send_appointment` with some logging and a parse of the outputs - // in case there are no errors. All the error cases will be tested in `send_appointment`. - let (tower_sk, tower_pk) = cryptography::get_random_keypair(); - let appointment = generate_random_appointment(None); - - let appointment_receipt = get_random_appointment_receipt(tower_sk); - let add_appointment_response = - get_dummy_add_appointment_response(appointment.locator, &appointment_receipt); - - 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.url()), - &None, - &appointment, - appointment_receipt.user_signature(), - ) - .await - .unwrap(); - - api_mock.assert_async().await; - assert_eq!(response, add_appointment_response.available_slots); - assert_eq!(receipt, appointment_receipt); - } - - #[tokio::test] - async fn test_send_appointment() { - let (tower_sk, tower_pk) = cryptography::get_random_keypair(); - let appointment = generate_random_appointment(None); - - let appointment_receipt = get_random_appointment_receipt(tower_sk); - let add_appointment_response = - get_dummy_add_appointment_response(appointment.locator, &appointment_receipt); - - 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.url()), - &None, - &appointment, - appointment_receipt.user_signature(), - ) - .await - .unwrap(); - - api_mock.assert_async().await; - assert_eq!(response, add_appointment_response); - assert_eq!(receipt, appointment_receipt); - } - - #[tokio::test] - async fn test_send_appointment_misbehaving() { - let (sybil_tower_sk, sibyl_tower_pk) = cryptography::get_random_keypair(); - let appointment = generate_random_appointment(None); - - let appointment_receipt = get_random_appointment_receipt(sybil_tower_sk); - let add_appointment_response = - get_dummy_add_appointment_response(appointment.locator, &appointment_receipt); - - 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.url()), - &None, - &appointment, - appointment_receipt.user_signature(), - ) - .await - .unwrap_err(); - - api_mock.assert_async().await; - if let AddAppointmentError::SignatureError(proof) = error { - assert_eq!( - MisbehaviorProof::new( - appointment.locator, - appointment_receipt, - TowerId(sibyl_tower_pk) - ), - proof - ) - } else { - panic!("SignatureError was expected") - } - } - - #[tokio::test] - async fn test_send_appointment_connection_error() { - let error = send_appointment( - get_random_user_id(), - &NetAddr::new("http://server_addr".to_owned()), - &None, - &generate_random_appointment(None), - "user_sig", - ) - .await - .unwrap_err(); - - if let AddAppointmentError::RequestError(e) = error { - assert!(matches!(e, RequestError::ConnectionError { .. })) - } else { - panic!("ConnectionError was expected") - } - } - - #[tokio::test] - async fn test_send_appointment_deserialize_error() { - 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.url()), - &None, - &generate_random_appointment(None), - "user_sig", - ) - .await - .unwrap_err(); - - api_mock.assert_async().await; - if let AddAppointmentError::RequestError(e) = error { - assert!(matches!(e, RequestError::DeserializeError { .. })) - } else { - panic!("DeserializeError was expected") - } - } - - #[tokio::test] - async fn test_send_appointment_api_error() { - let api_error = ApiError { - error: "error_msg".to_owned(), - error_code: 1, - }; - - 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.url()), - &None, - &generate_random_appointment(None), - "user_sig", - ) - .await - .unwrap_err(); - - 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 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.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_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 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.url()), - Endpoint::GetAppointment, - json!(""), - &None, - ) - .await, - ) - .await - .unwrap_err(); - - api_mock.assert_async().await; - assert!(matches!(error, RequestError::DeserializeError { .. })); - } -} diff --git a/watchtower-plugin/src/net/mod.rs b/watchtower-plugin/src/net/mod.rs deleted file mode 100644 index cdd2805..0000000 --- a/watchtower-plugin/src/net/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -use cln_plugin::messages; -use serde::Deserialize; -pub mod http; - -#[derive(Clone, Debug, Deserialize)] -pub struct ProxyInfo { - #[serde(flatten)] - /// The proxy data - inner: messages::ProxyInfo, - /// Whether to only send data though Tor or not - pub always_use: bool, -} - -impl ProxyInfo { - pub fn new(proxy: messages::ProxyInfo, always_use: bool) -> Self { - Self { - inner: proxy, - always_use, - } - } - - pub fn get_socks_addr(&self) -> String { - format!("socks5h://{}:{}", self.inner.address, self.inner.port) - } -} diff --git a/watchtower-plugin/src/retrier.rs b/watchtower-plugin/src/retrier.rs deleted file mode 100644 index e89d4e5..0000000 --- a/watchtower-plugin/src/retrier.rs +++ /dev/null @@ -1,1640 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::fmt::Display; -use std::sync::{Arc, Mutex}; -use std::time::{Duration, Instant}; -use tokio::sync::mpsc::{error::TryRecvError, UnboundedReceiver}; - -use backoff::future::retry_notify; -use backoff::{Error, ExponentialBackoff}; - -use teos_common::appointment::Locator; -use teos_common::cryptography; -use teos_common::errors; -use teos_common::UserId as TowerId; - -use crate::net::http::{self, AddAppointmentError}; -use crate::wt_client::{RevocationData, WTClient}; -use crate::{MisbehaviorProof, TowerStatus}; - -const POLLING_TIME: u64 = 1; - -#[derive(Eq, PartialEq, Debug)] -enum RetryError { - // bool marks whether the Subscription error is permanent or not - Subscription(String, bool), - Unreachable, - Misbehaving(MisbehaviorProof), - Abandoned, -} - -impl Display for RetryError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - RetryError::Subscription(r, _) => write!(f, "{r}"), - RetryError::Unreachable => write!(f, "Tower cannot be reached"), - RetryError::Misbehaving(_) => write!(f, "Tower misbehaved"), - RetryError::Abandoned => write!(f, "Tower was abandoned. Skipping retry"), - } - } -} - -impl RetryError { - fn is_permanent(&self) -> bool { - matches!( - self, - RetryError::Subscription(_, true) | RetryError::Misbehaving(_) | RetryError::Abandoned - ) - } -} - -pub struct RetryManager { - wt_client: Arc>, - unreachable_towers: UnboundedReceiver<(TowerId, RevocationData)>, - max_elapsed_time_secs: u16, - auto_retry_delay: u32, - max_interval_time_secs: u16, - retriers: HashMap>, -} - -impl RetryManager { - pub fn new( - wt_client: Arc>, - unreachable_towers: UnboundedReceiver<(TowerId, RevocationData)>, - max_elapsed_time_secs: u16, - auto_retry_delay: u32, - max_interval_time_secs: u16, - ) -> Self { - RetryManager { - wt_client, - unreachable_towers, - max_elapsed_time_secs, - auto_retry_delay, - max_interval_time_secs, - retriers: HashMap::new(), - } - } - - /// Starts the retry manager's main logic loop. - /// This method will keep running until the `unreachable_towers` sender disconnects. - /// - /// It will receive a `(tower_id, revocation_data)` pair and try to send all the appointments contained - /// in `revocation_data` (identified by `locator`) to the tower with `tower_id`. This is done by spawning - /// a tokio thread for each `tower_id` that tries to send all the pending appointments. - /// - /// The content of [RevocationData] will depend on who called `unreachable_towers.send`: - /// - If it was called by `on_commitment_revocation`, the data will be fresh and contain a single locator - /// - If it was called by the [WTClient] constructor, or by manually retrying, then the data will the stale - /// and contain a `HashSet` with, potentially, many locators. - pub async fn manage_retry(&mut self) { - log::info!("Starting retry manager"); - - loop { - match self.unreachable_towers.try_recv() { - Ok((tower_id, data)) => { - // Not start a retry if the tower is flagged to be abandoned - if !self - .wt_client - .lock() - .unwrap() - .towers - .contains_key(&tower_id) - { - log::info!("Skipping retrying abandoned tower {tower_id}"); - } 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 retrier. This should have never happened. Please report! ({data:?})"); - continue; - } - log::info!( - "Manually finished idling. Flagging {} for retry", - retrier.tower_id - ); - // While a retrier is idle data is not kept in memory. - // Load the pending appointments from the DB and feed them to the retrier - retrier.set_status(RetrierStatus::Stopped); - retrier.pending_appointments.lock().unwrap().extend( - self.wt_client - .lock() - .unwrap() - .dbm - .load_appointment_locators( - retrier.tower_id, - crate::AppointmentStatus::Pending, - ), - ); - } else { - self.add_pending_appointments(tower_id, data.into()); - } - } else { - self.add_pending_appointments(tower_id, data.into()); - } - } - Err(TryRecvError::Empty) => { - // Keep only running retriers and retriers ready to be started/re-started. - // This will remove failed ones and ones finished successfully and have no pending appointments. - // - // Note that a failed retrier could have received some new appointments to retry. In this case, we don't try to send - // them because we know that that tower is unreachable. We most likely received these new appointments while the tower - // was still flagged as temporarily unreachable when cleaning up after giving up retrying. - self.retriers.retain(|_, retrier| { - retrier.remove_if_failed(); - retrier.should_start() || retrier.is_running() || retrier.is_idle() - }); - // Start all the ready retriers. - for retrier in self.retriers.values() { - if retrier.should_start() { - self.start_retrying(retrier.clone()); - // Effectively this is the same as `if retrier.is_idle` plus returning for how long is true. - } else if let Some(t) = retrier.get_elapsed_time() { - if t > self.auto_retry_delay as u64 { - log::info!( - "Finished idling. Flagging {} for retry", - retrier.tower_id - ); - // While a retrier is idle data is not kept in memory. - // Load the pending appointments from the DB and feed them to the retrier - retrier.set_status(RetrierStatus::Stopped); - retrier.pending_appointments.lock().unwrap().extend( - self.wt_client - .lock() - .unwrap() - .dbm - .load_appointment_locators( - retrier.tower_id, - crate::AppointmentStatus::Pending, - ), - ); - } - } - } - // Sleep to not waste a lot of CPU cycles. - tokio::time::sleep(Duration::from_secs(POLLING_TIME)).await; - } - Err(TryRecvError::Disconnected) => break, - } - } - } - - /// Adds an appointment to pending for a given tower. - /// - /// If the tower is not currently being retried, a new entry for it is created, otherwise, the data is appended to the existing entry. - fn add_pending_appointments(&mut self, tower_id: TowerId, locators: HashSet) { - if let std::collections::hash_map::Entry::Vacant(e) = self.retriers.entry(tower_id) { - log::debug!("Creating a new entry for tower {tower_id}"); - e.insert(Arc::new(Retrier::new( - self.wt_client.clone(), - tower_id, - locators, - ))); - } else { - let mut pending_appointments = self - .retriers - .get(&tower_id) - .unwrap() - .pending_appointments - .lock() - .unwrap(); - for locator in locators { - log::debug!("Adding pending appointment {locator} to existing tower {tower_id}",); - pending_appointments.insert(locator); - } - } - } - - fn start_retrying(&self, retrier: Arc) { - log::info!("Retrying tower {}", retrier.tower_id); - retrier.start(self.max_elapsed_time_secs, self.max_interval_time_secs); - } -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum RetrierStatus { - /// Retrier is stopped. This could happen if the retrier was never started or it started and - /// finished successfully. If a retrier is stopped and has some pending appointments, it should be - /// started/re-started, otherwise, it can be deleted safely. - Stopped, - /// Retrier is currently retrying the tower. If the retrier receives new appointments, it will - /// **try** to send them along (but it might not send them). - /// - /// If a retrier status is `Running`, then its associated tower is either temporary unreachable or subscription error. - Running, - /// Retrier failed retrying the tower. Should not be re-started. - /// - /// If a retrier status is `Failed`, then its associated tower is neither reachable nor temporary unreachable. - Failed, - /// Retrier is currently idle waiting for a signal to start working again. An Idle retrier can be forced to start - /// working again by the user by manually calling `retrytower`. - /// - /// If a retrier status is `Idle`, then its associated tower is unreachable. - Idle(Instant), -} - -impl RetrierStatus { - /// Check whether the status is [Running](RetrierStatus::Stopped). - pub fn is_stopped(&self) -> bool { - *self == RetrierStatus::Stopped - } - - /// Check whether the status is [Running](RetrierStatus::Running). - pub fn is_running(&self) -> bool { - *self == RetrierStatus::Running - } - - /// Check whether the status is [Idle](RetrierStatus::Idle). - pub fn is_idle(&self) -> bool { - matches!(self, RetrierStatus::Idle { .. }) - } - - /// Check whether the status is [Failed](RetrierStatus::Failed). - pub fn failed(&self) -> bool { - *self == RetrierStatus::Failed - } - - /// Gets the elapsed time of an [Idle](RetrierStatus::Idle) status, [None] otherwise. - pub fn get_elapsed_time(&self) -> Option { - if let RetrierStatus::Idle(x) = *self { - Some(x.elapsed().as_secs()) - } else { - None - } - } -} - -pub struct Retrier { - wt_client: Arc>, - tower_id: TowerId, - pending_appointments: Mutex>, - status: Mutex, -} - -impl Retrier { - pub fn new( - wt_client: Arc>, - tower_id: TowerId, - locators: HashSet, - ) -> Self { - Self { - wt_client, - tower_id, - pending_appointments: Mutex::new(locators), - status: Mutex::new(RetrierStatus::Stopped), - } - } - - fn has_pending_appointments(&self) -> bool { - !self.pending_appointments.lock().unwrap().is_empty() - } - - fn set_status(&self, status: RetrierStatus) { - *self.status.lock().unwrap() = status.clone(); - - // Add or remove retriers from WTClient based on the RetrierStatus - if self.is_running() || self.is_idle() { - log::debug!("Adding {} to active retriers", self.tower_id); - self.wt_client - .lock() - .unwrap() - .retriers - .insert(self.tower_id, status); - } else if self.is_stopped() { - // We are not removing failed retriers here to prevent a manual retry until the retrier is removed from - // the manager - log::debug!("Removing retrier {} from active retriers", self.tower_id); - self.wt_client - .lock() - .unwrap() - .retriers - .remove(&self.tower_id); - } - } - - /// Maps [RetrierStatus::is_stopped] - pub fn is_stopped(&self) -> bool { - self.status.lock().unwrap().is_stopped() - } - - /// Maps [RetrierStatus::is_running] - pub fn is_running(&self) -> bool { - self.status.lock().unwrap().is_running() - } - - /// Maps [RetrierStatus::is_idle] - pub fn is_idle(&self) -> bool { - self.status.lock().unwrap().is_idle() - } - - /// Maps [RetrierStatus::failed] - pub fn failed(&self) -> bool { - self.status.lock().unwrap().failed() - } - - /// Maps [RetrierStatus::get_elapsed_time] - pub fn get_elapsed_time(&self) -> Option { - self.status.lock().unwrap().get_elapsed_time() - } - - pub fn should_start(&self) -> bool { - // A retrier can be started/re-started if it is stopped (i.e. not running and not failed) - // and has some pending appointments. - self.is_stopped() && self.has_pending_appointments() - } - - pub fn start(self: Arc, max_elapsed_time_secs: u16, max_interval_time_secs: u16) { - // We shouldn't be retrying failed and running retriers. - debug_assert_eq!(*self.status.lock().unwrap(), RetrierStatus::Stopped); - - // When manually retrying the tower may be in either SubscriptionError or Unreachable state. - // Flag this as TemporaryUnreachable only if there is no SubscriptionError. - // Rationale: if there is a subscription error that needs to be handled first, otherwise we'll - // waste a retry cycle with a request that will always fail. - { - let mut state = self.wt_client.lock().unwrap(); - if !state - .get_tower_status(&self.tower_id) - .unwrap() - .is_subscription_error() - { - state.set_tower_status(self.tower_id, TowerStatus::TemporaryUnreachable); - } - } - self.set_status(RetrierStatus::Running); - - tokio::spawn(async move { - let r = retry_notify( - ExponentialBackoff { - max_elapsed_time: Some(Duration::from_secs(max_elapsed_time_secs as u64)), - max_interval: Duration::from_secs(max_interval_time_secs as u64), - ..ExponentialBackoff::default() - }, - || async { self.run().await }, - |err, _| { - log::warn!("Retry error happened with {}. {err}", self.tower_id); - }, - ) - .await; - - match r { - Ok(_) => { - log::info!("Retry strategy succeeded for {}", self.tower_id); - // Set the tower status now so new appointment doesn't go to the retry manager. - self.wt_client - .lock() - .unwrap() - .set_tower_status(self.tower_id, TowerStatus::Reachable); - // Retrier succeeded and can be re-used by re-starting it. - self.set_status(RetrierStatus::Stopped); - } - Err(e) => { - // Notice we'll end up here after a permanent error. That is, either after finishing the backoff strategy - // unsuccessfully or by manually raising such an error (like when facing a tower misbehavior). - log::warn!("Retry strategy gave up for {}. {e}", self.tower_id); - if e.is_permanent() { - self.set_status(RetrierStatus::Failed); - } - - match e { - RetryError::Subscription(_, true) => { - log::info!("Setting {} status as subscription error", self.tower_id); - self.wt_client - .lock() - .unwrap() - .set_tower_status(self.tower_id, TowerStatus::SubscriptionError) - } - RetryError::Misbehaving(p) => { - log::warn!("Cannot recover known tower_id from the appointment receipt. Flagging tower as misbehaving"); - self.wt_client - .lock() - .unwrap() - .flag_misbehaving_tower(self.tower_id, p); - } - RetryError::Abandoned => { - log::info!("Skipping retrying abandoned tower {}", self.tower_id) - } - // This covers `RetryError::Unreachable` and `RetryError::Subscription(_, false)` - _ => { - log::debug!("Starting to idle"); - self.set_status(RetrierStatus::Idle(Instant::now())); - // Clear all pending appointments so they do not waste any memory while idling - self.pending_appointments.lock().unwrap().clear(); - self.wt_client - .lock() - .unwrap() - .set_tower_status(self.tower_id, TowerStatus::Unreachable); - } - } - } - } - }); - } - - async fn run(&self) -> Result<(), Error> { - // 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.contains_key(&self.tower_id) { - return Err(Error::permanent(RetryError::Abandoned)); - } - - let tower = wt_client.towers.get(&self.tower_id).unwrap(); - ( - self.tower_id, - tower.status, - tower.net_addr.clone(), - wt_client.user_id, - wt_client.user_sk, - wt_client.proxy.clone(), - ) - }; - - // If the tower state is subscription_error we need to re-register first. If we cannot, then the retry is aborted. - if status.is_subscription_error() { - let receipt = http::register(tower_id, user_id, &net_addr, &proxy) - .await - .map_err(|e| { - log::debug!("Cannot renew registration with tower. Error: {e:?}"); - Error::transient(RetryError::Subscription( - "Cannot renew registration with tower".to_owned(), - false, - )) - })?; - if !receipt.verify(&tower_id) { - return Err(Error::permanent(RetryError::Subscription("Registration receipt contains bad signature. Are you using the right tower_id?".to_owned(), true))); - } - self.wt_client - .lock() - .unwrap() - .add_update_tower(tower_id, net_addr.net_addr(), &receipt) - .map_err(|e| { - let reason = if e.is_expiry() { - "Registration receipt contains a subscription expiry that is not higher than the one we are currently registered for" - } else { - "Registration receipt does not contain more slots than the ones we are currently registered for" - }; - Error::permanent(RetryError::Subscription(reason.to_owned(), true)) - })?; - } - - while self.has_pending_appointments() { - let locators = self.pending_appointments.lock().unwrap().clone(); - for locator in locators.into_iter() { - let appointment = self - .wt_client - .lock() - .unwrap() - .dbm - .load_appointment(locator) - .unwrap(); - - match http::add_appointment( - tower_id, - &net_addr, - &proxy, - &appointment, - &cryptography::sign(&appointment.to_vec(), &user_sk), - ) - .await - { - Ok((slots, receipt)) => { - self.pending_appointments.lock().unwrap().remove(&locator); - let mut wt_client = self.wt_client.lock().unwrap(); - wt_client.add_appointment_receipt( - tower_id, - appointment.locator, - slots, - &receipt, - ); - wt_client.remove_pending_appointment(tower_id, appointment.locator); - log::debug!("Response verified and data stored in the database"); - } - Err(e) => { - match e { - AddAppointmentError::RequestError(e) => { - if e.is_connection() { - log::warn!( - "{tower_id} cannot be reached. Tower will be retried later" - ); - return Err(Error::transient(RetryError::Unreachable)); - } - } - AddAppointmentError::ApiError(e) => match e.error_code { - errors::INVALID_SIGNATURE_OR_SUBSCRIPTION_ERROR => { - log::warn!("There is a subscription issue with {tower_id}"); - self.wt_client - .lock() - .unwrap() - .set_tower_status(tower_id, TowerStatus::SubscriptionError); - return Err(Error::transient(RetryError::Subscription( - "Subscription error".to_owned(), - false, - ))); - } - _ => { - log::warn!( - "{tower_id} rejected the appointment. Error: {}, error_code: {}", - e.error, - e.error_code - ); - // We need to move the appointment from pending to invalid - // Add it first to invalid and remove it from pending later so a cascade delete is not triggered - self.pending_appointments.lock().unwrap().remove(&locator); - let mut wt_client = self.wt_client.lock().unwrap(); - wt_client.add_invalid_appointment(tower_id, &appointment); - wt_client - .remove_pending_appointment(tower_id, appointment.locator); - } - }, - AddAppointmentError::SignatureError(proof) => { - return Err(Error::permanent(RetryError::Misbehaving(proof))); - } - } - } - } - } - } - - Ok(()) - } - - /// Removed our retrier identifier from the WTClient if the retrier has failed - pub fn remove_if_failed(&self) { - if self.failed() { - log::debug!( - "Removing failed retrier {} from active retriers", - self.tower_id - ); - self.wt_client - .lock() - .unwrap() - .retriers - .remove(&self.tower_id); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - 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, - get_registration_receipt_from_previous, - }; - - use crate::net::http::ApiError; - use crate::test_utils::get_dummy_add_appointment_response; - - const LONG_AUTO_RETRY_DELAY: u32 = 60; - const SHORT_AUTO_RETRY_DELAY: u32 = 3; - const API_DELAY: f64 = 0.5; - const HALF_API_DELAY: f64 = API_DELAY / 2.0; - const MAX_ELAPSED_TIME: u16 = 2; - 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 { - wt_client, - tower_id, - pending_appointments: Mutex::new(HashSet::new()), - status: Mutex::new(RetrierStatus::Stopped), - } - } - } - - #[tokio::test] - 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 mut server = mockito::Server::new_async().await; - - // Add a tower with pending appointments - let (tower_sk, tower_pk) = cryptography::get_random_keypair(); - let tower_id = TowerId(tower_pk); - let receipt = get_random_registration_receipt(); - wt_client - .lock() - .unwrap() - .add_update_tower(tower_id, &server.url(), &receipt) - .unwrap(); - - // Add appointment to pending - let appointment = generate_random_appointment(None); - wt_client - .lock() - .unwrap() - .add_pending_appointment(tower_id, &appointment); - - // Prepare the mock response - let mut add_appointment_receipt = AppointmentReceipt::new( - 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("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))) - .unwrap(); - - let wt_client_clone = wt_client.clone(); - let task = tokio::spawn(async move { - RetryManager::new( - wt_client_clone, - rx, - MAX_ELAPSED_TIME, - LONG_AUTO_RETRY_DELAY, - MAX_INTERVAL_TIME, - ) - .manage_retry() - .await - }); - - // Wait for a fraction of the API delay and check how the tower status changed - tokio::time::sleep(Duration::from_secs_f64(HALF_API_DELAY)).await; - assert!(wt_client - .lock() - .unwrap() - .get_retrier_status(&tower_id) - .unwrap() - .is_running()); - - wait_until!(wt_client - .lock() - .unwrap() - .get_retrier_status(&tower_id) - .is_none()); - - { - 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(); - } - - #[tokio::test] - async fn test_manage_retry_unreachable() { - 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, - )); - - // Add a tower with pending appointments - let (tower_sk, tower_pk) = cryptography::get_random_keypair(); - let tower_id = TowerId(tower_pk); - let receipt = get_random_registration_receipt(); - wt_client - .lock() - .unwrap() - .add_update_tower(tower_id, "http://unreachable.tower", &receipt) - .unwrap(); - - // Add appointment to pending - let appointment = generate_random_appointment(None); - wt_client - .lock() - .unwrap() - .add_pending_appointment(tower_id, &appointment); - - // Start the task and send the tower to the channel for retry - tx.send((tower_id, RevocationData::Fresh(appointment.locator))) - .unwrap(); - - let wt_client_clone = wt_client.clone(); - let task = tokio::spawn(async move { - RetryManager::new( - wt_client_clone, - rx, - MAX_ELAPSED_TIME, - SHORT_AUTO_RETRY_DELAY, - MAX_INTERVAL_TIME, - ) - .manage_retry() - .await - }); - - // Wait for one retry round and check to tower status - tokio::time::sleep(Duration::from_secs_f64(MAX_RUN_TIME)).await; - assert!(wt_client - .lock() - .unwrap() - .get_tower_status(&tower_id) - .unwrap() - .is_temporary_unreachable()); - assert!(wt_client - .lock() - .unwrap() - .get_retrier_status(&tower_id) - .unwrap() - .is_running()); - - // 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 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), - 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("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 - .lock() - .unwrap() - .add_update_tower( - tower_id, - &server.url(), - &get_registration_receipt_from_previous(&receipt), - ) - .unwrap(); - - // Wait and check. We wait twice the short retry delay because it can be the case that the first auto retry - // is performed while we are patching the mock. - tokio::time::sleep(Duration::from_secs((SHORT_AUTO_RETRY_DELAY * 2) as u64)).await; - assert_eq!( - wt_client - .lock() - .unwrap() - .get_tower_status(&tower_id) - .unwrap(), - TowerStatus::Reachable - ); - assert!(!wt_client - .lock() - .unwrap() - .towers - .get(&tower_id) - .unwrap() - .pending_appointments - .contains(&appointment.locator)); - assert!(!wt_client.lock().unwrap().retriers.contains_key(&tower_id)); - api_mock.assert_async().await; - - task.abort(); - } - - #[tokio::test] - async fn test_manage_retry_rejected() { - 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 mut server = mockito::Server::new_async().await; - - // Add a tower with pending appointments - let (_, tower_pk) = cryptography::get_random_keypair(); - let tower_id = TowerId(tower_pk); - let receipt = get_random_registration_receipt(); - wt_client - .lock() - .unwrap() - .add_update_tower(tower_id, &server.url(), &receipt) - .unwrap(); - - // Add appointment to pending - let appointment = generate_random_appointment(None); - wt_client - .lock() - .unwrap() - .add_pending_appointment(tower_id, &appointment); - - // Prepare the mock response - 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))) - .unwrap(); - - let wt_client_clone = wt_client.clone(); - let task = tokio::spawn(async move { - RetryManager::new( - wt_client_clone, - rx, - MAX_ELAPSED_TIME, - LONG_AUTO_RETRY_DELAY, - MAX_INTERVAL_TIME, - ) - .manage_retry() - .await - }); - - // Wait for a fraction of the API delay and check how the tower status changed - tokio::time::sleep(Duration::from_secs_f64(HALF_API_DELAY)).await; - assert!(wt_client - .lock() - .unwrap() - .get_retrier_status(&tower_id) - .unwrap() - .is_running()); - - // Wait for the remaining time and re-check - 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() - .towers - .get(&tower_id) - .unwrap() - .pending_appointments - .contains(&appointment.locator)); - assert!(wt_client - .lock() - .unwrap() - .towers - .get(&tower_id) - .unwrap() - .invalid_appointments - .contains(&appointment.locator)); - api_mock.assert_async().await; - - task.abort(); - } - - #[tokio::test] - async fn test_manage_retry_misbehaving() { - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let (tx, rx) = unbounded_channel(); - let wt_client = Arc::new(Mutex::new( - WTClient::new(tmp_path.path().to_path_buf(), tx.clone()).await, - )); - let mut server = mockito::Server::new_async().await; - - // Add a tower with pending appointments - let (_, tower_pk) = cryptography::get_random_keypair(); - let tower_id = TowerId(tower_pk); - let receipt = get_random_registration_receipt(); - wt_client - .lock() - .unwrap() - .add_update_tower(tower_id, &server.url(), &receipt) - .unwrap(); - - // Add appointment to pending - let appointment = generate_random_appointment(None); - wt_client - .lock() - .unwrap() - .add_pending_appointment(tower_id, &appointment); - - // Prepare the mock response - let mut add_appointment_receipt = AppointmentReceipt::new( - 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("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))) - .unwrap(); - - let wt_client_clone = wt_client.clone(); - let task = tokio::spawn(async move { - RetryManager::new( - wt_client_clone, - rx, - MAX_ELAPSED_TIME, - LONG_AUTO_RETRY_DELAY, - MAX_INTERVAL_TIME, - ) - .manage_retry() - .await - }); - - // Wait for a fraction of the API delay and check how the tower status changed - tokio::time::sleep(Duration::from_secs_f64(HALF_API_DELAY)).await; - assert!(wt_client - .lock() - .unwrap() - .get_retrier_status(&tower_id) - .unwrap() - .is_running()); - - // 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()); - api_mock.assert_async().await; - - task.abort(); - } - - #[tokio::test] - async fn test_manage_retry_abandoned() { - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let (tx, rx) = unbounded_channel(); - let wt_client = Arc::new(Mutex::new( - WTClient::new(tmp_path.path().to_path_buf(), tx.clone()).await, - )); - let server = mockito::Server::new_async().await; - - // Add a tower with pending appointments - let (_, tower_pk) = cryptography::get_random_keypair(); - let tower_id = TowerId(tower_pk); - let receipt = get_random_registration_receipt(); - wt_client - .lock() - .unwrap() - .add_update_tower(tower_id, &server.url(), &receipt) - .unwrap(); - - // Remove the tower (to simulate it has been abandoned) - wt_client.lock().unwrap().remove_tower(tower_id).unwrap(); - - // Start the task and send the tower to the channel for retry - tx.send((tower_id, RevocationData::None)).unwrap(); - - let wt_client_clone = wt_client.clone(); - let task = tokio::spawn(async move { - RetryManager::new( - wt_client_clone, - rx, - MAX_ELAPSED_TIME, - LONG_AUTO_RETRY_DELAY, - MAX_INTERVAL_TIME, - ) - .manage_retry() - .await - }); - assert!(!wt_client.lock().unwrap().towers.contains_key(&tower_id)); - - task.abort(); - } - - #[tokio::test] - async fn test_manage_retry_subscription_error() { - 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 mut server = mockito::Server::new_async().await; - - // Add a tower with pending appointments - let (tower_sk, tower_pk) = cryptography::get_random_keypair(); - let tower_id = TowerId(tower_pk); - let mut registration_receipt = - RegistrationReceipt::new(wt_client.lock().unwrap().user_id, 21, 42, 420); - registration_receipt.sign(&tower_sk); - wt_client - .lock() - .unwrap() - .add_update_tower(tower_id, &server.url(), ®istration_receipt) - .unwrap(); - - // Add appointment to pending - let appointment = generate_random_appointment(None); - wt_client - .lock() - .unwrap() - .add_pending_appointment(tower_id, &appointment); - - // 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), - 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("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 - .lock() - .unwrap() - .set_tower_status(tower_id, TowerStatus::SubscriptionError); - - // Start the task and send the tower to the channel for retry - tx.send((tower_id, RevocationData::Fresh(appointment.locator))) - .unwrap(); - - let wt_client_clone = wt_client.clone(); - let task = tokio::spawn(async move { - RetryManager::new( - wt_client_clone, - rx, - MAX_ELAPSED_TIME, - LONG_AUTO_RETRY_DELAY, - MAX_INTERVAL_TIME, - ) - .manage_retry() - .await - }); - - // Wait for a fraction of the API delay and check how the tower status changed - tokio::time::sleep(Duration::from_secs_f64(HALF_API_DELAY)).await; - assert!(wt_client - .lock() - .unwrap() - .get_retrier_status(&tower_id) - .unwrap() - .is_running()); - - // Wait for the remaining time and re-check - wait_until!(wt_client - .lock() - .unwrap() - .get_retrier_status(&tower_id) - .is_none()); - - { - 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; - - task.abort(); - } - - #[tokio::test] - async fn test_manage_retry_while_idle() { - use crate::dbm::DBM; - // Let's try adding a tower, setting it to idle and send revocation data in all its forms - // This replicates the three types of data the retrier can receive: - // - Initialization (from db) with stale data - // - Regular (fresh) data from `on_commitment_revocation` - // - A wake up call with no data - - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let (tx, rx) = unbounded_channel(); - - // Stale data is sent on WTClient initialization if found in the database. We'll force that to happen by populating the DB before initializing the WTClient - let (tower_sk, tower_pk) = cryptography::get_random_keypair(); - let tower_id = TowerId(tower_pk); - - let mut dbm = DBM::new(&tmp_path.path().to_path_buf().join("watchtowers_db.sql3")).unwrap(); - let receipt = get_random_registration_receipt(); - dbm.store_tower_record(tower_id, "http://unreachable.tower", &receipt) - .unwrap(); - - let appointment = generate_random_appointment(None); - dbm.store_pending_appointment(tower_id, &appointment) - .unwrap(); - - // Now we can create the WTClient and check that the data is pending - let wt_client = Arc::new(Mutex::new( - WTClient::new(tmp_path.path().to_path_buf(), tx.clone()).await, - )); - - // Also create the retrier thread so retries can be managed - let wt_client_clone = wt_client.clone(); - let task = tokio::spawn(async move { - RetryManager::new( - wt_client_clone, - rx, - MAX_ELAPSED_TIME, - LONG_AUTO_RETRY_DELAY, - MAX_INTERVAL_TIME, - ) - .manage_retry() - .await - }); - - { - // After the retriers gives up, it should go idling and flag the tower as unreachable - tokio::time::sleep(Duration::from_secs_f64( - MAX_ELAPSED_TIME as f64 + MAX_RUN_TIME, - )) - .await; - - 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); - } - - // 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 appointment2 = generate_random_appointment(None); - wt_client - .lock() - .unwrap() - .add_pending_appointment(tower_id, &appointment2); - tx.send((tower_id, RevocationData::Fresh(appointment2.locator))) - .unwrap(); - - { - tokio::time::sleep(Duration::from_secs_f64(POLLING_TIME as f64 + MAX_RUN_TIME)).await; - let state = wt_client.lock().unwrap(); - assert!(state.get_retrier_status(&tower_id).unwrap().is_idle()); - let tower = state.towers.get(&tower_id).unwrap(); - assert_eq!(tower.status, TowerStatus::Unreachable); - } - - // 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 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 - .lock() - .unwrap() - .towers - .get_mut(&tower_id) - .unwrap() - .set_net_addr(server.url()); - - // Check pending data is still there now, and is it not once the retrier succeeds - assert_eq!( - wt_client - .lock() - .unwrap() - .towers - .get(&tower_id) - .unwrap() - .pending_appointments - .len(), - 2, - ); - - // 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; - 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; - - task.abort(); - } - - #[tokio::test] - async fn test_retry_tower() { - let (tower_sk, tower_pk) = cryptography::get_random_keypair(); - let tower_id = TowerId(tower_pk); - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let wt_client = Arc::new(Mutex::new( - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, - )); - let 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.url(), &receipt) - .unwrap(); - - // Add appointment to pending - let appointment = generate_random_appointment(None); - wt_client - .lock() - .unwrap() - .add_pending_appointment(tower_id, &appointment); - - // Prepare the mock response - let mut add_appointment_receipt = AppointmentReceipt::new( - 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("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_async().await; - } - - #[tokio::test] - async fn test_retry_tower_no_pending() { - let (_, tower_pk) = cryptography::get_random_keypair(); - let tower_id = TowerId(tower_pk); - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let wt_client = Arc::new(Mutex::new( - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, - )); - let server = 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.url(), &receipt) - .unwrap(); - - // If there are no pending appointments the method will simply return - let r = Retrier::empty(wt_client, tower_id).run().await; - assert_eq!(r, Ok(())); - } - - #[tokio::test] - async fn test_retry_tower_misbehaving() { - let (_, tower_pk) = cryptography::get_random_keypair(); - let tower_id = TowerId(tower_pk); - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let wt_client = Arc::new(Mutex::new( - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, - )); - let 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.url(), &receipt) - .unwrap(); - - // Add appointment to pending - let appointment = generate_random_appointment(None); - wt_client - .lock() - .unwrap() - .add_pending_appointment(tower_id, &appointment); - - // Prepare the mock response - let mut add_appointment_receipt = AppointmentReceipt::new( - 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("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!(matches!( - r, - Err(Error::Permanent(RetryError::Misbehaving { .. },)) - )); - api_mock.assert_async().await; - } - - #[tokio::test] - async fn test_retry_tower_unreachable() { - let (_, tower_pk) = cryptography::get_random_keypair(); - let tower_id = TowerId(tower_pk); - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let wt_client = Arc::new(Mutex::new( - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, - )); - - // 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, "http://unreachable.tower", &receipt) - .unwrap(); - - // Add some pending appointments and try again (with an unreachable tower). - let appointment = generate_random_appointment(None); - wt_client - .lock() - .unwrap() - .add_pending_appointment(tower_id, &appointment); - - // 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, Err(Error::transient(RetryError::Unreachable))); - } - - #[tokio::test] - async fn test_retry_tower_subscription_error() { - let (_, tower_pk) = cryptography::get_random_keypair(); - let tower_id = TowerId(tower_pk); - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let wt_client = Arc::new(Mutex::new( - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, - )); - let 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.url(), &receipt) - .unwrap(); - - 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); - wt_client - .lock() - .unwrap() - .add_pending_appointment(tower_id, &appointment); - - // 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!(matches!( - r, - Err(Error::Transient { - err: RetryError::Subscription { .. }, - .. - }) - )); - api_mock.assert_async().await; - } - - #[tokio::test] - async fn test_retry_tower_rejected() { - let (_, tower_pk) = cryptography::get_random_keypair(); - let tower_id = TowerId(tower_pk); - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let wt_client = Arc::new(Mutex::new( - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, - )); - let 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.url(), &receipt) - .unwrap(); - - 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); - wt_client - .lock() - .unwrap() - .add_pending_appointment(tower_id, &appointment); - - // Since we are retrying manually, we need to add the data to pending appointments manually too - let retrier = Retrier::new( - wt_client.clone(), - tower_id, - HashSet::from([appointment.locator]), - ); - let r = retrier.run().await; - - assert!(wt_client - .lock() - .unwrap() - .towers - .get(&tower_id) - .unwrap() - .invalid_appointments - .contains(&appointment.locator)); - assert!(r.is_ok()); - api_mock.assert_async().await; - } - - #[tokio::test] - async fn test_retry_tower_abandoned() { - let (_, tower_pk) = cryptography::get_random_keypair(); - let tower_id = TowerId(tower_pk); - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let wt_client = Arc::new(Mutex::new( - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await, - )); - - // 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, "http://tower.adrress", &receipt) - .unwrap(); - - // Remove the tower (to simulate it has been abandoned) - wt_client.lock().unwrap().remove_tower(tower_id).unwrap(); - - // If there are no pending appointments the method will simply return - let r = Retrier::empty(wt_client, tower_id).run().await; - - assert_eq!(r, Err(Error::permanent(RetryError::Abandoned))); - } -} diff --git a/watchtower-plugin/src/ser.rs b/watchtower-plugin/src/ser.rs deleted file mode 100644 index d0c2e9b..0000000 --- a/watchtower-plugin/src/ser.rs +++ /dev/null @@ -1,70 +0,0 @@ -use bitcoin::consensus::encode; -use bitcoin::Transaction; - -use teos_common::appointment::{Appointment, Locator}; - -use hex::FromHex; -use serde::{de, ser::SerializeMap, Deserializer, Serialize, Serializer}; -use std::collections::HashMap; - -pub fn deserialize_tx<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - struct TransactionVisitor; - - impl<'de> de::Visitor<'de> for TransactionVisitor { - type Value = Transaction; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a hex string containing the transaction") - } - - fn visit_str(self, v: &str) -> Result - where - E: de::Error, - { - let tx = encode::deserialize( - &Vec::from_hex(v).map_err(|_| E::custom("transaction is not hex encoded"))?, - ) - .map_err(|_| E::custom("transaction cannot be deserialized"))?; - Ok(tx) - } - } - - deserializer.deserialize_any(TransactionVisitor) -} - -pub fn serialize_receipts(hm: &HashMap, s: S) -> Result -where - S: Serializer, -{ - let mut map = s.serialize_map(Some(hm.len()))?; - for (locator, sig) in hm { - map.serialize_entry(&hex::encode(locator), sig)?; - } - map.end() -} - -#[derive(Serialize)] -struct AppointmentInners { - encrypted_blob: String, - to_self_delay: u32, -} - -pub fn serialize_appointments(v: &Vec, s: S) -> Result -where - S: Serializer, -{ - let mut map = s.serialize_map(Some(v.len()))?; - for a in v { - map.serialize_entry( - &hex::encode(a.locator), - &AppointmentInners { - encrypted_blob: hex::encode(&a.encrypted_blob), - to_self_delay: a.to_self_delay, - }, - )?; - } - map.end() -} diff --git a/watchtower-plugin/src/test_utils.rs b/watchtower-plugin/src/test_utils.rs deleted file mode 100644 index be89b61..0000000 --- a/watchtower-plugin/src/test_utils.rs +++ /dev/null @@ -1,16 +0,0 @@ -use teos_common::appointment::Locator; -use teos_common::protos as common_msgs; -use teos_common::receipts::AppointmentReceipt; - -pub fn get_dummy_add_appointment_response( - locator: Locator, - receipt: &AppointmentReceipt, -) -> common_msgs::AddAppointmentResponse { - common_msgs::AddAppointmentResponse { - locator: locator.to_vec(), - start_block: receipt.start_block(), - signature: receipt.signature().unwrap(), - available_slots: 21, - subscription_expiry: 1000, - } -} diff --git a/watchtower-plugin/src/wt_client.rs b/watchtower-plugin/src/wt_client.rs deleted file mode 100644 index ecf26db..0000000 --- a/watchtower-plugin/src/wt_client.rs +++ /dev/null @@ -1,901 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::iter::FromIterator; -use std::path::PathBuf; -use tokio::fs; -use tokio::sync::mpsc::UnboundedSender; - -use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; - -use teos_common::appointment::{Appointment, Locator}; -use teos_common::cryptography; -use teos_common::dbm::Error as DBError; -use teos_common::receipts::{AppointmentReceipt, RegistrationReceipt}; -use teos_common::{TowerId, UserId}; - -use crate::dbm::DBM; -use crate::net::ProxyInfo; -use crate::retrier::RetrierStatus; -use crate::{MisbehaviorProof, SubscriptionError, TowerInfo, TowerStatus, TowerSummary}; - -#[derive(Eq, PartialEq)] -pub enum RevocationData { - Fresh(Locator), - Stale(HashSet), - None, -} - -impl RevocationData { - pub fn is_none(&self) -> bool { - *self == RevocationData::None - } -} - -impl From for HashSet { - fn from(r: RevocationData) -> Self { - match r { - RevocationData::Fresh(l) => HashSet::from_iter(vec![l]), - RevocationData::Stale(hs) => hs, - RevocationData::None => HashSet::new(), - } - } -} - -impl std::fmt::Debug for RevocationData { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - match self { - RevocationData::Fresh(l) => format!("Fresh: {l}"), - RevocationData::Stale(hs) => format!( - "Stale: {:?}", - hs.iter().map(|l| l.to_string()).collect::>() - ), - RevocationData::None => "None".to_owned(), - } - ) - } -} - -/// Represents the watchtower client that is being used as the CoreLN plugin state. -pub struct WTClient { - /// A [DBM] instance. - pub dbm: DBM, - /// A collection of towers the client is registered to. - pub towers: HashMap, - /// Queue of unreachable towers. - pub unreachable_towers: UnboundedSender<(TowerId, RevocationData)>, - // Map of existing retriers and its state. - pub retriers: HashMap, - /// The user secret key. - pub user_sk: SecretKey, - /// The user identifier. - pub user_id: UserId, - /// Optional proxy - pub proxy: Option, -} - -impl WTClient { - pub async fn new( - data_dir: PathBuf, - unreachable_towers: UnboundedSender<(TowerId, RevocationData)>, - ) -> Self { - Self::with_proxy(data_dir, unreachable_towers, None).await - } - - pub async fn with_proxy( - data_dir: PathBuf, - unreachable_towers: UnboundedSender<(TowerId, RevocationData)>, - proxy: Option, - ) -> Self { - // Create data dir if it does not exist - fs::create_dir_all(&data_dir).await.unwrap_or_else(|e| { - log::error!("Cannot create data dir: {e:?}"); - std::process::exit(1); - }); - - let dbm = DBM::new(&data_dir.join("watchtowers_db.sql3")).unwrap(); - - let (user_sk, user_id) = if let Some(sk) = dbm.load_client_key() { - ( - sk, - UserId(PublicKey::from_secret_key(&Secp256k1::new(), &sk)), - ) - } 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(); - for (tower_id, tower) in towers.iter() { - if tower.status.is_temporary_unreachable() { - unreachable_towers - .send(( - *tower_id, - RevocationData::Stale(tower.pending_appointments.iter().cloned().collect()), - )) - .unwrap(); - } - } - - log::info!("Plugin watchtower client initialized. User id = {user_id}"); - - WTClient { - towers, - unreachable_towers, - retriers: HashMap::new(), - dbm, - user_sk, - user_id, - proxy, - } - } - - /// Adds or updates a tower entry. - pub fn add_update_tower( - &mut self, - tower_id: TowerId, - tower_net_addr: &str, - receipt: &RegistrationReceipt, - ) -> Result<(), SubscriptionError> { - if let Some(tower) = self.towers.get(&tower_id) { - // TODO: For now we're forcing updates to increase both slots and expiry. This is not mandatory and may - // be changed in the future, but the tower is currently set to do this anyway so let's keep it simple. - if receipt.subscription_expiry() <= tower.subscription_expiry { - return Err(SubscriptionError::Expiry); - } else { - let tower_info = self.dbm.load_tower_record(tower_id).unwrap(); - if receipt.available_slots() <= tower_info.available_slots { - return Err(SubscriptionError::Slots); - } - } - } - - self.dbm - .store_tower_record(tower_id, tower_net_addr, receipt) - .unwrap(); - - if let Some(summary) = self.towers.get_mut(&tower_id) { - summary.udpate( - tower_net_addr.to_owned(), - receipt.available_slots(), - receipt.subscription_start(), - receipt.subscription_expiry(), - ); - } else { - self.towers.insert( - tower_id, - TowerSummary::new( - tower_net_addr.to_owned(), - receipt.available_slots(), - receipt.subscription_start(), - receipt.subscription_expiry(), - ), - ); - }; - - Ok(()) - } - - /// Gets the latest registration receipt of a given tower. - 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) -> Option { - self.dbm.load_tower_record(tower_id) - } - - /// Gets the given tower status (identified by tower_id), if found. - pub fn get_tower_status(&self, tower_id: &TowerId) -> Option { - Some(self.towers.get(tower_id)?.status) - } - - /// Sets the tower status to any of the `TowerStatus` variants. - pub fn set_tower_status(&mut self, tower_id: TowerId, status: TowerStatus) { - if let Some(tower) = self.towers.get_mut(&tower_id) { - if tower.status != status { - tower.status = status - } else { - log::debug!("{tower_id} status is already {status}") - } - } else { - log::error!("Cannot change tower status to {status}. Unknown tower_id: {tower_id}"); - } - } - - /// Gets the given tower status (identified by tower_id), if found. - pub fn get_retrier_status(&self, tower_id: &TowerId) -> Option<&RetrierStatus> { - self.retriers.get(tower_id) - } - - /// Adds an appointment receipt to the tower record. - pub fn add_appointment_receipt( - &mut self, - tower_id: TowerId, - locator: Locator, - available_slots: u32, - receipt: &AppointmentReceipt, - ) { - if let Some(tower) = self.towers.get_mut(&tower_id) { - // DISCUSS: It may be nice to independently compute the slots and compare - tower.available_slots = available_slots; - - self.dbm - .store_appointment_receipt(tower_id, locator, available_slots, receipt) - .unwrap(); - } else { - log::error!("Cannot add appointment receipt to tower. Unknown tower_id: {tower_id}"); - } - } - - /// Gets an appointment receipt from the database (if found). - pub fn get_appointment_receipt( - &self, - tower_id: TowerId, - locator: Locator, - ) -> Option { - self.dbm.load_appointment_receipt(tower_id, locator) - } - - /// Adds a pending appointment to the tower record. - pub fn add_pending_appointment(&mut self, tower_id: TowerId, appointment: &Appointment) { - if let Some(tower) = self.towers.get_mut(&tower_id) { - tower.pending_appointments.insert(appointment.locator); - - self.dbm - .store_pending_appointment(tower_id, appointment) - .unwrap(); - } else { - log::error!("Cannot add pending appointment to tower. Unknown tower_id: {tower_id}"); - } - } - - /// Removes a pending appointment from the tower record. - pub fn remove_pending_appointment(&mut self, tower_id: TowerId, locator: Locator) { - if let Some(tower) = self.towers.get_mut(&tower_id) { - tower.pending_appointments.remove(&locator); - - self.dbm - .delete_pending_appointment(tower_id, locator) - .unwrap(); - } else { - log::error!("Cannot remove pending appointment to tower. Unknown tower_id: {tower_id}"); - } - } - - /// Adds an invalid appointment to the tower record. - pub fn add_invalid_appointment(&mut self, tower_id: TowerId, appointment: &Appointment) { - if let Some(tower) = self.towers.get_mut(&tower_id) { - tower.invalid_appointments.insert(appointment.locator); - - self.dbm - .store_invalid_appointment(tower_id, appointment) - .unwrap(); - } else { - log::error!("Cannot add invalid appointment to tower. Unknown tower_id: {tower_id}"); - } - } - - /// Flags a given tower as misbehaving, storing the misbehaving proof in the database. - pub fn flag_misbehaving_tower(&mut self, tower_id: TowerId, proof: MisbehaviorProof) { - if let Some(tower) = self.towers.get_mut(&tower_id) { - self.dbm.store_misbehaving_proof(tower_id, &proof).unwrap(); - tower.status = TowerStatus::Misbehaving; - } else { - log::error!("Cannot flag tower. Unknown tower_id: {tower_id}"); - } - } - - /// Removes a tower from the client (both memory and database). - /// - /// Any data associated to the tower will be deleted (i.e. links to appointments) - pub fn remove_tower(&mut self, tower_id: TowerId) -> Result<(), DBError> { - if self.towers.contains_key(&tower_id) { - self.towers.remove(&tower_id); - self.dbm.remove_tower_record(tower_id) - } else { - Err(DBError::NotFound) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use tempdir::TempDir; - use tokio::sync::mpsc::unbounded_channel; - - use teos_common::test_utils::{ - generate_random_appointment, get_random_appointment_receipt, - get_random_registration_receipt, get_random_user_id, - get_registration_receipt_from_previous, - }; - - #[tokio::test] - async fn test_add_update_load_tower() { - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let mut wt_client = - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await; - - // Adding a new tower will add a summary to towers and the full data to the - let mut receipt = get_random_registration_receipt(); - let (tower_sk, tower_pk) = cryptography::get_random_keypair(); - let tower_id = TowerId(tower_pk); - let tower_info = TowerInfo::empty( - "talaia.watch".to_owned(), - receipt.available_slots(), - receipt.subscription_start(), - receipt.subscription_expiry(), - ); - - wt_client - .add_update_tower(tower_id, &tower_info.net_addr, &receipt) - .unwrap(); - assert_eq!( - wt_client.towers.get(&tower_id), - Some(&TowerSummary::from(tower_info.clone())) - ); - assert_eq!(wt_client.load_tower_info(tower_id).unwrap(), tower_info); - - // Calling the method again with updated information should also updated the records in memory and the database - receipt = get_registration_receipt_from_previous(&receipt); - - let updated_tower_info = TowerInfo::empty( - "talaia.watch".to_owned(), - receipt.available_slots(), - receipt.subscription_start(), - receipt.subscription_expiry(), - ); - wt_client - .add_update_tower(tower_id, &updated_tower_info.net_addr, &receipt) - .unwrap(); - - assert_eq!( - wt_client.towers.get(&tower_id), - Some(&TowerSummary::from(updated_tower_info.clone())) - ); - assert_eq!( - wt_client.load_tower_info(tower_id).unwrap(), - updated_tower_info - ); - - // If we try to update without increasing both the end_time and the slots, this will fail - let mut receipt_same_slots = RegistrationReceipt::new( - receipt.user_id(), - receipt.available_slots(), - receipt.subscription_start(), - receipt.subscription_expiry() + 1, - ); - receipt_same_slots.sign(&tower_sk); - let mut receipt_same_expiry = RegistrationReceipt::new( - receipt.user_id(), - receipt.available_slots() + 1, - receipt.subscription_start(), - receipt.subscription_expiry(), - ); - receipt_same_expiry.sign(&tower_sk); - - assert!(matches!( - wt_client.add_update_tower(tower_id, &updated_tower_info.net_addr, &receipt), - Err(SubscriptionError::Expiry) - )); - assert!(matches!( - wt_client.add_update_tower(tower_id, &updated_tower_info.net_addr, &receipt_same_slots), - Err(SubscriptionError::Slots) - )); - assert!(matches!( - wt_client.add_update_tower( - tower_id, - &updated_tower_info.net_addr, - &receipt_same_expiry - ), - Err(SubscriptionError::Expiry) - )); - - // Decrease the slots count (simulate exhaustion) and update with more than the current count it should work - let locator = generate_random_appointment(None).locator; - wt_client.add_appointment_receipt( - tower_id, - locator, - 0, - &get_random_appointment_receipt(tower_sk), - ); - wt_client - .add_update_tower(tower_id, &updated_tower_info.net_addr, &receipt_same_slots) - .unwrap(); - } - - #[tokio::test] - async fn test_get_tower_status() { - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let mut wt_client = - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await; - - // If the tower is unknown, get_tower_status returns None - let tower_id = get_random_user_id(); - assert!(wt_client.get_tower_status(&tower_id).is_none()); - - // Add a tower - let receipt = get_random_registration_receipt(); - wt_client - .add_update_tower(tower_id, "talaia.watch", &receipt) - .unwrap(); - - // If the tower is known, get_tower_status matches getting the same data from the towers collection - assert_eq!( - wt_client.towers.get(&tower_id).unwrap().status, - wt_client.get_tower_status(&tower_id).unwrap() - ) - } - - #[tokio::test] - async fn test_set_tower_status() { - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let mut wt_client = - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await; - - // If the tower is unknown nothing will happen - let unknown_tower = get_random_user_id(); - wt_client.set_tower_status(unknown_tower, TowerStatus::Reachable); - assert!(!wt_client.towers.contains_key(&unknown_tower)); - - // If the tower is known, the status will be updated. - let receipt = get_random_registration_receipt(); - let tower_id = get_random_user_id(); - wt_client - .add_update_tower(tower_id, "talaia.watch", &receipt) - .unwrap(); - - for status in [ - TowerStatus::Reachable, - TowerStatus::TemporaryUnreachable, - TowerStatus::Unreachable, - TowerStatus::SubscriptionError, - TowerStatus::Misbehaving, - ] { - wt_client.set_tower_status(tower_id, status); - assert_eq!(status, wt_client.get_tower_status(&tower_id).unwrap()); - } - } - - #[tokio::test] - async fn test_add_appointment_receipt() { - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let mut wt_client = - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await; - - let (tower_sk, tower_pk) = cryptography::get_random_keypair(); - let tower_id = TowerId(tower_pk); - - let locator = generate_random_appointment(None).locator; - let registration_receipt = get_random_registration_receipt(); - let appointment_receipt = get_random_appointment_receipt(tower_sk); - - // If we call this on an unknown tower it will simply do nothing - wt_client.add_appointment_receipt( - tower_id, - locator, - registration_receipt.available_slots(), - &appointment_receipt, - ); - assert!(!wt_client.towers.contains_key(&tower_id)); - - // Add the tower to the state and try again - let tower_info = TowerInfo::new( - "talaia.watch".to_owned(), - registration_receipt.available_slots(), - registration_receipt.subscription_start(), - registration_receipt.subscription_expiry(), - HashMap::from([(locator, appointment_receipt.signature().unwrap())]), - Vec::new(), - Vec::new(), - ); - wt_client - .add_update_tower(tower_id, &tower_info.net_addr, ®istration_receipt) - .unwrap(); - wt_client.add_appointment_receipt( - tower_id, - locator, - registration_receipt.available_slots(), - &appointment_receipt, - ); - - assert!(wt_client.towers.contains_key(&tower_id)); - assert_eq!( - wt_client.towers.get(&tower_id).unwrap(), - &TowerSummary::from(tower_info.clone()) - ); - assert_eq!(wt_client.load_tower_info(tower_id).unwrap(), tower_info); - } - - #[tokio::test] - async fn test_add_pending_appointment() { - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let mut wt_client = - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await; - - let tower_id = get_random_user_id(); - - let registration_receipt = get_random_registration_receipt(); - let appointment = generate_random_appointment(None); - - // If we call this on an unknown tower it will simply do nothing - wt_client.add_pending_appointment(tower_id, &appointment); - assert!(!wt_client.towers.contains_key(&tower_id)); - - // Add the tower to the state and try again - let tower_info = TowerInfo::new( - "talaia.watch".to_owned(), - registration_receipt.available_slots(), - registration_receipt.subscription_start(), - registration_receipt.subscription_expiry(), - HashMap::new(), - vec![appointment.clone()], - Vec::new(), - ); - - wt_client - .add_update_tower(tower_id, &tower_info.net_addr, ®istration_receipt) - .unwrap(); - wt_client.add_pending_appointment(tower_id, &appointment); - - assert!(wt_client.towers.contains_key(&tower_id)); - assert_eq!( - wt_client.towers.get(&tower_id).unwrap(), - &TowerSummary::from(tower_info.clone()) - ); - // When towers data is loaded from the database, it is assumed to be reachable. - assert_eq!( - wt_client.load_tower_info(tower_id).unwrap(), - tower_info.with_status(TowerStatus::TemporaryUnreachable) - ); - } - - #[tokio::test] - async fn test_remove_pending_appointment() { - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let mut wt_client = - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await; - - let tower_id = get_random_user_id(); - - let registration_receipt = get_random_registration_receipt(); - let appointment = generate_random_appointment(None); - - // If we call this on an unknown tower it will simply do nothing - wt_client.remove_pending_appointment(tower_id, appointment.locator); - - // Add the tower to the state and try again - wt_client - .add_update_tower(tower_id, "talaia.watch", ®istration_receipt) - .unwrap(); - wt_client.add_pending_appointment(tower_id, &appointment); - - wt_client.remove_pending_appointment(tower_id, appointment.locator); - assert!(!wt_client - .towers - .get(&tower_id) - .unwrap() - .pending_appointments - .contains(&appointment.locator)); - // This bit is tested exhaustively in the DBM. - assert!(!wt_client.dbm.appointment_exists(appointment.locator)); - } - - #[tokio::test] - async fn test_add_invalid_appointment() { - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let mut wt_client = - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await; - - let tower_id = get_random_user_id(); - - let registration_receipt = get_random_registration_receipt(); - let appointment = generate_random_appointment(None); - - // If we call this on an unknown tower it will simply do nothing - wt_client.add_invalid_appointment(tower_id, &appointment); - assert!(!wt_client.towers.contains_key(&tower_id)); - - // Add the tower to the state and try again - let tower_info = TowerInfo::new( - "talaia.watch".to_owned(), - registration_receipt.available_slots(), - registration_receipt.subscription_start(), - registration_receipt.subscription_expiry(), - HashMap::new(), - Vec::new(), - vec![appointment.clone()], - ); - - wt_client - .add_update_tower(tower_id, &tower_info.net_addr, ®istration_receipt) - .unwrap(); - wt_client.add_invalid_appointment(tower_id, &appointment); - - assert!(wt_client.towers.contains_key(&tower_id)); - assert_eq!( - wt_client.towers.get(&tower_id).unwrap(), - &TowerSummary::from(tower_info.clone()) - ); - assert_eq!(wt_client.load_tower_info(tower_id).unwrap(), tower_info); - } - - #[tokio::test] - async fn test_move_pending_appointment_to_invalid() { - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let mut wt_client = - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await; - - let tower_id = get_random_user_id(); - - let registration_receipt = get_random_registration_receipt(); - let appointment = generate_random_appointment(None); - - wt_client - .add_update_tower(tower_id, "talaia.watch", ®istration_receipt) - .unwrap(); - wt_client.add_pending_appointment(tower_id, &appointment); - - // Check that the appointment can be moved from pending to invalid - wt_client.add_invalid_appointment(tower_id, &appointment); - wt_client.remove_pending_appointment(tower_id, appointment.locator); - - assert!(!wt_client - .towers - .get(&tower_id) - .unwrap() - .pending_appointments - .contains(&appointment.locator)); - assert!(wt_client - .towers - .get(&tower_id) - .unwrap() - .invalid_appointments - .contains(&appointment.locator)); - assert!(!wt_client - .dbm - .load_appointment_locators(tower_id, crate::AppointmentStatus::Pending) - .contains(&appointment.locator)); - assert!(wt_client - .dbm - .load_appointment_locators(tower_id, crate::AppointmentStatus::Invalid) - .contains(&appointment.locator)); - assert!(wt_client.dbm.appointment_exists(appointment.locator)); - } - - #[tokio::test] - async fn test_move_pending_appointment_to_invalid_multiple_towers() { - // Check that moving an appointment from pending to invalid can be done even if multiple towers have a reference to it - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let mut wt_client = - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await; - - let tower_id = get_random_user_id(); - let another_tower_id = get_random_user_id(); - let tower_net_addr = "talaia.watch"; - - let registration_receipt = get_random_registration_receipt(); - let appointment = generate_random_appointment(None); - - wt_client - .add_update_tower(tower_id, tower_net_addr, ®istration_receipt) - .unwrap(); - wt_client - .add_update_tower(another_tower_id, tower_net_addr, ®istration_receipt) - .unwrap(); - wt_client.add_pending_appointment(tower_id, &appointment); - wt_client.add_pending_appointment(another_tower_id, &appointment); - - // Check that the appointment can be moved from pending to invalid - wt_client.add_invalid_appointment(tower_id, &appointment); - wt_client.remove_pending_appointment(tower_id, appointment.locator); - - // TOWER_ID CHECKS - assert!(!wt_client - .towers - .get(&tower_id) - .unwrap() - .pending_appointments - .contains(&appointment.locator)); - assert!(wt_client - .towers - .get(&tower_id) - .unwrap() - .invalid_appointments - .contains(&appointment.locator)); - assert!(!wt_client - .dbm - .load_appointment_locators(tower_id, crate::AppointmentStatus::Pending) - .contains(&appointment.locator)); - assert!(wt_client - .dbm - .load_appointment_locators(tower_id, crate::AppointmentStatus::Invalid) - .contains(&appointment.locator)); - - // ANOTHER_TOWER_ID CHECKS - assert!(wt_client - .towers - .get(&another_tower_id) - .unwrap() - .pending_appointments - .contains(&appointment.locator)); - assert!(!wt_client - .towers - .get(&another_tower_id) - .unwrap() - .invalid_appointments - .contains(&appointment.locator)); - assert!(wt_client - .dbm - .load_appointment_locators(another_tower_id, crate::AppointmentStatus::Pending) - .contains(&appointment.locator)); - assert!(!wt_client - .dbm - .load_appointment_locators(another_tower_id, crate::AppointmentStatus::Invalid) - .contains(&appointment.locator)); - - // GENERAL - assert!(wt_client.dbm.appointment_exists(appointment.locator)); - } - - #[tokio::test] - async fn test_flag_misbehaving_tower() { - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let mut wt_client = - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await; - - let (tower_sk, tower_pk) = cryptography::get_random_keypair(); - let tower_id = TowerId(tower_pk); - - // If we call this on an unknown tower it will simply do nothing - let appointment = generate_random_appointment(None); - let receipt = get_random_appointment_receipt(tower_sk); - let proof = MisbehaviorProof::new(appointment.locator, receipt, get_random_user_id()); - wt_client.flag_misbehaving_tower(tower_id, proof.clone()); - assert!(!wt_client.towers.contains_key(&tower_id)); - - // // Add the tower to the state and try again - let registration_receipt = get_random_registration_receipt(); - wt_client - .add_update_tower(tower_id, "talaia.watch", ®istration_receipt) - .unwrap(); - wt_client.flag_misbehaving_tower(tower_id, proof.clone()); - - // Check data in memory - let tower_summary = wt_client.towers.get(&tower_id); - assert!(tower_summary.is_some()); - assert!(tower_summary.unwrap().status.is_misbehaving()); - - // Check data in DB - let loaded_info = wt_client.load_tower_info(tower_id).unwrap(); - assert!(loaded_info.status.is_misbehaving()); - assert_eq!(loaded_info.misbehaving_proof, Some(proof)); - assert!(loaded_info.appointments.contains_key(&appointment.locator)); - } - - #[tokio::test] - async fn test_remove_tower() { - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let mut wt_client = - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await; - - let receipt = get_random_registration_receipt(); - let (tower_sk, tower_pk) = cryptography::get_random_keypair(); - let tower_id = TowerId(tower_pk); - let tower_info = TowerInfo::empty( - "talaia.watch".to_owned(), - receipt.available_slots(), - receipt.subscription_start(), - receipt.subscription_expiry(), - ); - - // Add the tower and check it is there - wt_client - .add_update_tower(tower_id, &tower_info.net_addr, &receipt) - .unwrap(); - assert_eq!( - wt_client.towers.get(&tower_id), - Some(&TowerSummary::from(tower_info.clone())) - ); - assert_eq!(wt_client.load_tower_info(tower_id).unwrap(), tower_info); - - // Remove the tower and check it is not there anymore - wt_client.remove_tower(tower_id).unwrap(); - assert!(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 - wt_client - .add_update_tower(tower_id, &tower_info.net_addr, &receipt) - .unwrap(); - - let locator = generate_random_appointment(None).locator; - let registration_receipt = get_random_registration_receipt(); - let appointment_receipt = get_random_appointment_receipt(tower_sk); - - // If we call this on an unknown tower it will simply do nothing - wt_client.add_appointment_receipt( - tower_id, - locator, - registration_receipt.available_slots(), - &appointment_receipt, - ); - assert!(wt_client.dbm.appointment_receipt_exists(locator, tower_id)); - - // Remove and check both the tower and the appointment - wt_client.remove_tower(tower_id).unwrap(); - assert!(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)); - } - - #[tokio::test] - async fn test_remove_tower_shared_appointment() { - // Lets test removing a tower that has associated data shared with another tower. - // For instance, having an appointment that was sent to two towers, and then deleting one of them - // should only remove the link between the tower and the appointment, but not delete the data. - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let mut wt_client = - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await; - - let receipt = get_random_registration_receipt(); - let (tower1_sk, tower1_pk) = cryptography::get_random_keypair(); - let tower1_id = TowerId(tower1_pk); - let (tower2_sk, tower2_pk) = cryptography::get_random_keypair(); - let tower2_id = TowerId(tower2_pk); - - wt_client - .add_update_tower(tower1_id, "talaia.watch", &receipt) - .unwrap(); - wt_client - .add_update_tower(tower2_id, "talaia.watch", &receipt) - .unwrap(); - - let locator = generate_random_appointment(None).locator; - let registration_receipt = get_random_registration_receipt(); - let appointment_receipt_1 = get_random_appointment_receipt(tower1_sk); - let appointment_receipt_2 = get_random_appointment_receipt(tower2_sk); - - wt_client.add_appointment_receipt( - tower1_id, - locator, - registration_receipt.available_slots(), - &appointment_receipt_1, - ); - wt_client.add_appointment_receipt( - tower2_id, - locator, - registration_receipt.available_slots(), - &appointment_receipt_2, - ); - - // Check that the data exists in both towers - assert!(wt_client.dbm.appointment_receipt_exists(locator, tower1_id)); - assert!(wt_client.dbm.appointment_receipt_exists(locator, tower2_id)); - - // Remove tower1 and check that the appointment receipt can still be found for tower2 - wt_client.remove_tower(tower1_id).unwrap(); - assert!(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)); - } - - #[tokio::test] - async fn test_remove_inexistent_tower() { - let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap(); - let mut wt_client = - WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await; - - assert!(matches!( - wt_client.remove_tower(get_random_user_id()), - Err(DBError::NotFound) - )); - } -} diff --git a/watchtower-plugin/tests/conftest.py b/watchtower-plugin/tests/conftest.py deleted file mode 100644 index 7a7be94..0000000 --- a/watchtower-plugin/tests/conftest.py +++ /dev/null @@ -1,122 +0,0 @@ -from pathlib import Path -import subprocess - -from pyln.testing.fixtures import * # noqa: F401,F403 -from pyln.testing.utils import BITCOIND_CONFIG, TailableProc - -WT_PLUGIN = Path("~/.cargo/bin/watchtower-client").expanduser() -TEOSD_CONFIG = { - "btc_network": "regtest", - "polling_delta": 0, -} - - -def write_toml_config(filename, opts): - with open(filename, "w") as f: - for k, v in opts.items(): - if isinstance(v, str): - f.write('{} = "{}"\n'.format(k, v)) - else: - f.write("{} = {}\n".format(k, v)) - - -class TeosCLI: - def __init__(self, directory="/tmp/watchtower-test"): - self.datadir = directory - - def _call(self, method_name, *args): - try: - r = subprocess.run( - ["teos-cli", f"--datadir={self.datadir}/teos", method_name, *args], - capture_output=True, - text=True, - ) - if r.returncode != 0: - result = ValueError(f"Unknown method {method_name}") - else: - result = json.loads(r.stdout) - except json.JSONDecodeError: - result = None - return result - - def __getattr__(self, name): - if name.startswith("__") and name.endswith("__"): - # Prevent RPC calls for non-existing python internal attribute - # access. If someone tries to get an internal attribute - # of RawProxy instance, and the instance does not have this - # attribute, we do not want the bogus RPC call to happen. - raise AttributeError - - # Create a callable to do the actual call - f = lambda *args: self._call(name, *args) # noqa: E731 - - # Make debuggers show rather than > - f.__name__ = name - return f - - -class TeosD(TailableProc): - def __init__(self, bitcoind_rpcport, directory="/tmp/watchtower-test"): - self.teos_dir = os.path.join(directory, "teos") - self.prefix = "teosd" - TailableProc.__init__(self, self.teos_dir) - self.cli = TeosCLI(directory) - - if not os.path.exists(self.teos_dir): - os.makedirs(self.teos_dir) - - self.cmd_line = [ - "teosd", - f"--datadir={self.teos_dir}", - f'--btcrpcuser={BITCOIND_CONFIG["rpcuser"]}', - f'--btcrpcpassword={BITCOIND_CONFIG["rpcpassword"]}', - f"--btcrpcport={bitcoind_rpcport}", - ] - - self.conf_file = os.path.join(self.teos_dir, "teos.toml") - write_toml_config(self.conf_file, TEOSD_CONFIG) - - def start(self, overwrite_key=False): - if overwrite_key: - self.cmd_line.append("--overwritekey") - TailableProc.start(self) - self.wait_for_log("Tower ready") - - logging.info("TeosD started") - - def stop(self): - self.cli.stop() - self.wait_for_log("Shutting down tower") - - return TailableProc.stop(self) - - -@pytest.fixture -def teosd(bitcoind, directory): - # Set the user data dir for the watchtower-plugin so it uses a unique one per test. - os.environ["TOWERS_DATA_DIR"] = os.path.join(directory, "watchtower") - - teosd = TeosD(directory=directory, bitcoind_rpcport=bitcoind.rpcport) - teosd.start() - yield teosd - - teosd.stop() - - -@pytest.hookimpl(tryfirst=True, hookwrapper=True) -def pytest_runtest_makereport(item, call): - # execute all other hooks to obtain the report object - outcome = yield - rep = outcome.get_result() - - # set a report attribute for each phase of a call, which can - # be "setup", "call", "teardown" - - setattr(item, "rep_" + rep.when, rep) - - -@pytest.fixture(scope="function", autouse=True) -def log_name(request): - # Here logging is used, you can use whatever you want to use for logs - logging.info("Starting '{}'".format(request.node.name)) diff --git a/watchtower-plugin/tests/pyproject.toml b/watchtower-plugin/tests/pyproject.toml deleted file mode 100644 index f11efae..0000000 --- a/watchtower-plugin/tests/pyproject.toml +++ /dev/null @@ -1,21 +0,0 @@ -[tool.poetry] -name = "tests" -version = "0.1.2" -description = "watchtower-plugin tests" -authors = ["Sergi Delgado Segura "] -license = "MIT" - -[tool.poetry.dependencies] -python = "^3.9" -black = "^22.6.0" - -[tool.poetry.dev-dependencies] -pytest = "^7.1.2" -pytest-timeout = "^2.1.0" -pyln-testing = "^24.2.1" -pyln-client = "^24.2.1" - - -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" diff --git a/watchtower-plugin/tests/test.py b/watchtower-plugin/tests/test.py deleted file mode 100644 index 7b4657c..0000000 --- a/watchtower-plugin/tests/test.py +++ /dev/null @@ -1,243 +0,0 @@ -import pytest -from conftest import WT_PLUGIN - - -def change_endianness(x): - """Changes the endianness (from BE to LE and vice versa) of a given value. - - :param x: Given value which endianness will be changed. - :type x: hex str - :return: The opposite endianness representation of the given value. - :rtype: hex str - """ - - b = bytes.fromhex(x) - return b[::-1].hex() - - -def test_watchtower(node_factory, bitcoind, teosd): - """ - Test watchtower hook. - - l1 and l2 open a channel, make a couple of updates and then l1 cheats on - l2 while that one is offline. The watchtower plugin meanwhile stashes all - the penalty transactions and we release the one matching the offending - commitment transaction. - """ - - 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"] - l2.rpc.registertower(tower_id) - - # Force a new commitment - l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"]) - tx = l1.rpc.dev_sign_last_tx(l2.info["id"])["tx"] - - # Now make sure it is out of date - l1.rpc.pay(l2.rpc.invoice(25000000, "lbl2", "desc2")["bolt11"]) - - # l2 stops watching the chain, allowing the watchtower to react - l2.stop() - - # Now l1 cheats - dispute_txid = bitcoind.rpc.sendrawtransaction(tx) - 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 _: {"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 - assert l2.rpc.getappointment(tower_id, locator)["status"] == "being_watched" - - # Confirm the dispute so the tower can react with the penalty - bitcoind.generate_block() - l1.daemon.wait_for_log("State changed from FUNDING_SPEND_SEEN to ONCHAIN") - penalty_txid = bitcoind.rpc.getrawmempool()[0] - - # The channel still exists between the two peers, but it's on chain - 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 - for i in range(101): - bitcoind.generate_block() - if i < 100: - assert l2.rpc.getappointment(tower_id, locator)["status"] == "dispute_responded" - else: - # Once the channel gets irrevocably resolved the tower will forget about it - assert l2.rpc.getappointment(tower_id, locator) == { - "error": "Appointment not found", - "error_code": 36, - } - - # Make sure the penalty outputs are in l2's wallet - fund_txids = [o["txid"] for o in l2.rpc.listfunds()["outputs"]] - assert penalty_txid in fund_txids - - -@pytest.mark.timeout(60) -def test_unreachable_watchtower(node_factory, bitcoind, teosd): - # Set the max retry interval to 1 sec so we know how much to wait for the next retry attempt - max_interval_time = 1 - l1, l2 = node_factory.line_graph( - 2, - opts=[ - {}, - { - "plugin": WT_PLUGIN, - "dev-watchtower-max-retry-interval": max_interval_time, - }, - ], - ) - - # We need to register l2 with the tower - tower_id = teosd.cli.gettowerinfo()["tower_id"] - l2.rpc.registertower(tower_id) - - # Stop the tower - teosd.stop() - - # Make a new payment with an unreachable tower - l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"]) - assert l2.rpc.gettowerinfo(tower_id)["status"] == "temporary_unreachable" - assert l2.rpc.gettowerinfo(tower_id)["pending_appointments"] - - # Start the tower and check the automatic backoff works - teosd.start() - l2.daemon.wait_for_log(f"Retry strategy succeeded for {tower_id}") - - assert l2.rpc.gettowerinfo(tower_id)["status"] == "reachable" - - -def test_auto_retry_watchtower(node_factory, bitcoind, teosd): - # The plugin is set to give up on retrying straight-away so we can test this fast. - l1, l2 = node_factory.line_graph( - 2, - opts=[ - {}, - { - "plugin": WT_PLUGIN, - "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, - }, - ], - ) - - # We need to register l2 with the tower - tower_id = teosd.cli.gettowerinfo()["tower_id"] - l2.rpc.registertower(tower_id) - - # Stop the tower - teosd.stop() - - # Make a new payment with an unreachable tower - l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"]) - - # Wait until the tower has been flagged as unreachable - l2.daemon.wait_for_log("Starting to idle") - assert l2.rpc.gettowerinfo(tower_id)["status"] == "unreachable" - assert l2.rpc.gettowerinfo(tower_id)["pending_appointments"] - - # Start the tower and retry it - teosd.start() - - l2.daemon.wait_for_log(f"Finished idling. Flagging {tower_id} for retry") - l2.daemon.wait_for_log(f"Retry strategy succeeded for {tower_id}") - assert l2.rpc.gettowerinfo(tower_id)["status"] == "reachable" - - -def test_manually_retry_watchtower(node_factory, bitcoind, teosd): - # The plugin is set to give up on retrying straight-away so we can test this fast. - l1, l2 = node_factory.line_graph( - 2, - opts=[ - {}, - { - "plugin": WT_PLUGIN, - "watchtower-max-retry-time": 0, - }, - ], - ) - - # We need to register l2 with the tower - tower_id = teosd.cli.gettowerinfo()["tower_id"] - l2.rpc.registertower(tower_id) - - # Stop the tower - teosd.stop() - - # Make a new payment with an unreachable tower - l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"]) - - # Wait until the tower has been flagged as unreachable - l2.daemon.wait_for_log("Starting to idle") - assert l2.rpc.gettowerinfo(tower_id)["status"] == "unreachable" - assert l2.rpc.gettowerinfo(tower_id)["pending_appointments"] - - # Start the tower and retry it - teosd.start() - - # Manual retry - l2.rpc.retrytower(tower_id) - l2.daemon.wait_for_log(f"Manually finished idling. Flagging {tower_id} for retry") - l2.daemon.wait_for_log(f"Retry strategy succeeded for {tower_id}") - assert l2.rpc.gettowerinfo(tower_id)["status"] == "reachable" - - -def test_misbehaving_watchtower(node_factory, bitcoind, teosd, directory): - 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"] - l2.rpc.registertower(tower_id) - - # Restart overwriting the tower private key - teosd.stop() - teosd.start(overwrite_key=True) - - # Make a new payment and check the state - l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"]) - assert l2.rpc.gettowerinfo(tower_id)["status"] == "misbehaving" - assert l2.rpc.gettowerinfo(tower_id)["misbehaving_proof"] - - -def test_get_appointment(node_factory, bitcoind, teosd, directory): - l1, l2 = node_factory.line_graph(2, opts=[{}, {"plugin": WT_PLUGIN}]) - - # We need to register l2 with the tower - tower_id = teosd.cli.gettowerinfo()["tower_id"] - l2.rpc.registertower(tower_id) - - # Force a new commitment - l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"]) - tx = l1.rpc.dev_sign_last_tx(l2.info["id"])["tx"] - - # Now make sure it is out of date - l1.rpc.pay(l2.rpc.invoice(25000000, "lbl2", "desc2")["bolt11"]) - - # Now l1 cheats - dispute_txid = bitcoind.rpc.sendrawtransaction(tx) - locator = change_endianness(dispute_txid[32:]) - - # Check the appointment before mining a block - appointment = l2.rpc.getappointment(tower_id, locator)["appointment"] - assert "locator" in appointment and "encrypted_blob" in appointment and "to_self_delay" in appointment - - # And after. Now this should be a tracker - bitcoind.generate_block() - teosd.wait_for_log("New tracker added") - tracker = l2.rpc.getappointment(tower_id, locator)["appointment"] - assert "dispute_txid" in tracker and "penalty_txid" in tracker and "penalty_rawtx" in tracker - - # Manually stop l2, otherwise the tower may be stopped before the tower client and we may get some BROKEN logs. - l2.stop()