mirror of
https://github.com/talaia-labs/rust-teos.git
synced 2026-08-13 12:33:22 +02:00
Compare commits
72 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be344ecc52 | ||
|
|
3e652eeb28 | ||
|
|
70ea8b5625 | ||
|
|
4824cb3372 | ||
|
|
ce4dafcab8 | ||
|
|
3fd4f41325 | ||
|
|
316b0c1d87 | ||
|
|
469e247ac3 | ||
|
|
42db02157e | ||
|
|
b3a621a781 | ||
|
|
4e47db227e | ||
|
|
cc0d18393d | ||
|
|
9248f3a2b9 | ||
|
|
1684571808 | ||
|
|
b8a996e4f0 | ||
|
|
011ba645bb | ||
|
|
609da2dc23 | ||
|
|
2bf359582d | ||
|
|
81a659c0f6 | ||
|
|
bb2df6a700 | ||
|
|
9d0d3af398 | ||
|
|
d30fc7d121 | ||
|
|
e1085104eb | ||
|
|
f6decde5ff | ||
|
|
c9f82bce9f | ||
|
|
95f1c4c21d | ||
|
|
997f77f695 | ||
|
|
37a75ec6e7 | ||
|
|
21acbe0185 | ||
|
|
99dd5bbd15 | ||
|
|
56ede5383c | ||
|
|
52ebbe6e06 | ||
|
|
7ea92bbdd5 | ||
|
|
534d6390ba | ||
|
|
a515118eea | ||
|
|
e6495c31aa | ||
|
|
a4accedfee | ||
|
|
ddd4bd9ef1 | ||
|
|
96efd4320c | ||
|
|
658fcca0ce | ||
|
|
1790fe31e0 | ||
|
|
cab6151ccc | ||
|
|
fad3ad1c08 | ||
|
|
8df58f99d1 | ||
|
|
6cc466c359 | ||
|
|
092f549f7e | ||
|
|
6cbc8df165 | ||
|
|
565eda42ba | ||
|
|
bacac072e9 | ||
|
|
ed78c89613 | ||
|
|
59c770b25b | ||
|
|
7093882f47 | ||
|
|
1a89c5da70 | ||
|
|
2bf555c07f | ||
|
|
7e0f0f0981 | ||
|
|
7fc30577ac | ||
|
|
6caf452daf | ||
|
|
c0f4ebc82c | ||
|
|
d1edc98a85 | ||
|
|
2229251702 | ||
|
|
94624fbe0b | ||
|
|
ca577c558b | ||
|
|
886e0fffe5 | ||
|
|
801ef5d808 | ||
|
|
67aa345de4 | ||
|
|
7dc3fcd2cc | ||
|
|
b60299c881 | ||
|
|
939d87c0f0 | ||
|
|
076c972913 | ||
|
|
f63fd14184 | ||
|
|
df948753d3 | ||
|
|
f628b358db |
50 changed files with 5479 additions and 4990 deletions
51
.github/workflows/build.yaml
vendored
51
.github/workflows/build.yaml
vendored
|
|
@ -1,6 +1,10 @@
|
|||
name: Continuous Integration Checks
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
@ -16,13 +20,15 @@ jobs:
|
|||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust ${{ matrix.toolchain }} toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
override: true
|
||||
profile: minimal
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build on Rust ${{ matrix.toolchain }}
|
||||
run: |
|
||||
cargo build ${{ matrix.arguments }} --verbose --color always
|
||||
|
|
@ -34,16 +40,43 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
components: rustfmt, clippy
|
||||
- name: Install Protoc
|
||||
uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Run rustfmt
|
||||
run: |
|
||||
cargo fmt --verbose --check -- --color always
|
||||
- name: Run clippy
|
||||
run: |
|
||||
cargo clippy --all-features --all-targets --color always -- --deny warnings
|
||||
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
|
||||
|
||||
|
|
|
|||
72
.github/workflows/cln-plugin.yaml
vendored
72
.github/workflows/cln-plugin.yaml
vendored
|
|
@ -1,47 +1,73 @@
|
|||
name: CI tests for CLN watchtower-plugin
|
||||
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
bitcoind_version: 0.20.1
|
||||
cln_version: 0.12.1
|
||||
bitcoind_version: "27.0"
|
||||
cln_version: "24.11.1"
|
||||
|
||||
jobs:
|
||||
check-cln-cache:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
cache-hit: ${{ steps.cache.outputs.cache-hit }}
|
||||
steps:
|
||||
- name: Check CLN cache
|
||||
id: cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: lightning
|
||||
key: ${{ runner.os }}-build-cache-cln-dev-v${{ env.cln_version }}
|
||||
|
||||
cache-cln:
|
||||
runs-on: ubuntu-latest
|
||||
needs: check-cln-cache
|
||||
if: ${{ needs.check-cln-cache.outputs.cache-hit != 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.9'
|
||||
check-latest: true
|
||||
- uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Create CLN cache
|
||||
id: cache-cln
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-cln-dev
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: lightning
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-v${{ env.cln_version }}
|
||||
key: ${{ runner.os }}-build-cache-cln-dev-v${{ env.cln_version }}
|
||||
- name: Compile CLN
|
||||
env:
|
||||
env:
|
||||
PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring
|
||||
if: ${{ steps.cache-cln.outputs.cache-hit != 'true' }}
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install gettext
|
||||
sudo apt-get update && sudo apt-get install -y gettext
|
||||
git clone https://github.com/ElementsProject/lightning.git && cd lightning && git checkout v${{ env.cln_version }}
|
||||
pip install --user poetry && poetry install
|
||||
./configure --enable-developer && poetry run make
|
||||
./configure && poetry run make -j 8
|
||||
|
||||
cln-plugin:
|
||||
needs: cache-cln
|
||||
if: ${{ always() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.9'
|
||||
check-latest: true
|
||||
- uses: arduino/setup-protoc@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
components: rustfmt, clippy
|
||||
- name: Install bitcoind
|
||||
run: |
|
||||
wget https://bitcoincore.org/bin/bitcoin-core-${{ env.bitcoind_version }}/bitcoin-${{ env.bitcoind_version }}-x86_64-linux-gnu.tar.gz
|
||||
|
|
@ -49,24 +75,22 @@ jobs:
|
|||
ln -s $(pwd)/bitcoin-${{ env.bitcoind_version }}/bin/bitcoin* /usr/local/bin
|
||||
- name: Load CLN cache
|
||||
id: cache-cln
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-cln-dev
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: lightning
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-v${{ env.cln_version }}
|
||||
key: ${{ runner.os }}-build-cache-cln-dev-v${{ env.cln_version }}
|
||||
- name: Link CLN
|
||||
run: |
|
||||
cd lightning && sudo make install
|
||||
cd lightning && sudo PATH=$PATH make install
|
||||
- name: Install teos and the plugin
|
||||
run: |
|
||||
cargo install --locked --path teos
|
||||
cargo install --locked --path teos
|
||||
cargo install --locked --path watchtower-plugin
|
||||
- name: Add test dependencies
|
||||
run: |
|
||||
cd watchtower-plugin/tests
|
||||
pip install --user poetry && poetry install
|
||||
pip install --user poetry && poetry install --no-root
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd watchtower-plugin/tests
|
||||
DEVELOPER=1 SLOW_MACHINE=1 poetry run pytest test.py --log-cli-level=INFO -s
|
||||
VALGRIND=0 SLOW_MACHINE=1 poetry run pytest test.py --log-cli-level=INFO -s
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,4 +1,5 @@
|
|||
target
|
||||
__pycache__
|
||||
.vscode
|
||||
.idea
|
||||
.idea
|
||||
result
|
||||
|
|
|
|||
|
|
@ -72,6 +72,35 @@ pub struct Responder {
|
|||
## Test Coverage
|
||||
Tests should be provided to cover both positive and negative conditions. Tests should cover both the proper execution as well as all the covered error paths. PR with no proper test coverage will not be merged.
|
||||
|
||||
## Signing Commits
|
||||
## Git conventions
|
||||
|
||||
### Commits, titles, and descriptions
|
||||
|
||||
- Changes must be split logically in commits, such that a commit is self-contained
|
||||
- In general terms, all commits need to pass the test suite. There may be some exceptions to this rule if the change you are working on touches several components of the codebase and it makes more sense to split the change by component (or group of components)
|
||||
- Commit titles need to be short and explanatory. If we are, for instance, adding an RPC command to the backend, "Adds command X to the backend" will be a good short description, "Add command" or "Fix #123" where #123 is an issue referencing this feature **IS NOT**
|
||||
- Descriptions can be provided to give more context about what has been fixed and how
|
||||
|
||||
### Pull requests
|
||||
|
||||
- Pull request titles need to be explanatory, in the same way, commits titles were. If a PR includes a single commit, they can share the title, otherwise, a general title of what we are trying to achieve is required. **DO NOT REFERENCE ISSUES IN PULL REQUEST TITLES**, save that for the PR description
|
||||
- PR descriptions need to guide the reviewer into what has been changed. You can reference issues here. If the PR is a fix of a simple issue, "Fix #123" may suffice, however, if it involves several changes, a proper explanation of both what has been fixed and how is due. These are two good examples of PR descriptions, both long and short: [188](https://github.com/talaia-labs/rust-teos/pull/188), [194](https://github.com/talaia-labs/rust-teos/pull/194)
|
||||
- **WE DO NOT PILE "fix" COMMITS IN A PULL REQUEST**, that is, if some fixes are requested by reviewers, or something was missing from our original approach, it needs to be squashed. Do **NOT** do this:
|
||||
|
||||
```
|
||||
886b0ff Adds X functionality to component Y
|
||||
801ff5d Fixes the previous commit because Z
|
||||
67ac345 Addresses review comments
|
||||
7dc7fcd Updates X because G was missing
|
||||
b60999c Adds missing test
|
||||
...
|
||||
```
|
||||
|
||||
- Create a new branch to work on your pull request. **DO NOT** work from the master branch of your fork*
|
||||
- **DO NOT** merge master into your branch, rebase master instead*
|
||||
|
||||
\* If you're not sure how to handle this, check external documentation on how to manage multiple remotes for the same repository.
|
||||
|
||||
### Signing Commits
|
||||
|
||||
We require that all commits to be merged into master are signed. You can enable commit signing on GitHub by following [Signing commits](https://help.github.com/en/github/authenticating-to-github/signing-commits).
|
||||
|
|
|
|||
3160
Cargo.lock
generated
3160
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,8 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
members = [
|
||||
"teos",
|
||||
"teos-common",
|
||||
"watchtower-plugin"
|
||||
]
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
- `bitcoind`
|
||||
|
||||
### Minimum Supported Rust Version (MSRV)
|
||||
FIXME: Define MSRV
|
||||
Refer to [toolchain](./rust-toolchain.toml)
|
||||
|
||||
### Installing Rust
|
||||
Refer to [rust-lang.org](https://www.rust-lang.org/tools/install).
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
**THIS IS CURRENTLY WIP**
|
||||
|
||||
# The Eye of Satoshi (rust-teos)
|
||||
|
||||
The Eye of Satoshi is a Lightning watchtower compliant with [BOLT13](https://github.com/sr-gi/bolt13), written in Rust.
|
||||
|
||||
[](https://discord.gg/EyVbrNMDUP)
|
||||
[](https://github.com/talaia-labs/rust-teos/actions/workflows/build.yaml)
|
||||
[](https://github.com/talaia-labs/rust-teos/releases/latest)
|
||||
|
||||
|
||||
`rust-teos` consists of two main crates:
|
||||
|
||||
- `teos`: including the tower's main functionality (server-side) and a CLI. Compiling this crate will generate two binaries: `teosd` and `teos-cli`.
|
||||
|
|
@ -20,6 +23,8 @@ Refer to [INSTALL.md](INSTALL.md)
|
|||
|
||||
Make sure `bitcoind` is running before running `teosd` (it will fail at startup if it cannot connect to `bitcoind`). [Here](DEPENDENCIES.md#installing-bitcoind) you can find a sample bitcoin.conf.
|
||||
|
||||
Please see [Docker instructions](docker/README.md) for instructions on how to set up `teosd` in Docker.
|
||||
|
||||
### Starting the tower daemon ♖
|
||||
|
||||
Once installed, you can start the tower by running:
|
||||
|
|
|
|||
10
coffee.yml
Normal file
10
coffee.yml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
plugin:
|
||||
name: rust-teos
|
||||
version: 0.2.0
|
||||
lang: rust
|
||||
install: |
|
||||
cargo build --release --locked --package watchtower-plugin
|
||||
cp target/release/watchtower-client .
|
||||
cargo clean
|
||||
main: watchtower-client
|
||||
49
docker/Dockerfile
Normal file
49
docker/Dockerfile
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Use the rust image as the base image for the build stage
|
||||
FROM rust:latest AS builder
|
||||
|
||||
# Copy the rust-teos source code
|
||||
COPY . /tmp/rust-teos
|
||||
|
||||
# Install the dependencies required for building rust-teos
|
||||
RUN apt-get update\
|
||||
&& apt-get -y --no-install-recommends install libffi-dev libssl-dev musl-tools pkg-config
|
||||
|
||||
RUN cd /tmp/rust-teos \
|
||||
&& rustup target add x86_64-unknown-linux-musl \
|
||||
# Rustfmt is needed to format the grpc stubs generated by tonic
|
||||
&& rustup component add rustfmt \
|
||||
# Cross compile with musl as the target, so teosd can run on alpine
|
||||
&& RUSTFLAGS='-C target-feature=+crt-static' cargo build --manifest-path=teos/Cargo.toml --locked --release --target x86_64-unknown-linux-musl
|
||||
|
||||
# Use a new stage with a smaller base image to reduce image size
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk update && apk upgrade
|
||||
|
||||
# UID and GID for the teosd user
|
||||
ENV TEOS_UID=1001 TEOS_GID=1001
|
||||
|
||||
# Copy the teos binaries from the build stage to the new stage
|
||||
COPY --from=builder \
|
||||
/tmp/rust-teos/target/x86_64-unknown-linux-musl/release/teosd \
|
||||
/tmp/rust-teos/target/x86_64-unknown-linux-musl/release/teos-cli /usr/local/bin/
|
||||
|
||||
# Copy the entrypoint script to the container
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
|
||||
# Set the entrypoint script as executable and add running user
|
||||
RUN chmod +x /entrypoint.sh \
|
||||
&& addgroup -g ${TEOS_GID} -S teos \
|
||||
&& adduser -S -G teos -u ${TEOS_UID} teos
|
||||
|
||||
# Expose the default port used by teosd
|
||||
EXPOSE 9814/tcp
|
||||
|
||||
# Switch user so that we don't run stuff as root
|
||||
USER teos
|
||||
|
||||
# Create the teos data directory
|
||||
RUN mkdir /home/teos/.teos
|
||||
|
||||
# Start teosd when the container starts
|
||||
ENTRYPOINT [ "/entrypoint.sh" ]
|
||||
113
docker/README.md
Normal file
113
docker/README.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
## Running `teosd` in a docker container
|
||||
A `teos` image can be built from the Dockerfile located in `docker`. You can create the image by running:
|
||||
|
||||
cd rust-teos
|
||||
docker build -f docker/Dockerfile -t teos .
|
||||
|
||||
Then we can create a container by running:
|
||||
|
||||
docker run -it teos
|
||||
|
||||
One way to feed `teos` custom config options is to set environment variables:
|
||||
|
||||
docker run -it -e <ENV_VARIABLES> teos
|
||||
|
||||
Notice that the ENV variables are optional, if unset the corresponding default setting is used. The following ENVs are available:
|
||||
|
||||
```
|
||||
- API_BIND=<teos_api_hostname>
|
||||
- API_PORT=<teos_api_port>
|
||||
- RPC_BIND=<teos_rpc_hostname>
|
||||
- RPC_PORT=<teos_rpc_port>
|
||||
- BTC_NETWORK=<btc_network>
|
||||
- BTC_RPC_CONNECT=<btc_node_hostname>
|
||||
- BTC_RPC_PORT=<btc_node_port>
|
||||
- BTC_RPC_USER=<btc_rpc_username>
|
||||
- BTC_RPC_PASSWORD=<btc_rpc_password>
|
||||
# The following options can be set turned on by setting them to "true"
|
||||
- DEBUG=<debug_bool>
|
||||
- DEPS_DEBUG=<deps_debug_bool>
|
||||
- OVERWRITE_KEY=<overwrite_key_bool>
|
||||
- FORCE_UPDATE=<force_update_bool>
|
||||
```
|
||||
|
||||
### 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=<btc_rpc_username> \
|
||||
-e BTC_RPC_PASSWORD=<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=<btc_rpc_username> \
|
||||
-e BTC_RPC_PASSWORD=<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 <CONTAINER_NAME> 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`.
|
||||
66
docker/entrypoint.sh
Executable file
66
docker/entrypoint.sh
Executable file
|
|
@ -0,0 +1,66 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Define the start command
|
||||
START_COMMAND="teosd"
|
||||
|
||||
# Set the API bind address
|
||||
if [[ ! -z ${API_BIND} ]]; then
|
||||
START_COMMAND="$START_COMMAND --apibind $API_BIND"
|
||||
fi
|
||||
|
||||
# Set the API port
|
||||
if [[ ! -z ${API_PORT} ]]; then
|
||||
START_COMMAND="$START_COMMAND --apiport $API_PORT"
|
||||
fi
|
||||
|
||||
# Set the RPC bind address
|
||||
if [[ ! -z ${RPC_BIND} ]]; then
|
||||
START_COMMAND="$START_COMMAND --rpcbind $RPC_BIND"
|
||||
fi
|
||||
|
||||
# Set the RPC port
|
||||
if [[ ! -z ${RPC_PORT} ]]; then
|
||||
START_COMMAND="$START_COMMAND --rpcport $RPC_PORT"
|
||||
fi
|
||||
|
||||
# Set the Bitcoin network
|
||||
if [[ ! -z ${BTC_NETWORK} ]]; then
|
||||
START_COMMAND="$START_COMMAND --btcnetwork $BTC_NETWORK"
|
||||
fi
|
||||
|
||||
# Set the Bitcoin RPC credentials
|
||||
if [[ ! -z ${BTC_RPC_USER} ]]; then
|
||||
START_COMMAND="$START_COMMAND --btcrpcuser $BTC_RPC_USER"
|
||||
fi
|
||||
|
||||
if [[ ! -z ${BTC_RPC_PASSWORD} ]]; then
|
||||
START_COMMAND="$START_COMMAND --btcrpcpassword $BTC_RPC_PASSWORD"
|
||||
fi
|
||||
|
||||
# Set the Bitcoin RPC connection details
|
||||
if [[ ! -z ${BTC_RPC_CONNECT} ]]; then
|
||||
START_COMMAND="$START_COMMAND --btcrpcconnect $BTC_RPC_CONNECT"
|
||||
fi
|
||||
|
||||
if [[ ! -z ${BTC_RPC_PORT} ]]; then
|
||||
START_COMMAND="$START_COMMAND --btcrpcport $BTC_RPC_PORT"
|
||||
fi
|
||||
|
||||
if [ "${DEBUG}" == "true" ]; then
|
||||
START_COMMAND="$START_COMMAND --debug"
|
||||
fi
|
||||
|
||||
if [ "${DEPS_DEBUG}" == "true" ]; then
|
||||
START_COMMAND="$START_COMMAND --depsdebug"
|
||||
fi
|
||||
|
||||
if [ "${OVERWRITE_KEY}" == "true" ]; then
|
||||
START_COMMAND="$START_COMMAND --overwritekey"
|
||||
fi
|
||||
|
||||
if [ "${FORCE_UPDATE}" == "true" ]; then
|
||||
START_COMMAND="$START_COMMAND --forceupdate"
|
||||
fi
|
||||
|
||||
# Start the TEOS daemon
|
||||
$START_COMMAND
|
||||
116
flake.lock
generated
Normal file
116
flake.lock
generated
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1758758545,
|
||||
"narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1758782550,
|
||||
"narHash": "sha256-olCvyP5r6+HQTl2EUudtjlA5UammsBpkzAl0l9+utZc=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "32f4e350c03cc5762be811e9c700e8696cd13c02",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1758589230,
|
||||
"narHash": "sha256-zMTCFGe8aVGTEr2RqUi/QzC1nOIQ0N1HRsbqB4f646k=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d1d883129b193f0b495d75c148c2c3a7d95789a0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-25.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"fenix": "fenix",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1758620797,
|
||||
"narHash": "sha256-Ly4rHgrixFMBnkbMursVt74mxnntnE6yVdF5QellJ+A=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "905641f3520230ad6ef421bcf5da9c6b49f2479b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
114
flake.nix
Normal file
114
flake.nix
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
{
|
||||
description = "Build teos (The Eye of Satoshi) server and plugin";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
||||
|
||||
crane.url = "github:ipetkov/crane";
|
||||
|
||||
fenix = {
|
||||
url = "github:nix-community/fenix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
nixpkgs,
|
||||
crane,
|
||||
fenix,
|
||||
flake-utils,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
|
||||
inherit (pkgs) lib;
|
||||
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain fenix.packages.${system}.stable.minimalToolchain;
|
||||
|
||||
env = {
|
||||
PROTOC = "${pkgs.protobuf}/bin/protoc";
|
||||
PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig";
|
||||
LD_LIBRARY_PATH = lib.makeLibraryPath [ pkgs.openssl.out ];
|
||||
};
|
||||
commonArgs = {
|
||||
inherit env;
|
||||
strictDeps = true;
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.pkg-config
|
||||
pkgs.rustfmt # needed for tonic build
|
||||
pkgs.cacert
|
||||
pkgs.openssl.dev
|
||||
];
|
||||
|
||||
buildInputs =
|
||||
[ ]
|
||||
++ lib.optionals pkgs.stdenv.isDarwin [
|
||||
# Additional darwin specific inputs can be set here
|
||||
pkgs.libiconv
|
||||
];
|
||||
};
|
||||
|
||||
fileSetForCrate =
|
||||
crate:
|
||||
lib.fileset.toSource {
|
||||
root = ./.;
|
||||
fileset = lib.fileset.unions [
|
||||
./Cargo.toml
|
||||
./Cargo.lock
|
||||
./teos-common
|
||||
./teos
|
||||
./watchtower-plugin
|
||||
crate
|
||||
];
|
||||
};
|
||||
|
||||
plugin = craneLib.buildPackage (
|
||||
commonArgs
|
||||
// {
|
||||
pname = "watchtower-plugin";
|
||||
cargoExtraArgs = "-p watchtower-plugin";
|
||||
src = fileSetForCrate ./watchtower-plugin;
|
||||
inherit (craneLib.crateNameFromCargoToml { cargoToml = ./watchtower-plugin/Cargo.toml; }) version;
|
||||
}
|
||||
);
|
||||
teos = craneLib.buildPackage (
|
||||
commonArgs
|
||||
// {
|
||||
pname = "teos";
|
||||
cargoExtraArgs = "-p teos";
|
||||
src = fileSetForCrate ./teos;
|
||||
inherit (craneLib.crateNameFromCargoToml { cargoToml = ./teos/Cargo.toml; }) version;
|
||||
}
|
||||
);
|
||||
in
|
||||
{
|
||||
packages = {
|
||||
inherit plugin teos;
|
||||
default = teos;
|
||||
};
|
||||
|
||||
apps = {
|
||||
plugin = flake-utils.lib.mkApp { drv = plugin; };
|
||||
teos = flake-utils.lib.mkApp { drv = teos; };
|
||||
};
|
||||
|
||||
formatter = pkgs.nixfmt-tree;
|
||||
|
||||
checks = {
|
||||
inherit teos plugin;
|
||||
};
|
||||
|
||||
devShells.default = craneLib.devShell {
|
||||
inherit env;
|
||||
packages = commonArgs.buildInputs ++ commonArgs.nativeBuildInputs;
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
6
rust-toolchain.toml
Normal file
6
rust-toolchain.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[toolchain]
|
||||
channel = "1.81.0"
|
||||
components = [
|
||||
"rustfmt",
|
||||
"clippy",
|
||||
]
|
||||
|
|
@ -2,26 +2,26 @@
|
|||
name = "teos-common"
|
||||
version = "0.2.0"
|
||||
authors = ["Sergi Delgado Segura <sergi.delgado.s@gmail.com>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
# General
|
||||
hex = { version = "0.4.3", features = [ "serde" ] }
|
||||
prost = "0.9"
|
||||
prost = "0.12"
|
||||
rusqlite = { version = "0.26.0", features = [ "bundled", "limits" ] }
|
||||
serde = "1.0.130"
|
||||
serde_json = "1.0"
|
||||
tonic = "0.6"
|
||||
tonic = "0.11"
|
||||
|
||||
# Crypto
|
||||
rand = "0.8.4"
|
||||
chacha20poly1305 = "0.8.0"
|
||||
|
||||
# Bitcoin and Lightning
|
||||
bitcoin = { version = "0.28.0", features = [ "use-serde" ] }
|
||||
lightning = "0.0.108"
|
||||
bitcoin = { version = "0.32.0", features = [ "serde" ] }
|
||||
lightning = "0.1.0"
|
||||
|
||||
[build-dependencies]
|
||||
tonic-build = "0.6"
|
||||
tonic-build = "0.11"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
//! Cryptography module, used in the interaction between users and towers.
|
||||
|
||||
use rand::distributions::Uniform;
|
||||
use rand::Rng;
|
||||
|
||||
use chacha20poly1305::aead::{Aead, NewAead};
|
||||
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
|
||||
use rand::distributions::Uniform;
|
||||
use rand::Rng;
|
||||
|
||||
use bitcoin::consensus;
|
||||
use bitcoin::hashes::{sha256, Hash};
|
||||
|
|
@ -20,7 +19,7 @@ pub enum DecryptingError {
|
|||
}
|
||||
|
||||
/// Shadows [message_signing::sign].
|
||||
pub fn sign(msg: &[u8], sk: &SecretKey) -> Result<String, Error> {
|
||||
pub fn sign(msg: &[u8], sk: &SecretKey) -> String {
|
||||
message_signing::sign(msg, sk)
|
||||
}
|
||||
|
||||
|
|
@ -47,8 +46,8 @@ pub fn encrypt(
|
|||
) -> Result<Vec<u8>, chacha20poly1305::aead::Error> {
|
||||
// Defaults is [0; 12]
|
||||
let nonce = Nonce::default();
|
||||
let _k = sha256::Hash::hash(secret);
|
||||
let key = Key::from_slice(&_k);
|
||||
let k = sha256::Hash::hash(secret.as_byte_array());
|
||||
let key = Key::from_slice(k.as_byte_array());
|
||||
|
||||
let cypher = ChaCha20Poly1305::new(key);
|
||||
cypher.encrypt(&nonce, consensus::serialize(message).as_ref())
|
||||
|
|
@ -64,8 +63,8 @@ pub fn encrypt(
|
|||
pub fn decrypt(encrypted_blob: &[u8], secret: &Txid) -> Result<Transaction, DecryptingError> {
|
||||
// Defaults is [0; 12]
|
||||
let nonce = Nonce::default();
|
||||
let _k = sha256::Hash::hash(secret);
|
||||
let key = Key::from_slice(&_k);
|
||||
let k = sha256::Hash::hash(secret.as_byte_array());
|
||||
let key = Key::from_slice(k.as_byte_array());
|
||||
|
||||
let cypher = ChaCha20Poly1305::new(key);
|
||||
|
||||
|
|
@ -81,17 +80,13 @@ pub fn decrypt(encrypted_blob: &[u8], secret: &Txid) -> Result<Transaction, Decr
|
|||
pub fn get_random_bytes(size: usize) -> Vec<u8> {
|
||||
let mut rng = rand::thread_rng();
|
||||
let uniform_u8 = Uniform::new(u8::MIN, u8::MAX);
|
||||
let v: Vec<u8> = (&mut rng).sample_iter(uniform_u8).take(size).collect();
|
||||
|
||||
v
|
||||
(&mut rng).sample_iter(uniform_u8).take(size).collect()
|
||||
}
|
||||
|
||||
/// Gets a key pair generated in a pseudorandom way.
|
||||
pub fn get_random_keypair() -> (SecretKey, PublicKey) {
|
||||
let raw_sk = get_random_bytes(32);
|
||||
|
||||
loop {
|
||||
if let Ok(sk) = SecretKey::from_slice(&raw_sk) {
|
||||
if let Ok(sk) = SecretKey::from_slice(&get_random_bytes(32)) {
|
||||
return (sk, PublicKey::from_secret_key(&Secp256k1::new(), &sk));
|
||||
}
|
||||
}
|
||||
|
|
@ -99,6 +94,8 @@ pub fn get_random_keypair() -> (SecretKey, PublicKey) {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
use bitcoin::consensus;
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
|
|
@ -112,8 +109,8 @@ mod tests {
|
|||
let expected_enc_blob = Vec::from_hex(ENC_BLOB).unwrap();
|
||||
let tx_bytes = Vec::from_hex(HEX_TX).unwrap();
|
||||
|
||||
let tx = consensus::deserialize(&tx_bytes).unwrap();
|
||||
let txid = Txid::from_hex(HEX_TXID).unwrap();
|
||||
let tx: Transaction = consensus::deserialize(&tx_bytes).unwrap();
|
||||
let txid = bitcoin::Txid::from_str(HEX_TXID).unwrap();
|
||||
assert_eq!(encrypt(&tx, &txid).unwrap(), expected_enc_blob);
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +119,7 @@ mod tests {
|
|||
let expected_tx = consensus::deserialize(&Vec::from_hex(HEX_TX).unwrap()).unwrap();
|
||||
|
||||
let encrypted_blob = Vec::from_hex(ENC_BLOB).unwrap();
|
||||
let txid = Txid::from_hex(HEX_TXID).unwrap();
|
||||
let txid = bitcoin::Txid::from_str(HEX_TXID).unwrap();
|
||||
assert_eq!(decrypt(&encrypted_blob, &txid).unwrap(), expected_tx);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ pub enum Endpoint {
|
|||
AddAppointment,
|
||||
GetAppointment,
|
||||
GetSubscriptionInfo,
|
||||
Ping,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Endpoint {
|
||||
|
|
@ -15,6 +16,7 @@ impl std::fmt::Display for Endpoint {
|
|||
Endpoint::AddAppointment => "add_appointment",
|
||||
Endpoint::GetAppointment => "get_appointment",
|
||||
Endpoint::GetSubscriptionInfo => "get_subscription_info",
|
||||
Endpoint::Ping => "ping",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ use crate::{cryptography, UserId};
|
|||
/// Proof that a user has registered with a tower. This serves two purposes:
|
||||
///
|
||||
/// - First, the user is able to prove that the tower agreed on providing a service. If a tower refuses to accept appointments
|
||||
/// from a user (claiming the subscription has expired) but the expiry time has still not passed and the tower cannot
|
||||
/// provide the relevant appointments signed by the user, it means it is cheating.
|
||||
/// from a user (claiming the subscription has expired) but the expiry time has still not passed and the tower cannot
|
||||
/// provide the relevant appointments signed by the user, it means it is cheating.
|
||||
/// - Second, it serves as proof, alongside an appointment receipt, that an appointment was not fulfilled. A registration receipt
|
||||
/// specifies a subscription period (`subscription_start` - `subscription_expiry`) and the appointment a `start_block` so inclusion
|
||||
/// can be proved.
|
||||
/// specifies a subscription period (`subscription_start` - `subscription_expiry`) and the appointment a `start_block` so inclusion
|
||||
/// can be proved.
|
||||
///
|
||||
/// TODO: / DISCUSS: In order to minimize the amount of receipts the user has to store, the tower could batch subscription receipts
|
||||
/// as long as the user info is still known. That is, if a user has a subscription with range (S, E) and the user renews the subscription
|
||||
|
|
@ -92,8 +92,7 @@ impl RegistrationReceipt {
|
|||
}
|
||||
|
||||
pub fn sign(&mut self, sk: &SecretKey) {
|
||||
// TODO: Check if there's any case where this can actually fail. Don't unwrap if so.
|
||||
self.signature = Some(cryptography::sign(&self.to_vec(), sk).unwrap());
|
||||
self.signature = Some(cryptography::sign(&self.to_vec(), sk));
|
||||
}
|
||||
|
||||
pub fn verify(&self, id: &UserId) -> bool {
|
||||
|
|
@ -153,8 +152,7 @@ impl AppointmentReceipt {
|
|||
}
|
||||
|
||||
pub fn sign(&mut self, sk: &SecretKey) {
|
||||
// TODO: Check if there's any case where this can actually fail. Don't unwrap if so.
|
||||
self.signature = Some(cryptography::sign(&self.to_vec(), sk).unwrap());
|
||||
self.signature = Some(cryptography::sign(&self.to_vec(), sk));
|
||||
}
|
||||
|
||||
pub fn verify(&self, id: &UserId) -> bool {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
use std::convert::TryInto;
|
||||
|
||||
use bitcoin::script::PushBytesBuf;
|
||||
use hex::FromHex;
|
||||
use rand::distributions::Standard;
|
||||
use rand::prelude::Distribution;
|
||||
use rand::Rng;
|
||||
|
||||
use bitcoin::consensus;
|
||||
use bitcoin::hashes::Hash;
|
||||
use bitcoin::secp256k1::SecretKey;
|
||||
use bitcoin::Txid;
|
||||
use bitcoin::{consensus, Amount, ScriptBuf, Transaction, TxOut, Txid};
|
||||
|
||||
use crate::appointment::{Appointment, Locator};
|
||||
use crate::cryptography;
|
||||
|
|
@ -32,6 +32,12 @@ pub fn get_random_user_id() -> UserId {
|
|||
UserId(pk)
|
||||
}
|
||||
|
||||
pub fn get_random_locator() -> Locator {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
Locator::from_slice(&rng.gen::<[u8; 16]>()).unwrap()
|
||||
}
|
||||
|
||||
pub fn generate_random_appointment(dispute_txid: Option<&Txid>) -> Appointment {
|
||||
let dispute_txid = match dispute_txid {
|
||||
Some(l) => *l,
|
||||
|
|
@ -42,7 +48,18 @@ pub fn generate_random_appointment(dispute_txid: Option<&Txid>) -> Appointment {
|
|||
};
|
||||
|
||||
let tx_bytes = Vec::from_hex(TX_HEX).unwrap();
|
||||
let penalty_tx = consensus::deserialize(&tx_bytes).unwrap();
|
||||
let mut penalty_tx: Transaction = consensus::deserialize(&tx_bytes).unwrap();
|
||||
let size = get_random_int::<usize>() % 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]);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ name = "teos"
|
|||
version = "0.2.0"
|
||||
authors = ["Sergi Delgado Segura <sergi.delgado.s@gmail.com>"]
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
default-run="teosd"
|
||||
|
||||
[[bin]]
|
||||
|
|
@ -19,32 +19,33 @@ path = "src/main.rs"
|
|||
hex = { version = "0.4.3", features = [ "serde" ] }
|
||||
home = "0.5.3"
|
||||
log = "0.4"
|
||||
prost = "0.9"
|
||||
rcgen = { version = "0.8", features = ["pem", "x509-parser"] }
|
||||
prost = "0.12"
|
||||
rcgen = { version = "0.13.1", features = ["pem", "x509-parser"] }
|
||||
rusqlite = { version = "0.26.0", features = [ "bundled", "limits" ] }
|
||||
serde = "1.0.130"
|
||||
serde_json = "1.0"
|
||||
simple_logger = "2.1.0"
|
||||
structopt = "0.3"
|
||||
toml = "0.5"
|
||||
tonic = { version = "0.6", features = [ "tls", "transport" ] }
|
||||
tonic = { version = "0.11", features = [ "tls", "transport" ] }
|
||||
tokio = { version = "1.5", features = [ "rt-multi-thread" ] }
|
||||
triggered = "0.1.2"
|
||||
warp = "0.3.2"
|
||||
warp = "0.3.5"
|
||||
torut = "0.2.1"
|
||||
base64 = "0.22.1"
|
||||
|
||||
# Bitcoin and Lightning
|
||||
bitcoin = { version = "0.28.0", features = [ "base64" ] }
|
||||
bitcoincore-rpc = "0.15.0"
|
||||
lightning = "0.0.108"
|
||||
lightning-net-tokio = "0.0.108"
|
||||
lightning-block-sync = { version = "0.0.108", features = [ "rpc-client" ] }
|
||||
bitcoin = { version = "0.32.0" }
|
||||
bitcoincore-rpc = "0.19.0"
|
||||
lightning = "0.1.0"
|
||||
lightning-net-tokio = "0.1.0"
|
||||
lightning-block-sync = { version = "0.1.0", features = [ "rpc-client" ] }
|
||||
|
||||
# Local
|
||||
teos-common = { path = "../teos-common" }
|
||||
|
||||
[build-dependencies]
|
||||
tonic-build = "0.6"
|
||||
tonic-build = "0.11"
|
||||
|
||||
[dev-dependencies]
|
||||
jsonrpc-http-server = "17.1.0"
|
||||
|
|
|
|||
|
|
@ -217,6 +217,14 @@ async fn get_subscription_info(
|
|||
Ok(reply::with_status(body, status))
|
||||
}
|
||||
|
||||
async fn ping(addr: Option<SocketAddr>) -> Result<impl Reply, Rejection> {
|
||||
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<Channel>,
|
||||
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone {
|
||||
|
|
@ -251,10 +259,16 @@ fn router(
|
|||
.and(with_grpc(grpc_conn))
|
||||
.and_then(get_subscription_info);
|
||||
|
||||
let ping = warp::get()
|
||||
.and(warp::path(Endpoint::Ping.to_string()))
|
||||
.and(warp::addr::remote())
|
||||
.and_then(ping);
|
||||
|
||||
register
|
||||
.or(add_appointment)
|
||||
.or(get_appointment)
|
||||
.or(get_subscription_info)
|
||||
.or(ping)
|
||||
.recover(handle_rejection)
|
||||
}
|
||||
|
||||
|
|
@ -290,12 +304,12 @@ async fn handle_rejection(err: Rejection) -> Result<impl Reply, Rejection> {
|
|||
|
||||
pub async fn serve(
|
||||
http_bind: SocketAddr,
|
||||
grpc_bind: String,
|
||||
grpc_bind: SocketAddr,
|
||||
service_ready: Trigger,
|
||||
shutdown_signal: Listener,
|
||||
) {
|
||||
let grpc_conn = loop {
|
||||
match PublicTowerServicesClient::connect(grpc_bind.clone()).await {
|
||||
match PublicTowerServicesClient::connect(format!("http://{grpc_bind}")).await {
|
||||
Ok(conn) => break conn,
|
||||
Err(_) => {
|
||||
log::error!("Cannot connect to the gRPC server. Retrying shortly");
|
||||
|
|
@ -615,7 +629,7 @@ mod tests_failures {
|
|||
.reply(&router(grpc_conn))
|
||||
.await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||
assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -646,8 +660,11 @@ mod tests_methods {
|
|||
};
|
||||
use super::*;
|
||||
|
||||
use crate::extended_appointment::UUID;
|
||||
use crate::test_utils::{generate_dummy_appointment, ApiConfig, DURATION, SLOTS};
|
||||
use crate::responder::{ConfirmationStatus, TransactionTracker};
|
||||
use crate::test_utils::{
|
||||
generate_dummy_appointment, get_random_tx, ApiConfig, DURATION, SLOTS,
|
||||
};
|
||||
use crate::watcher::Breach;
|
||||
|
||||
use teos_common::test_utils::get_random_user_id;
|
||||
use teos_common::{cryptography, UserId};
|
||||
|
|
@ -750,7 +767,7 @@ mod tests_methods {
|
|||
|
||||
// Then try to add an appointment
|
||||
let appointment = generate_dummy_appointment(None).inner;
|
||||
let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
|
||||
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
|
||||
|
||||
let response = request_to_api::<
|
||||
common_msgs::AddAppointmentRequest,
|
||||
|
|
@ -776,7 +793,7 @@ mod tests_methods {
|
|||
let (server_addr, _s) = run_tower_in_background().await;
|
||||
let (user_sk, _s) = cryptography::get_random_keypair();
|
||||
let appointment = generate_dummy_appointment(None).inner;
|
||||
let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
|
||||
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
|
||||
|
||||
assert_eq!(
|
||||
check_api_error(
|
||||
|
|
@ -816,14 +833,20 @@ mod tests_methods {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
// Add the appointment to the Responder so it counts as triggered
|
||||
let appointment = generate_dummy_appointment(None).inner;
|
||||
let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
|
||||
// Add the appointment to the Responder as a tracker so it counts as triggered
|
||||
let dispute_tx = get_random_tx();
|
||||
let tracker = TransactionTracker::new(
|
||||
Breach::new(dispute_tx.clone(), get_random_tx()),
|
||||
UserId(user_pk),
|
||||
ConfirmationStatus::ConfirmedIn(100),
|
||||
);
|
||||
internal_api
|
||||
.get_watcher()
|
||||
.add_random_tracker_to_responder(UUID::new(appointment.locator, UserId(user_pk)));
|
||||
.add_dummy_tracker_to_responder(&tracker);
|
||||
|
||||
// Try to add it via the http API
|
||||
let appointment = generate_dummy_appointment(Some(&dispute_tx.compute_txid())).inner;
|
||||
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
|
||||
assert_eq!(
|
||||
check_api_error(
|
||||
Endpoint::AddAppointment,
|
||||
|
|
@ -852,7 +875,7 @@ mod tests_methods {
|
|||
.await;
|
||||
let (user_sk, _) = cryptography::get_random_keypair();
|
||||
let appointment = generate_dummy_appointment(None).inner;
|
||||
let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
|
||||
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
|
||||
|
||||
assert_eq!(
|
||||
check_api_error(
|
||||
|
|
@ -892,7 +915,7 @@ mod tests_methods {
|
|||
|
||||
// Add an appointment
|
||||
let appointment = generate_dummy_appointment(None).inner;
|
||||
let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
|
||||
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
|
||||
|
||||
request_to_api::<common_msgs::AddAppointmentRequest, common_msgs::AddAppointmentResponse>(
|
||||
Endpoint::AddAppointment,
|
||||
|
|
@ -916,8 +939,7 @@ mod tests_methods {
|
|||
signature: cryptography::sign(
|
||||
format!("get appointment {}", appointment.locator).as_bytes(),
|
||||
&user_sk,
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
},
|
||||
server_addr,
|
||||
)
|
||||
|
|
@ -947,7 +969,6 @@ mod tests_methods {
|
|||
format!("get appointment {}", appointment.locator).as_bytes(),
|
||||
&user_sk,
|
||||
)
|
||||
.unwrap()
|
||||
})),
|
||||
server_addr,
|
||||
)
|
||||
|
|
@ -990,7 +1011,6 @@ mod tests_methods {
|
|||
format!("get appointment {}", appointment.locator).as_bytes(),
|
||||
&user_sk,
|
||||
)
|
||||
.unwrap()
|
||||
})),
|
||||
server_addr,
|
||||
)
|
||||
|
|
@ -1025,7 +1045,6 @@ mod tests_methods {
|
|||
format!("get appointment {}", appointment.locator).as_bytes(),
|
||||
&user_sk,
|
||||
)
|
||||
.unwrap()
|
||||
})),
|
||||
server_addr,
|
||||
)
|
||||
|
|
@ -1063,8 +1082,7 @@ mod tests_methods {
|
|||
>(
|
||||
Endpoint::GetSubscriptionInfo,
|
||||
common_msgs::GetSubscriptionInfoRequest {
|
||||
signature: cryptography::sign("get subscription info".as_bytes(), &user_sk)
|
||||
.unwrap(),
|
||||
signature: cryptography::sign("get subscription info".as_bytes(), &user_sk),
|
||||
},
|
||||
server_addr,
|
||||
)
|
||||
|
|
@ -1088,7 +1106,6 @@ mod tests_methods {
|
|||
Endpoint::GetSubscriptionInfo,
|
||||
RequestBody::Json(serde_json::json!(common_msgs::GetSubscriptionInfoRequest {
|
||||
signature: cryptography::sign("get subscription info".as_bytes(), &user_sk)
|
||||
.unwrap(),
|
||||
})),
|
||||
server_addr,
|
||||
)
|
||||
|
|
@ -1116,7 +1133,6 @@ mod tests_methods {
|
|||
Endpoint::GetSubscriptionInfo,
|
||||
RequestBody::Json(serde_json::json!(common_msgs::GetSubscriptionInfoRequest {
|
||||
signature: cryptography::sign("get subscription info".as_bytes(), &user_sk)
|
||||
.unwrap(),
|
||||
})),
|
||||
server_addr,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use std::sync::{Arc, Condvar, Mutex};
|
|||
use tonic::{Code, Request, Response, Status};
|
||||
use triggered::Trigger;
|
||||
|
||||
use crate::extended_appointment::UUID;
|
||||
use crate::protos as msgs;
|
||||
use crate::protos::private_tower_services_server::PrivateTowerServices;
|
||||
use crate::protos::public_tower_services_server::PublicTowerServices;
|
||||
|
|
@ -386,10 +387,14 @@ impl PrivateTowerServices for Arc<InternalAPI> {
|
|||
})?;
|
||||
|
||||
match self.watcher.get_user_info(user_id) {
|
||||
Some(info) => Ok(Response::new(msgs::GetUserResponse {
|
||||
Some((info, locators)) => Ok(Response::new(msgs::GetUserResponse {
|
||||
available_slots: info.available_slots,
|
||||
subscription_expiry: info.subscription_expiry,
|
||||
appointments: info.appointments.keys().map(|uuid| uuid.to_vec()).collect(),
|
||||
// TODO: Should make it return locators and make `get_appointments` queryable using the (user_id, locator) pair for consistency.
|
||||
appointments: locators
|
||||
.into_iter()
|
||||
.map(|locator| UUID::new(locator, user_id).to_vec())
|
||||
.collect(),
|
||||
})),
|
||||
None => Err(Status::new(Code::NotFound, "User not found")),
|
||||
}
|
||||
|
|
@ -429,11 +434,10 @@ mod tests_private_api {
|
|||
use bitcoin::hashes::Hash;
|
||||
use bitcoin::Txid;
|
||||
|
||||
use crate::extended_appointment::UUID;
|
||||
use crate::responder::{ConfirmationStatus, TransactionTracker};
|
||||
use crate::test_utils::{
|
||||
create_api, generate_dummy_appointment, generate_uuid, get_random_tx, DURATION, SLOTS,
|
||||
START_HEIGHT,
|
||||
create_api, generate_dummy_appointment, generate_dummy_appointment_with_user,
|
||||
get_random_tx, DURATION, SLOTS, START_HEIGHT,
|
||||
};
|
||||
use crate::watcher::Breach;
|
||||
|
||||
|
|
@ -462,7 +466,7 @@ mod tests_private_api {
|
|||
internal_api.watcher.register(UserId(user_pk)).unwrap();
|
||||
|
||||
let appointment = generate_dummy_appointment(None).inner;
|
||||
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
|
||||
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk);
|
||||
internal_api
|
||||
.watcher
|
||||
.add_appointment(appointment.clone(), user_signature)
|
||||
|
|
@ -486,9 +490,7 @@ mod tests_private_api {
|
|||
let (internal_api, _s) = create_api().await;
|
||||
|
||||
// Add data to the Responser so we can retrieve it later on
|
||||
internal_api
|
||||
.watcher
|
||||
.add_random_tracker_to_responder(generate_uuid());
|
||||
internal_api.watcher.add_random_tracker_to_responder();
|
||||
|
||||
let response = internal_api
|
||||
.get_all_appointments(Request::new(()))
|
||||
|
|
@ -507,7 +509,7 @@ mod tests_private_api {
|
|||
async fn test_get_appointments() {
|
||||
let (internal_api, _s) = create_api().await;
|
||||
|
||||
let locator = Locator::new(get_random_tx().txid()).to_vec();
|
||||
let locator = Locator::new(get_random_tx().compute_txid()).to_vec();
|
||||
let response = internal_api
|
||||
.get_appointments(Request::new(msgs::GetAppointmentsRequest { locator }))
|
||||
.await
|
||||
|
|
@ -523,7 +525,7 @@ mod tests_private_api {
|
|||
|
||||
for i in 0..3 {
|
||||
// Create a dispute tx to be used for creating different dummy appointments with the same locator.
|
||||
let dispute_txid = get_random_tx().txid();
|
||||
let dispute_txid = get_random_tx().compute_txid();
|
||||
|
||||
// The number of different appointments to create for this dispute tx.
|
||||
let appointments_to_create = 4 * i + 7;
|
||||
|
|
@ -533,7 +535,7 @@ mod tests_private_api {
|
|||
let (user_sk, user_pk) = get_random_keypair();
|
||||
internal_api.watcher.register(UserId(user_pk)).unwrap();
|
||||
let appointment = generate_dummy_appointment(Some(&dispute_txid)).inner;
|
||||
let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
|
||||
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
|
||||
internal_api
|
||||
.watcher
|
||||
.add_appointment(appointment, signature)
|
||||
|
|
@ -588,10 +590,10 @@ mod tests_private_api {
|
|||
);
|
||||
internal_api
|
||||
.watcher
|
||||
.add_dummy_tracker_to_responder(generate_uuid(), &tracker);
|
||||
.add_dummy_tracker_to_responder(&tracker);
|
||||
}
|
||||
|
||||
let locator = Locator::new(dispute_tx.txid());
|
||||
let locator = Locator::new(dispute_tx.compute_txid());
|
||||
|
||||
// Query for the current locator and assert it retrieves correct trackers.
|
||||
let response = internal_api
|
||||
|
|
@ -646,7 +648,7 @@ mod tests_private_api {
|
|||
// Add data to the Watcher
|
||||
for _ in 0..2 {
|
||||
let appointment = generate_dummy_appointment(None).inner;
|
||||
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
|
||||
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk);
|
||||
internal_api
|
||||
.watcher
|
||||
.add_appointment(appointment.clone(), user_signature)
|
||||
|
|
@ -655,9 +657,7 @@ mod tests_private_api {
|
|||
|
||||
// And the Responder
|
||||
for _ in 0..3 {
|
||||
internal_api
|
||||
.watcher
|
||||
.add_random_tracker_to_responder(generate_uuid());
|
||||
internal_api.watcher.add_random_tracker_to_responder();
|
||||
}
|
||||
|
||||
let response = internal_api
|
||||
|
|
@ -730,12 +730,11 @@ mod tests_private_api {
|
|||
assert!(response.appointments.is_empty());
|
||||
|
||||
// Add an appointment and check back
|
||||
let appointment = generate_dummy_appointment(None).inner;
|
||||
let uuid = UUID::new(appointment.locator, user_id);
|
||||
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
|
||||
let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None);
|
||||
let user_signature = cryptography::sign(&appointment.inner.to_vec(), &user_sk);
|
||||
internal_api
|
||||
.watcher
|
||||
.add_appointment(appointment.clone(), user_signature)
|
||||
.add_appointment(appointment.inner, user_signature)
|
||||
.unwrap();
|
||||
|
||||
let response = internal_api
|
||||
|
|
@ -786,10 +785,12 @@ mod tests_private_api {
|
|||
mod tests_public_api {
|
||||
use super::*;
|
||||
|
||||
use crate::extended_appointment::UUID;
|
||||
use crate::responder::{ConfirmationStatus, TransactionTracker};
|
||||
use crate::test_utils::{
|
||||
create_api, create_api_with_config, generate_dummy_appointment, ApiConfig, DURATION, SLOTS,
|
||||
create_api, create_api_with_config, generate_dummy_appointment, get_random_tx, ApiConfig,
|
||||
DURATION, SLOTS,
|
||||
};
|
||||
use crate::watcher::Breach;
|
||||
use teos_common::cryptography::{self, get_random_keypair};
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -900,12 +901,12 @@ mod tests_public_api {
|
|||
internal_api.watcher.register(UserId(user_pk)).unwrap();
|
||||
|
||||
let appointment = generate_dummy_appointment(None).inner;
|
||||
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
|
||||
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
|
||||
|
||||
let response = internal_api
|
||||
.add_appointment(Request::new(common_msgs::AddAppointmentRequest {
|
||||
appointment: Some(appointment.clone().into()),
|
||||
signature: user_signature.clone(),
|
||||
signature,
|
||||
}))
|
||||
.await
|
||||
.unwrap()
|
||||
|
|
@ -925,12 +926,12 @@ mod tests_public_api {
|
|||
let (user_sk, _) = get_random_keypair();
|
||||
|
||||
let appointment = generate_dummy_appointment(None).inner;
|
||||
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
|
||||
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
|
||||
|
||||
match internal_api
|
||||
.add_appointment(Request::new(common_msgs::AddAppointmentRequest {
|
||||
appointment: Some(appointment.clone().into()),
|
||||
signature: user_signature.clone(),
|
||||
signature,
|
||||
}))
|
||||
.await
|
||||
{
|
||||
|
|
@ -954,12 +955,12 @@ mod tests_public_api {
|
|||
internal_api.watcher.register(UserId(user_pk)).unwrap();
|
||||
|
||||
let appointment = generate_dummy_appointment(None).inner;
|
||||
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
|
||||
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
|
||||
|
||||
match internal_api
|
||||
.add_appointment(Request::new(common_msgs::AddAppointmentRequest {
|
||||
appointment: Some(appointment.clone().into()),
|
||||
signature: user_signature.clone(),
|
||||
signature,
|
||||
}))
|
||||
.await
|
||||
{
|
||||
|
|
@ -983,12 +984,12 @@ mod tests_public_api {
|
|||
internal_api.watcher.register(UserId(user_pk)).unwrap();
|
||||
|
||||
let appointment = generate_dummy_appointment(None).inner;
|
||||
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
|
||||
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
|
||||
|
||||
match internal_api
|
||||
.add_appointment(Request::new(common_msgs::AddAppointmentRequest {
|
||||
appointment: Some(appointment.clone().into()),
|
||||
signature: user_signature.clone(),
|
||||
signature,
|
||||
}))
|
||||
.await
|
||||
{
|
||||
|
|
@ -1008,16 +1009,24 @@ mod tests_public_api {
|
|||
let user_id = UserId(user_pk);
|
||||
internal_api.watcher.register(user_id).unwrap();
|
||||
|
||||
let appointment = generate_dummy_appointment(None).inner;
|
||||
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
|
||||
// Add a tracker to the responder to simulate it being triggered.
|
||||
let dispute_tx = get_random_tx();
|
||||
let tracker = TransactionTracker::new(
|
||||
Breach::new(dispute_tx.clone(), get_random_tx()),
|
||||
user_id,
|
||||
ConfirmationStatus::ConfirmedIn(100),
|
||||
);
|
||||
internal_api
|
||||
.watcher
|
||||
.add_random_tracker_to_responder(UUID::new(appointment.locator, user_id));
|
||||
.get_watcher()
|
||||
.add_dummy_tracker_to_responder(&tracker);
|
||||
|
||||
// Try to add it again using the API.
|
||||
let appointment = generate_dummy_appointment(Some(&dispute_tx.compute_txid())).inner;
|
||||
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
|
||||
match internal_api
|
||||
.add_appointment(Request::new(common_msgs::AddAppointmentRequest {
|
||||
appointment: Some(appointment.clone().into()),
|
||||
signature: user_signature.clone(),
|
||||
appointment: Some(appointment.into()),
|
||||
signature,
|
||||
}))
|
||||
.await
|
||||
{
|
||||
|
|
@ -1038,12 +1047,12 @@ mod tests_public_api {
|
|||
|
||||
let (user_sk, _) = get_random_keypair();
|
||||
let appointment = generate_dummy_appointment(None).inner;
|
||||
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
|
||||
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
|
||||
|
||||
match internal_api
|
||||
.add_appointment(Request::new(common_msgs::AddAppointmentRequest {
|
||||
appointment: Some(appointment.clone().into()),
|
||||
signature: user_signature.clone(),
|
||||
signature,
|
||||
}))
|
||||
.await
|
||||
{
|
||||
|
|
@ -1065,7 +1074,7 @@ mod tests_public_api {
|
|||
|
||||
// Add the appointment
|
||||
let appointment = generate_dummy_appointment(None).inner;
|
||||
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
|
||||
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk);
|
||||
internal_api
|
||||
.watcher
|
||||
.add_appointment(appointment.clone(), user_signature)
|
||||
|
|
@ -1076,7 +1085,7 @@ mod tests_public_api {
|
|||
let response = internal_api
|
||||
.get_appointment(Request::new(common_msgs::GetAppointmentRequest {
|
||||
locator: appointment.locator.to_vec(),
|
||||
signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(),
|
||||
signature: cryptography::sign(message.as_bytes(), &user_sk),
|
||||
}))
|
||||
.await
|
||||
.unwrap()
|
||||
|
|
@ -1104,7 +1113,7 @@ mod tests_public_api {
|
|||
match internal_api
|
||||
.get_appointment(Request::new(common_msgs::GetAppointmentRequest {
|
||||
locator: appointment.locator.to_vec(),
|
||||
signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(),
|
||||
signature: cryptography::sign(message.as_bytes(), &user_sk),
|
||||
}))
|
||||
.await
|
||||
{
|
||||
|
|
@ -1131,7 +1140,7 @@ mod tests_public_api {
|
|||
match internal_api
|
||||
.get_appointment(Request::new(common_msgs::GetAppointmentRequest {
|
||||
locator: appointment.locator.to_vec(),
|
||||
signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(),
|
||||
signature: cryptography::sign(message.as_bytes(), &user_sk),
|
||||
}))
|
||||
.await
|
||||
{
|
||||
|
|
@ -1159,7 +1168,7 @@ mod tests_public_api {
|
|||
match internal_api
|
||||
.get_appointment(Request::new(common_msgs::GetAppointmentRequest {
|
||||
locator: appointment.locator.to_vec(),
|
||||
signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(),
|
||||
signature: cryptography::sign(message.as_bytes(), &user_sk),
|
||||
}))
|
||||
.await
|
||||
{
|
||||
|
|
@ -1182,7 +1191,7 @@ mod tests_public_api {
|
|||
match internal_api
|
||||
.get_appointment(Request::new(common_msgs::GetAppointmentRequest {
|
||||
locator: appointment.locator.to_vec(),
|
||||
signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(),
|
||||
signature: cryptography::sign(message.as_bytes(), &user_sk),
|
||||
}))
|
||||
.await
|
||||
{
|
||||
|
|
@ -1206,7 +1215,7 @@ mod tests_public_api {
|
|||
let message = "get subscription info".to_string();
|
||||
let response = internal_api
|
||||
.get_subscription_info(Request::new(common_msgs::GetSubscriptionInfoRequest {
|
||||
signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(),
|
||||
signature: cryptography::sign(message.as_bytes(), &user_sk),
|
||||
}))
|
||||
.await
|
||||
.unwrap()
|
||||
|
|
@ -1229,7 +1238,7 @@ mod tests_public_api {
|
|||
let message = "get subscription info".to_string();
|
||||
match internal_api
|
||||
.get_subscription_info(Request::new(common_msgs::GetSubscriptionInfoRequest {
|
||||
signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(),
|
||||
signature: cryptography::sign(message.as_bytes(), &user_sk),
|
||||
}))
|
||||
.await
|
||||
{
|
||||
|
|
@ -1253,7 +1262,7 @@ mod tests_public_api {
|
|||
let message = "get subscription info".to_string();
|
||||
match internal_api
|
||||
.get_subscription_info(Request::new(common_msgs::GetSubscriptionInfoRequest {
|
||||
signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(),
|
||||
signature: cryptography::sign(message.as_bytes(), &user_sk),
|
||||
}))
|
||||
.await
|
||||
{
|
||||
|
|
@ -1274,7 +1283,7 @@ mod tests_public_api {
|
|||
let message = "get subscription info".to_string();
|
||||
match internal_api
|
||||
.get_subscription_info(Request::new(common_msgs::GetSubscriptionInfoRequest {
|
||||
signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(),
|
||||
signature: cryptography::sign(message.as_bytes(), &user_sk),
|
||||
}))
|
||||
.await
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,19 +9,19 @@
|
|||
* at your option.
|
||||
*/
|
||||
|
||||
use base64::{engine::general_purpose::URL_SAFE as BASE64, Engine};
|
||||
use std::convert::TryInto;
|
||||
use std::io::{Error, ErrorKind};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use bitcoin::base64;
|
||||
use bitcoin::hash_types::{BlockHash, Txid};
|
||||
use bitcoin::hashes::hex::ToHex;
|
||||
use bitcoin::{Block, Transaction};
|
||||
use bitcoin::Transaction;
|
||||
use bitcoincore_rpc::{Auth, RawTx};
|
||||
use lightning::util::ser::Writeable;
|
||||
use lightning_block_sync::http::{HttpEndpoint, JsonResponse};
|
||||
use lightning_block_sync::rpc::RpcClient;
|
||||
use lightning_block_sync::{AsyncBlockSourceResult, BlockHeaderData, BlockSource};
|
||||
use lightning_block_sync::{AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource};
|
||||
|
||||
/// A simple implementation of a bitcoind client (`bitcoin-cli`) with the minimal functionality required by the tower.
|
||||
pub struct BitcoindClient<'a> {
|
||||
|
|
@ -32,9 +32,9 @@ pub struct BitcoindClient<'a> {
|
|||
/// The port to connect to.
|
||||
port: u16,
|
||||
/// The RPC user `bitcoind` is configured with.
|
||||
rpc_user: &'a str,
|
||||
rpc_user: String,
|
||||
/// The RPC password for the given user.
|
||||
rpc_password: &'a str,
|
||||
rpc_password: String,
|
||||
}
|
||||
|
||||
impl BlockSource for &BitcoindClient<'_> {
|
||||
|
|
@ -51,7 +51,10 @@ impl BlockSource for &BitcoindClient<'_> {
|
|||
}
|
||||
|
||||
/// Gets a block given its hash.
|
||||
fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, Block> {
|
||||
fn get_block<'a>(
|
||||
&'a self,
|
||||
header_hash: &'a BlockHash,
|
||||
) -> AsyncBlockSourceResult<'a, BlockData> {
|
||||
Box::pin(async move {
|
||||
let rpc = self.bitcoind_rpc_client.lock().await;
|
||||
rpc.get_block(header_hash).await
|
||||
|
|
@ -74,13 +77,34 @@ impl<'a> BitcoindClient<'a> {
|
|||
pub async fn new(
|
||||
host: &'a str,
|
||||
port: u16,
|
||||
rpc_user: &'a str,
|
||||
rpc_password: &'a str,
|
||||
auth: Auth,
|
||||
teos_network: &'a str,
|
||||
) -> std::io::Result<BitcoindClient<'a>> {
|
||||
let http_endpoint = HttpEndpoint::for_host(host.to_owned()).with_port(port);
|
||||
let rpc_credentials = base64::encode(&format!("{rpc_user}:{rpc_password}"));
|
||||
let bitcoind_rpc_client = RpcClient::new(&rpc_credentials, http_endpoint)?;
|
||||
let (rpc_user, rpc_password) = {
|
||||
let (user, pass) = auth.get_user_pass().map_err(|e| {
|
||||
Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
format!("Cannot read cookie file. {}", e),
|
||||
)
|
||||
})?;
|
||||
if user.is_none() {
|
||||
Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Empty btc_rpc_user parsed from rpc_cookie".to_string(),
|
||||
))
|
||||
} else if pass.is_none() {
|
||||
Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Empty btc_rpc_password parsed from rpc_cookie",
|
||||
))
|
||||
} else {
|
||||
Ok((user.unwrap(), pass.unwrap()))
|
||||
}
|
||||
}?;
|
||||
|
||||
let rpc_credentials = BASE64.encode(format!("{}:{}", rpc_user, rpc_password));
|
||||
let bitcoind_rpc_client = RpcClient::new(&rpc_credentials, http_endpoint);
|
||||
|
||||
let client = Self {
|
||||
bitcoind_rpc_client: Arc::new(Mutex::new(bitcoind_rpc_client)),
|
||||
|
|
@ -105,9 +129,10 @@ impl<'a> BitcoindClient<'a> {
|
|||
}
|
||||
|
||||
/// Gets a fresh RPC client.
|
||||
pub fn get_new_rpc_client(&self) -> std::io::Result<RpcClient> {
|
||||
pub fn get_new_rpc_client(&self) -> RpcClient {
|
||||
let http_endpoint = HttpEndpoint::for_host(self.host.to_owned()).with_port(self.port);
|
||||
let rpc_credentials = base64::encode(&format!("{}:{}", self.rpc_user, self.rpc_password));
|
||||
let rpc_credentials = BASE64.encode(format!("{}:{}", self.rpc_user, self.rpc_password));
|
||||
|
||||
RpcClient::new(&rpc_credentials, http_endpoint)
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +149,7 @@ impl<'a> BitcoindClient<'a> {
|
|||
pub async fn send_raw_transaction(&self, raw_tx: &Transaction) -> Result<Txid, std::io::Error> {
|
||||
let rpc = self.bitcoind_rpc_client.lock().await;
|
||||
|
||||
let raw_tx_json = serde_json::json!(raw_tx.encode().to_hex());
|
||||
let raw_tx_json = serde_json::json!(raw_tx.encode().raw_hex());
|
||||
rpc.call_method::<Txid>("sendrawtransaction", &[raw_tx_json])
|
||||
.await
|
||||
}
|
||||
|
|
@ -133,7 +158,7 @@ impl<'a> BitcoindClient<'a> {
|
|||
pub async fn get_raw_transaction(&self, txid: &Txid) -> Result<Transaction, std::io::Error> {
|
||||
let rpc = self.bitcoind_rpc_client.lock().await;
|
||||
|
||||
let txid_hex = serde_json::json!(txid.encode().to_hex());
|
||||
let txid_hex = serde_json::json!(txid.encode().raw_hex());
|
||||
rpc.call_method::<Transaction>("getrawtransaction", &[txid_hex])
|
||||
.await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,17 +80,17 @@ impl Carrier {
|
|||
pub(crate) fn send_transaction(&mut self, tx: &Transaction) -> ConfirmationStatus {
|
||||
self.hang_until_bitcoind_reachable();
|
||||
|
||||
if let Some(receipt) = self.issued_receipts.get(&tx.txid()) {
|
||||
log::info!("Transaction already sent: {}", tx.txid());
|
||||
if let Some(receipt) = self.issued_receipts.get(&tx.compute_txid()) {
|
||||
log::info!("Transaction already sent: {}", tx.compute_txid());
|
||||
return *receipt;
|
||||
}
|
||||
|
||||
log::info!("Pushing transaction to the network: {}", tx.txid());
|
||||
log::info!("Pushing transaction to the network: {}", tx.compute_txid());
|
||||
let receipt = match self.bitcoin_cli.send_raw_transaction(tx) {
|
||||
Ok(_) => {
|
||||
// Here the transaction could, potentially, have been in mempool before the current height.
|
||||
// This shouldn't really matter though.
|
||||
log::info!("Transaction successfully delivered: {}", tx.txid());
|
||||
log::info!("Transaction successfully delivered: {}", tx.compute_txid());
|
||||
ConfirmationStatus::InMempoolSince(self.block_height)
|
||||
}
|
||||
Err(JsonRpcError(RpcError(rpcerr))) => match rpcerr.code {
|
||||
|
|
@ -106,7 +106,7 @@ impl Carrier {
|
|||
rpc_errors::RPC_VERIFY_ALREADY_IN_CHAIN => {
|
||||
log::info!(
|
||||
"Transaction was confirmed long ago, not keeping track of it: {}",
|
||||
tx.txid()
|
||||
tx.compute_txid()
|
||||
);
|
||||
|
||||
// Given we are not using txindex, if a transaction bounces we cannot get its confirmation count. However, [send_transaction] is guarded by
|
||||
|
|
@ -117,7 +117,7 @@ impl Carrier {
|
|||
rpc_errors::RPC_DESERIALIZATION_ERROR => {
|
||||
// Adding this here just for completeness. We should never end up here. The Carrier only sends txs handed by the Responder,
|
||||
// who receives them from the Watcher, who checks that the tx can be properly deserialized.
|
||||
log::info!("Transaction cannot be deserialized: {}", tx.txid());
|
||||
log::info!("Transaction cannot be deserialized: {}", tx.compute_txid());
|
||||
ConfirmationStatus::Rejected(rpc_errors::RPC_DESERIALIZATION_ERROR)
|
||||
}
|
||||
_ => {
|
||||
|
|
@ -139,7 +139,7 @@ impl Carrier {
|
|||
}
|
||||
};
|
||||
|
||||
self.issued_receipts.insert(tx.txid(), receipt);
|
||||
self.issued_receipts.insert(tx.compute_txid(), receipt);
|
||||
|
||||
receipt
|
||||
}
|
||||
|
|
@ -184,6 +184,7 @@ impl Carrier {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
use std::thread;
|
||||
|
||||
use crate::test_utils::{get_random_tx, start_server, BitcoindMock, MockOptions, START_HEIGHT};
|
||||
|
|
@ -217,7 +218,7 @@ mod tests {
|
|||
// Lets add some dummy data into the cache
|
||||
for i in 0..10 {
|
||||
carrier.issued_receipts.insert(
|
||||
get_random_tx().txid(),
|
||||
get_random_tx().compute_txid(),
|
||||
ConfirmationStatus::ConfirmedIn(start_height - i),
|
||||
);
|
||||
}
|
||||
|
|
@ -243,7 +244,7 @@ mod tests {
|
|||
assert_eq!(r, ConfirmationStatus::InMempoolSince(start_height));
|
||||
|
||||
// Check the receipt is on the cache
|
||||
assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r);
|
||||
assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -261,7 +262,7 @@ mod tests {
|
|||
assert_eq!(r, ConfirmationStatus::InMempoolSince(start_height));
|
||||
|
||||
// Check the receipt is on the cache
|
||||
assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r);
|
||||
assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -284,7 +285,7 @@ mod tests {
|
|||
);
|
||||
|
||||
// Check the receipt is on the cache
|
||||
assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r);
|
||||
assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -306,7 +307,7 @@ mod tests {
|
|||
);
|
||||
|
||||
// Check the receipt is on the cache
|
||||
assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r);
|
||||
assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -326,7 +327,7 @@ mod tests {
|
|||
assert_eq!(r, ConfirmationStatus::IrrevocablyResolved);
|
||||
|
||||
// Check the receipt is on the cache
|
||||
assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r);
|
||||
assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -348,7 +349,7 @@ mod tests {
|
|||
);
|
||||
|
||||
// Check the receipt is on the cache
|
||||
assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r);
|
||||
assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -389,7 +390,7 @@ mod tests {
|
|||
start_server(bitcoind_mock.server);
|
||||
|
||||
let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
|
||||
let txid = Txid::from_hex(TXID_HEX).unwrap();
|
||||
let txid = Txid::from_str(TXID_HEX).unwrap();
|
||||
assert!(carrier.in_mempool(&txid));
|
||||
}
|
||||
|
||||
|
|
@ -402,7 +403,7 @@ mod tests {
|
|||
start_server(bitcoind_mock.server);
|
||||
|
||||
let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
|
||||
let txid = Txid::from_hex(TXID_HEX).unwrap();
|
||||
let txid = Txid::from_str(TXID_HEX).unwrap();
|
||||
assert!(!carrier.in_mempool(&txid));
|
||||
}
|
||||
|
||||
|
|
@ -417,7 +418,7 @@ mod tests {
|
|||
start_server(bitcoind_mock.server);
|
||||
|
||||
let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
|
||||
let txid = Txid::from_hex(TXID_HEX).unwrap();
|
||||
let txid = Txid::from_str(TXID_HEX).unwrap();
|
||||
assert!(!carrier.in_mempool(&txid));
|
||||
}
|
||||
|
||||
|
|
@ -431,7 +432,7 @@ mod tests {
|
|||
start_server(bitcoind_mock.server);
|
||||
|
||||
let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
|
||||
let txid = Txid::from_hex(TXID_HEX).unwrap();
|
||||
let txid = Txid::from_str(TXID_HEX).unwrap();
|
||||
assert!(!carrier.in_mempool(&txid));
|
||||
}
|
||||
|
||||
|
|
@ -444,7 +445,7 @@ mod tests {
|
|||
let start_height = START_HEIGHT as u32;
|
||||
let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable.clone(), start_height);
|
||||
|
||||
let txid = Txid::from_hex(TXID_HEX).unwrap();
|
||||
let txid = Txid::from_str(TXID_HEX).unwrap();
|
||||
let delay = std::time::Duration::new(3, 0);
|
||||
|
||||
thread::spawn(move || {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ where
|
|||
{
|
||||
/// A bitcoin client to poll best tips from.
|
||||
spv_client: SpvClient<'a, P, C, L>,
|
||||
/// The lat known block header by the [ChainMonitor].
|
||||
/// The last known block header by the [ChainMonitor].
|
||||
last_known_block_header: ValidatedBlockHeader,
|
||||
/// A [DBM] (database manager) instance. Used to persist block data into disk.
|
||||
dbm: Arc<Mutex<DBM>>,
|
||||
|
|
@ -135,8 +135,8 @@ mod tests {
|
|||
use std::iter::FromIterator;
|
||||
use std::thread;
|
||||
|
||||
use bitcoin::network::constants::Network;
|
||||
use bitcoin::BlockHash;
|
||||
use bitcoin::Network;
|
||||
use lightning_block_sync::{poll::ChainPoller, SpvClient, UnboundedCache};
|
||||
|
||||
use crate::test_utils::{Blockchain, START_HEIGHT};
|
||||
|
|
@ -158,7 +158,7 @@ mod tests {
|
|||
impl chain::Listen for DummyListener {
|
||||
fn filtered_block_connected(
|
||||
&self,
|
||||
header: &bitcoin::BlockHeader,
|
||||
header: &bitcoin::block::Header,
|
||||
_: &chain::transaction::TransactionData,
|
||||
_: u32,
|
||||
) {
|
||||
|
|
@ -167,7 +167,7 @@ mod tests {
|
|||
.insert(header.block_hash());
|
||||
}
|
||||
|
||||
fn block_disconnected(&self, header: &bitcoin::BlockHeader, _: u32) {
|
||||
fn block_disconnected(&self, header: &bitcoin::block::Header, _: u32) {
|
||||
self.disconnected_blocks
|
||||
.borrow_mut()
|
||||
.insert(header.block_hash());
|
||||
|
|
@ -264,10 +264,7 @@ mod tests {
|
|||
// If a new (worse, just one) block gets mined, nothing gets connected nor disconnected
|
||||
cm.poll_best_tip().await;
|
||||
assert_eq!(cm.last_known_block_header, best_tip);
|
||||
assert!(matches!(
|
||||
cm.dbm.lock().unwrap().load_last_known_block(),
|
||||
Err { .. }
|
||||
));
|
||||
assert!(cm.dbm.lock().unwrap().load_last_known_block().is_none());
|
||||
assert!(listener.connected_blocks.borrow().is_empty());
|
||||
assert!(listener.disconnected_blocks.borrow().is_empty());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ use teos::protos::private_tower_services_client::PrivateTowerServicesClient;
|
|||
use teos_common::appointment::Locator;
|
||||
use teos_common::UserId;
|
||||
|
||||
/// Prints the cli error to standard error and exits the process
|
||||
fn handle_error<T: std::fmt::Display>(error: T) {
|
||||
eprintln!("{}", error);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let opt = Opt::from_args();
|
||||
|
|
@ -27,7 +33,7 @@ async fn main() {
|
|||
let command = opt.command.clone();
|
||||
|
||||
// Load conf (from file or defaults) and patch it with the command line parameters received (if any)
|
||||
let mut conf = config::from_file::<Config>(path.join("teos.toml"));
|
||||
let mut conf = config::from_file::<Config>(&path.join("teos.toml"));
|
||||
conf.patch_with_options(opt);
|
||||
|
||||
let key = fs::read(&path.join("client-key.pem"))
|
||||
|
|
@ -47,7 +53,7 @@ async fn main() {
|
|||
.ca_certificate(ca_cert)
|
||||
.identity(Identity::from_pem(certificate, key));
|
||||
|
||||
let channel = Channel::from_shared(format!("http://{}:{}", conf.rpc_bind, conf.rpc_port))
|
||||
let channel = Channel::from_shared(format!("https://{}:{}", conf.rpc_bind, conf.rpc_port))
|
||||
.expect("Cannot create channel from endpoint")
|
||||
.tls_config(tls)
|
||||
.unwrap_or_else(|e| {
|
||||
|
|
@ -56,8 +62,8 @@ async fn main() {
|
|||
})
|
||||
.connect()
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("Could not connect to tower: {e:?}");
|
||||
.unwrap_or_else(|_| {
|
||||
eprintln!("Could not connect to tower. Is teosd running?");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
|
|
@ -80,10 +86,10 @@ async fn main() {
|
|||
Ok(appointments) => {
|
||||
println!("{}", pretty_json(&appointments.into_inner()).unwrap())
|
||||
}
|
||||
Err(status) => println!("{}", status.message()),
|
||||
Err(status) => handle_error(status.message()),
|
||||
}
|
||||
}
|
||||
Err(e) => println!("{e}"),
|
||||
Err(e) => handle_error(e),
|
||||
};
|
||||
}
|
||||
Command::GetTowerInfo => {
|
||||
|
|
@ -106,10 +112,10 @@ async fn main() {
|
|||
Ok(response) => {
|
||||
println!("{}", pretty_json(&response.into_inner()).unwrap())
|
||||
}
|
||||
Err(status) => println!("{}", status.message()),
|
||||
Err(status) => handle_error(status.message()),
|
||||
}
|
||||
}
|
||||
Err(e) => println!("{e}"),
|
||||
Err(e) => handle_error(e),
|
||||
};
|
||||
}
|
||||
Command::Stop => {
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ rpc_port = 8814
|
|||
# bitcoind
|
||||
btc_network = "mainnet"
|
||||
btc_rpc_user = "CSW"
|
||||
## Notice only user+password **OR** cookie is allowed as rpc auth, any other combination would be rejected
|
||||
btc_rpc_password = "NotSatoshi"
|
||||
btc_rpc_connect = "localhost"
|
||||
btc_rpc_cookie = "~/.bitcoin/.cookie"
|
||||
btc_rpc_port = 8332
|
||||
|
||||
# Flags
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! Logic related to the tower configuration and command line parameter parsing.
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use structopt::StructOpt;
|
||||
|
||||
|
|
@ -16,15 +16,12 @@ pub fn data_dir_absolute_path(data_dir: String) -> PathBuf {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn from_file<T: Default + serde::de::DeserializeOwned>(path: PathBuf) -> T {
|
||||
pub fn from_file<T: Default + serde::de::DeserializeOwned>(path: &PathBuf) -> T {
|
||||
match std::fs::read(path) {
|
||||
Ok(file_content) => toml::from_slice::<T>(&file_content).map_or_else(
|
||||
|e| {
|
||||
eprintln!("Couldn't parse config file: {e}");
|
||||
T::default()
|
||||
},
|
||||
|config| config,
|
||||
),
|
||||
Ok(file_content) => toml::from_slice::<T>(&file_content).unwrap_or_else(|e| {
|
||||
eprintln!("Couldn't parse config file: {e}");
|
||||
T::default()
|
||||
}),
|
||||
Err(_) => T::default(),
|
||||
}
|
||||
}
|
||||
|
|
@ -41,6 +38,14 @@ impl std::fmt::Display for ConfigError {
|
|||
|
||||
impl std::error::Error for ConfigError {}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum AuthMethod {
|
||||
UserPass,
|
||||
CookieFile,
|
||||
Multiple,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
/// Holds all the command line options.
|
||||
#[derive(StructOpt, Debug, Clone)]
|
||||
#[structopt(rename_all = "lowercase")]
|
||||
|
|
@ -66,14 +71,18 @@ pub struct Opt {
|
|||
#[structopt(long)]
|
||||
pub btc_network: Option<String>,
|
||||
|
||||
/// bitcoind rpcuser [default: user]
|
||||
/// bitcoind rpcuser
|
||||
#[structopt(long)]
|
||||
pub btc_rpc_user: Option<String>,
|
||||
|
||||
/// bitcoind rpcpassword [default: passwd]
|
||||
/// bitcoind rpcpassword
|
||||
#[structopt(long)]
|
||||
pub btc_rpc_password: Option<String>,
|
||||
|
||||
/// bitcoind rpccookie
|
||||
#[structopt(long)]
|
||||
pub btc_rpc_cookie: Option<String>,
|
||||
|
||||
/// bitcoind rpcconnect [default: localhost]
|
||||
#[structopt(long)]
|
||||
pub btc_rpc_connect: Option<String>,
|
||||
|
|
@ -102,6 +111,11 @@ pub struct Opt {
|
|||
#[structopt(long)]
|
||||
pub tor_support: bool,
|
||||
|
||||
/// Forces the tower to run even if the underlying chain has gone too far out of sync. This can only happen
|
||||
/// if the node is being run in pruned mode.
|
||||
#[structopt(long)]
|
||||
pub force_update: bool,
|
||||
|
||||
/// Tor control port [default: 9051]
|
||||
#[structopt(long)]
|
||||
pub tor_control_port: Option<u16>,
|
||||
|
|
@ -117,7 +131,7 @@ pub struct Opt {
|
|||
/// - Defaults
|
||||
/// - Configuration file
|
||||
/// - Command line options
|
||||
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
// API
|
||||
|
|
@ -131,6 +145,7 @@ pub struct Config {
|
|||
// Bitcoind
|
||||
pub btc_network: String,
|
||||
pub btc_rpc_user: String,
|
||||
pub btc_rpc_cookie: String,
|
||||
pub btc_rpc_password: String,
|
||||
pub btc_rpc_connect: String,
|
||||
pub btc_rpc_port: u16,
|
||||
|
|
@ -139,6 +154,7 @@ pub struct Config {
|
|||
pub debug: bool,
|
||||
pub deps_debug: bool,
|
||||
pub overwrite_key: bool,
|
||||
pub force_update: bool,
|
||||
|
||||
// General
|
||||
pub subscription_slots: u32,
|
||||
|
|
@ -158,6 +174,24 @@ pub struct Config {
|
|||
}
|
||||
|
||||
impl Config {
|
||||
/// The only combinations of valid authentication methods are:
|
||||
/// - User **AND** password
|
||||
/// - **OR** Cookie file
|
||||
//
|
||||
/// Any other combination will be rejected
|
||||
pub fn get_auth_method(&self) -> AuthMethod {
|
||||
match (
|
||||
self.btc_rpc_user.is_empty(),
|
||||
self.btc_rpc_password.is_empty(),
|
||||
self.btc_rpc_cookie.is_empty(),
|
||||
) {
|
||||
(false, false, true) => AuthMethod::UserPass,
|
||||
(true, true, false) => AuthMethod::CookieFile,
|
||||
(true, true, true) => AuthMethod::Invalid,
|
||||
_ => AuthMethod::Multiple,
|
||||
}
|
||||
}
|
||||
|
||||
/// Patches the configuration options with the command line options.
|
||||
pub fn patch_with_options(&mut self, options: Opt) {
|
||||
if options.api_bind.is_some() {
|
||||
|
|
@ -181,6 +215,9 @@ impl Config {
|
|||
if options.btc_rpc_password.is_some() {
|
||||
self.btc_rpc_password = options.btc_rpc_password.unwrap();
|
||||
}
|
||||
if options.btc_rpc_cookie.is_some() {
|
||||
self.btc_rpc_cookie = options.btc_rpc_cookie.unwrap();
|
||||
}
|
||||
if options.btc_rpc_connect.is_some() {
|
||||
self.btc_rpc_connect = options.btc_rpc_connect.unwrap();
|
||||
}
|
||||
|
|
@ -198,6 +235,7 @@ impl Config {
|
|||
self.debug |= options.debug;
|
||||
self.deps_debug |= options.deps_debug;
|
||||
self.overwrite_key = options.overwrite_key;
|
||||
self.force_update = options.force_update;
|
||||
}
|
||||
|
||||
/// Verifies that [Config] is properly built.
|
||||
|
|
@ -209,11 +247,14 @@ impl Config {
|
|||
/// This will also assign the default `btc_rpc_port` depending on the network if it has not
|
||||
/// been overwritten at this point.
|
||||
pub fn verify(&mut self) -> Result<(), ConfigError> {
|
||||
if self.btc_rpc_user == String::new() {
|
||||
return Err(ConfigError("btc_rpc_user must be set".to_owned()));
|
||||
}
|
||||
if self.btc_rpc_password == String::new() {
|
||||
return Err(ConfigError("btc_rpc_password must be set".to_owned()));
|
||||
let auth_method = self.get_auth_method();
|
||||
if auth_method == AuthMethod::Invalid {
|
||||
return Err(ConfigError("No valid bitcoind auth provided. Set either both btc_rpc_user/btc_rpc_password or btc_rpc_cookie".to_owned()));
|
||||
} else if auth_method == AuthMethod::Multiple {
|
||||
return Err(ConfigError(
|
||||
"Multiple bitcoind auth provided. Pick a single one (either btc_rpc_user/btc_rpc_password or btc_rpc_cookie)"
|
||||
.to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
// Normalize the network option to the ones used by bitcoind.
|
||||
|
|
@ -242,6 +283,27 @@ impl Config {
|
|||
pub fn is_default(&self) -> bool {
|
||||
self == &Config::default()
|
||||
}
|
||||
|
||||
/// Logs non-default options.
|
||||
pub fn log_non_default_options(&self) {
|
||||
let json_default_config = serde_json::json!(&Config::default());
|
||||
let json_config = serde_json::json!(&self);
|
||||
let sensitive_args = ["btc_rpc_user", "btc_rpc_password"];
|
||||
|
||||
for (key, value) in json_config.as_object().unwrap().iter() {
|
||||
if *value != json_default_config[key] {
|
||||
log::info!(
|
||||
"Custom config arg: {}: {}",
|
||||
key,
|
||||
if sensitive_args.contains(&key.as_str()) {
|
||||
"****".to_owned()
|
||||
} else {
|
||||
value.to_string()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
|
|
@ -263,12 +325,14 @@ impl Default for Config {
|
|||
btc_network: "mainnet".into(),
|
||||
btc_rpc_user: String::new(),
|
||||
btc_rpc_password: String::new(),
|
||||
btc_rpc_cookie: String::new(),
|
||||
btc_rpc_connect: "localhost".into(),
|
||||
btc_rpc_port: 0,
|
||||
|
||||
debug: false,
|
||||
deps_debug: false,
|
||||
overwrite_key: false,
|
||||
force_update: false,
|
||||
subscription_slots: 10000,
|
||||
subscription_duration: 4320,
|
||||
expiry_delta: 6,
|
||||
|
|
@ -297,6 +361,7 @@ mod tests {
|
|||
btc_network: None,
|
||||
btc_rpc_user: None,
|
||||
btc_rpc_password: None,
|
||||
btc_rpc_cookie: None,
|
||||
btc_rpc_connect: None,
|
||||
btc_rpc_port: None,
|
||||
data_dir: String::from("~/.teos"),
|
||||
|
|
@ -304,6 +369,7 @@ mod tests {
|
|||
debug: false,
|
||||
deps_debug: false,
|
||||
overwrite_key: false,
|
||||
force_update: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -323,7 +389,7 @@ mod tests {
|
|||
assert_eq!(config.api_bind, expected_value);
|
||||
|
||||
// Check the rest of fields are equal. The easiest is to just the field back and compare with a clone
|
||||
config.api_bind = config_clone.api_bind.clone();
|
||||
config.api_bind.clone_from(&config_clone.api_bind);
|
||||
assert_eq!(config, config_clone);
|
||||
}
|
||||
|
||||
|
|
@ -333,7 +399,7 @@ mod tests {
|
|||
// required to be updated by the user.
|
||||
let mut config = Config::default();
|
||||
assert!(
|
||||
matches!(config.verify(), Err(ConfigError(e)) if e.contains("btc_rpc_user must be set"))
|
||||
matches!(config.verify(), Err(ConfigError(e)) if e.contains("No valid bitcoind auth provided"))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
794
teos/src/dbm.rs
794
teos/src/dbm.rs
File diff suppressed because it is too large
Load diff
|
|
@ -23,7 +23,7 @@ impl UUID {
|
|||
pub fn new(locator: Locator, user_id: UserId) -> Self {
|
||||
let mut uuid_data = locator.to_vec();
|
||||
uuid_data.extend(user_id.0.serialize());
|
||||
UUID(ripemd160::Hash::hash(&uuid_data).into_inner())
|
||||
UUID(ripemd160::Hash::hash(&uuid_data).to_byte_array())
|
||||
}
|
||||
|
||||
/// Serializes the [UUID] returning its byte representation.
|
||||
|
|
@ -46,8 +46,6 @@ impl std::fmt::Display for UUID {
|
|||
/// An extended version of the appointment hold by the tower.
|
||||
///
|
||||
/// The [Appointment] is extended in terms of data, that is, it provides further information only relevant to the tower.
|
||||
/// Notice [ExtendedAppointment]s are not kept in memory but persisted on disk. The [Watcher](crate::watcher::Watcher)
|
||||
/// keeps [AppointmentSummary] instead.
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub(crate) struct ExtendedAppointment {
|
||||
/// The underlying appointment extended by [ExtendedAppointment].
|
||||
|
|
@ -60,18 +58,6 @@ pub(crate) struct ExtendedAppointment {
|
|||
pub start_block: u32,
|
||||
}
|
||||
|
||||
/// A summary of an appointment.
|
||||
///
|
||||
/// Contains the minimal amount of data the [Watcher](crate::watcher::Watcher) needs to keep in memory in order to
|
||||
/// watch for breaches.
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub(crate) struct AppointmentSummary {
|
||||
/// The [Appointment] locator.
|
||||
pub locator: Locator,
|
||||
/// The user this [Appointment] belongs to.
|
||||
pub user_id: UserId,
|
||||
}
|
||||
|
||||
impl ExtendedAppointment {
|
||||
/// Create a new [ExtendedAppointment].
|
||||
pub fn new(
|
||||
|
|
@ -103,12 +89,8 @@ impl ExtendedAppointment {
|
|||
self.inner.to_self_delay
|
||||
}
|
||||
|
||||
/// Computes the summary of the [ExtendedAppointment].
|
||||
pub fn get_summary(&self) -> AppointmentSummary {
|
||||
AppointmentSummary {
|
||||
locator: self.locator(),
|
||||
user_id: self.user_id,
|
||||
}
|
||||
pub fn uuid(&self) -> UUID {
|
||||
UUID::new(self.inner.locator, self.user_id)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -116,22 +98,14 @@ impl ExtendedAppointment {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use teos_common::appointment::Appointment;
|
||||
use teos_common::cryptography::get_random_bytes;
|
||||
use teos_common::test_utils::get_random_user_id;
|
||||
use crate::test_utils::generate_uuid;
|
||||
|
||||
#[test]
|
||||
fn test_get_summary() {
|
||||
let locator = Locator::from_slice(&get_random_bytes(16)).unwrap();
|
||||
let user_id = get_random_user_id();
|
||||
let signature = String::new();
|
||||
|
||||
let a = Appointment::new(locator, get_random_bytes(32), 42);
|
||||
let e = ExtendedAppointment::new(a, user_id, signature, 21);
|
||||
|
||||
let s = e.get_summary();
|
||||
|
||||
assert_eq!(e.locator(), s.locator);
|
||||
assert_eq!(e.user_id, s.user_id);
|
||||
fn test_uuid_ser_deser() {
|
||||
let original_uuid = generate_uuid();
|
||||
assert_eq!(
|
||||
UUID::from_slice(&original_uuid.to_vec()).unwrap(),
|
||||
original_uuid
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
//! Logic related to the Gatekeeper, the component in charge of managing access to the tower resources.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::iter::FromIterator;
|
||||
use lightning::chain;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use lightning::chain;
|
||||
|
||||
use teos_common::appointment::compute_appointment_slots;
|
||||
use teos_common::appointment::{compute_appointment_slots, Locator};
|
||||
use teos_common::constants::ENCRYPTED_BLOB_MAX_SIZE;
|
||||
use teos_common::cryptography;
|
||||
use teos_common::receipts::RegistrationReceipt;
|
||||
|
|
@ -17,7 +15,7 @@ use crate::dbm::DBM;
|
|||
use crate::extended_appointment::{ExtendedAppointment, UUID};
|
||||
|
||||
/// Data regarding a user subscription with the tower.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct UserInfo {
|
||||
/// Number of appointment slots available for a given user.
|
||||
pub(crate) available_slots: u32,
|
||||
|
|
@ -25,8 +23,6 @@ pub(crate) struct UserInfo {
|
|||
pub(crate) subscription_start: u32,
|
||||
/// Block height where the user subscription expires.
|
||||
pub(crate) subscription_expiry: u32,
|
||||
/// Map of appointment ids and the how many slots they take from the subscription.
|
||||
pub(crate) appointments: HashMap<UUID, u32>,
|
||||
}
|
||||
|
||||
impl UserInfo {
|
||||
|
|
@ -36,22 +32,6 @@ impl UserInfo {
|
|||
available_slots,
|
||||
subscription_start,
|
||||
subscription_expiry,
|
||||
appointments: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [UserInfo] instance with some associated appointments.
|
||||
pub fn with_appointments(
|
||||
available_slots: u32,
|
||||
subscription_start: u32,
|
||||
subscription_expiry: u32,
|
||||
appointments: HashMap<UUID, u32>,
|
||||
) -> Self {
|
||||
UserInfo {
|
||||
available_slots,
|
||||
subscription_start,
|
||||
subscription_expiry,
|
||||
appointments,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -134,8 +114,9 @@ impl Gatekeeper {
|
|||
}
|
||||
|
||||
/// Gets the data held by the tower about a given user.
|
||||
pub(crate) fn get_user_info(&self, user_id: UserId) -> Option<UserInfo> {
|
||||
self.registered_users.lock().unwrap().get(&user_id).cloned()
|
||||
pub(crate) fn get_user_info(&self, user_id: UserId) -> Option<(UserInfo, Vec<Locator>)> {
|
||||
let info = self.registered_users.lock().unwrap().get(&user_id).cloned();
|
||||
info.map(|info| (info, self.dbm.lock().unwrap().load_user_locators(user_id)))
|
||||
}
|
||||
|
||||
/// Authenticates a user.
|
||||
|
|
@ -219,7 +200,13 @@ impl Gatekeeper {
|
|||
// For updates, the difference between the existing appointment size and the update is computed.
|
||||
let mut registered_users = self.registered_users.lock().unwrap();
|
||||
let user_info = registered_users.get_mut(&user_id).unwrap();
|
||||
let used_slots = user_info.appointments.get(&uuid).map_or(0, |x| *x);
|
||||
let used_blob_size = self
|
||||
.dbm
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_appointment_length(uuid)
|
||||
.unwrap_or(0);
|
||||
let used_slots = compute_appointment_slots(used_blob_size, ENCRYPTED_BLOB_MAX_SIZE);
|
||||
|
||||
let required_slots =
|
||||
compute_appointment_slots(appointment.encrypted_blob().len(), ENCRYPTED_BLOB_MAX_SIZE);
|
||||
|
|
@ -228,7 +215,6 @@ impl Gatekeeper {
|
|||
if diff <= user_info.available_slots as i64 {
|
||||
// Filling / freeing slots depending on whether this is an update or not, and if it is bigger or smaller
|
||||
// than the old appointment
|
||||
user_info.appointments.insert(uuid, required_slots);
|
||||
user_info.available_slots = (user_info.available_slots as i64 - diff) as u32;
|
||||
|
||||
self.dbm.lock().unwrap().update_user(user_id, user_info);
|
||||
|
|
@ -258,56 +244,50 @@ impl Gatekeeper {
|
|||
|
||||
/// Gets a map of outdated users. Outdated users are those whose subscription has expired and the renewal grace period
|
||||
/// has already passed ([expiry_delta](Self::expiry_delta)).
|
||||
pub(crate) fn get_outdated_users(&self, block_height: u32) -> HashMap<UserId, HashSet<UUID>> {
|
||||
let registered_users = self.registered_users.lock().unwrap().clone();
|
||||
registered_users
|
||||
.into_iter()
|
||||
.filter(|(_, info)| block_height == info.subscription_expiry + self.expiry_delta)
|
||||
.map(|(id, info)| (id, info.appointments.keys().cloned().collect()))
|
||||
pub(crate) fn get_outdated_users(&self, block_height: u32) -> Vec<UserId> {
|
||||
self.registered_users
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
// NOTE: Ideally there won't be a user with `block_height > subscription_expiry + expiry_delta`, but
|
||||
// this might happen if we skip a couple of block connections due to a force update.
|
||||
.filter(|(_, info)| block_height >= info.subscription_expiry + self.expiry_delta)
|
||||
.map(|(user_id, _)| *user_id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Gets a set of outdated user ids.
|
||||
pub(crate) fn get_outdated_user_ids(&self, block_height: u32) -> HashSet<UserId> {
|
||||
self.get_outdated_users(block_height)
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get a map of outdated appointments (from any user).
|
||||
pub(crate) fn get_outdated_appointments(&self, block_height: u32) -> HashSet<UUID> {
|
||||
HashSet::from_iter(
|
||||
self.get_outdated_users(block_height)
|
||||
.into_values()
|
||||
.flatten(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Deletes a collection of appointments from the users' subscriptions (from memory only)
|
||||
/// and updates the available_slots count for the given user.
|
||||
/// Deletes these appointments from the database and updates the user's information.
|
||||
///
|
||||
/// Notice appointments are only de-linked from users, but not actually removed. This is because the [Gatekeeper]
|
||||
/// does not actually hold any [ExtendedAppointment](crate::extended_appointment::ExtendedAppointment) data,
|
||||
/// just references to them.
|
||||
pub(crate) fn delete_appointments_from_memory(
|
||||
&self,
|
||||
appointments: &HashMap<UUID, UserId>,
|
||||
) -> HashMap<UserId, UserInfo> {
|
||||
let mut updated_users = HashMap::new();
|
||||
let mut registered_users = self.registered_users.lock().unwrap();
|
||||
/// If `refund` is set, the appointments owners will get their slots refunded back.
|
||||
///
|
||||
/// DISCUSS: When `refund` is `false` we don't give back the slots to the user for the deleted appointments.
|
||||
/// This is to discourage misbehavior (sending bad appointments, either non-decryptable or rejected by the network).
|
||||
pub(crate) fn delete_appointments(&self, appointments: Vec<UUID>, refund: bool) {
|
||||
let mut dbm = self.dbm.lock().unwrap();
|
||||
|
||||
for (uuid, user_id) in appointments {
|
||||
// Remove the appointment from the appointment list and update the available slots
|
||||
if let Some(user_info) = registered_users.get_mut(user_id) {
|
||||
if let Some(x) = user_info.appointments.remove(uuid) {
|
||||
user_info.available_slots += x;
|
||||
}
|
||||
updated_users.insert(*user_id, user_info.clone());
|
||||
};
|
||||
let updated_users = if refund {
|
||||
let mut updated_users = HashMap::new();
|
||||
let mut registered_users = self.registered_users.lock().unwrap();
|
||||
// Give back the consumed slots to each user.
|
||||
for uuid in appointments.iter() {
|
||||
let (user_id, blob_size) = dbm.get_appointment_user_and_length(*uuid).unwrap();
|
||||
registered_users.get_mut(&user_id).unwrap().available_slots +=
|
||||
compute_appointment_slots(blob_size, ENCRYPTED_BLOB_MAX_SIZE);
|
||||
updated_users.insert(user_id, registered_users[&user_id]);
|
||||
}
|
||||
updated_users
|
||||
} else {
|
||||
// No updated users.
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
// An optimization for the case when only one appointment is being deleted without refunding.
|
||||
// This avoids creating a DB transaction for a single query.
|
||||
if appointments.len() == 1 && updated_users.is_empty() {
|
||||
dbm.remove_appointment(appointments[0])
|
||||
} else {
|
||||
dbm.batch_remove_appointments(&appointments, &updated_users);
|
||||
}
|
||||
|
||||
updated_users
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -317,19 +297,24 @@ impl chain::Listen for Gatekeeper {
|
|||
/// This is mainly used to keep track of time and expire / outdate subscriptions when needed.
|
||||
fn filtered_block_connected(
|
||||
&self,
|
||||
header: &bitcoin::BlockHeader,
|
||||
header: &bitcoin::block::Header,
|
||||
_: &chain::transaction::TransactionData,
|
||||
height: u32,
|
||||
) {
|
||||
log::info!("New block received: {}", header.block_hash());
|
||||
|
||||
// Expired user deletion is delayed. Users are deleted when their subscription is outdated, not expired.
|
||||
let outdated_users = self.get_outdated_user_ids(height);
|
||||
let outdated_users = self.get_outdated_users(height);
|
||||
if !outdated_users.is_empty() {
|
||||
self.registered_users
|
||||
.lock()
|
||||
.unwrap()
|
||||
.retain(|id, _| !outdated_users.contains(id));
|
||||
// Remove the outdated users from memory first.
|
||||
{
|
||||
let mut registered_users = self.registered_users.lock().unwrap();
|
||||
// Removing each outdated user in a loop is more efficient than retaining non-outdated users
|
||||
// because retaining would loop over all the available users which is always more than the outdated ones.
|
||||
for outdated_user in outdated_users.iter() {
|
||||
registered_users.remove(outdated_user);
|
||||
}
|
||||
}
|
||||
self.dbm.lock().unwrap().batch_remove_users(&outdated_users);
|
||||
}
|
||||
|
||||
|
|
@ -339,7 +324,7 @@ impl chain::Listen for Gatekeeper {
|
|||
}
|
||||
|
||||
/// Handles reorgs in the [Gatekeeper]. Simply updates the last_known_block_height.
|
||||
fn block_disconnected(&self, header: &bitcoin::BlockHeader, height: u32) {
|
||||
fn block_disconnected(&self, header: &bitcoin::block::Header, height: u32) {
|
||||
log::warn!("Block disconnected: {}", header.block_hash());
|
||||
// There's nothing to be done here but updating the last known block
|
||||
self.last_known_block_height
|
||||
|
|
@ -351,14 +336,13 @@ impl chain::Listen for Gatekeeper {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::test_utils::{
|
||||
generate_dummy_appointment, generate_dummy_appointment_with_user, generate_uuid, Blockchain,
|
||||
};
|
||||
use crate::test_utils::{generate_dummy_appointment_with_user, get_random_tracker, Blockchain};
|
||||
use lightning::chain::Listen;
|
||||
use teos_common::cryptography::{get_random_bytes, get_random_keypair};
|
||||
use teos_common::dbm::Error as DBError;
|
||||
use teos_common::test_utils::get_random_user_id;
|
||||
|
||||
use crate::responder::ConfirmationStatus;
|
||||
|
||||
const SLOTS: u32 = 21;
|
||||
const DURATION: u32 = 500;
|
||||
const EXPIRY_DELTA: u32 = 42;
|
||||
|
|
@ -381,21 +365,11 @@ mod tests {
|
|||
&self.registered_users
|
||||
}
|
||||
|
||||
pub(crate) fn add_outdated_user(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
outdates_at: u32,
|
||||
appointments: Option<Vec<UUID>>,
|
||||
) {
|
||||
pub(crate) fn add_outdated_user(&self, user_id: UserId, outdates_at: u32) {
|
||||
self.add_update_user(user_id).unwrap();
|
||||
let mut registered_users = self.registered_users.lock().unwrap();
|
||||
let mut user = registered_users.get_mut(&user_id).unwrap();
|
||||
let user = registered_users.get_mut(&user_id).unwrap();
|
||||
user.subscription_expiry = outdates_at - self.expiry_delta;
|
||||
if let Some(uuids) = appointments {
|
||||
for uuid in uuids.iter() {
|
||||
user.appointments.insert(*uuid, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -461,7 +435,7 @@ mod tests {
|
|||
|
||||
// Let's now provide data generated by an actual user, still the user is unknown
|
||||
let (user_sk, user_pk) = get_random_keypair();
|
||||
let signature = cryptography::sign(message, &user_sk).unwrap();
|
||||
let signature = cryptography::sign(message, &user_sk);
|
||||
assert_eq!(
|
||||
gatekeeper.authenticate_user(message, &signature),
|
||||
Err(AuthenticationFailure("User not found."))
|
||||
|
|
@ -568,25 +542,32 @@ mod tests {
|
|||
let available_slots = gatekeeper
|
||||
.add_update_appointment(user_id, uuid, &appointment)
|
||||
.unwrap();
|
||||
// Simulate the watcher adding the appointment in the database.
|
||||
gatekeeper
|
||||
.dbm
|
||||
.lock()
|
||||
.unwrap()
|
||||
.store_appointment(uuid, &appointment)
|
||||
.unwrap();
|
||||
|
||||
assert!(gatekeeper.registered_users.lock().unwrap()[&user_id]
|
||||
.appointments
|
||||
.contains_key(&uuid));
|
||||
let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap();
|
||||
assert!(user_locators.contains(&appointment.locator()));
|
||||
assert_eq!(slots_before, available_slots + 1);
|
||||
|
||||
// Slots should have been updated in the database too. Notice the appointment won't be there yet
|
||||
// given the Watcher is responsible for adding it, and it will do so after calling this method
|
||||
// Slots should have been updated in the database too.
|
||||
let mut loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap();
|
||||
assert_eq!(loaded_user.available_slots, available_slots);
|
||||
|
||||
// Adding the exact same appointment should leave the slots count unchanged
|
||||
// Adding the exact same appointment should leave the slots count unchanged.
|
||||
// We don't really need to update the appointment in the DB since it's the very same appointment.
|
||||
let mut updated_slot_count = gatekeeper
|
||||
.add_update_appointment(user_id, uuid, &appointment)
|
||||
.unwrap();
|
||||
assert!(gatekeeper.registered_users.lock().unwrap()[&user_id]
|
||||
.appointments
|
||||
.contains_key(&uuid));
|
||||
|
||||
let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap();
|
||||
assert!(user_locators.contains(&appointment.locator()));
|
||||
assert_eq!(updated_slot_count, available_slots);
|
||||
|
||||
loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap();
|
||||
assert_eq!(loaded_user.available_slots, updated_slot_count);
|
||||
|
||||
|
|
@ -596,10 +577,18 @@ mod tests {
|
|||
updated_slot_count = gatekeeper
|
||||
.add_update_appointment(user_id, uuid, &bigger_appointment)
|
||||
.unwrap();
|
||||
assert!(gatekeeper.registered_users.lock().unwrap()[&user_id]
|
||||
.appointments
|
||||
.contains_key(&uuid));
|
||||
// Simulate the watcher updating the appointment in the database.
|
||||
gatekeeper
|
||||
.dbm
|
||||
.lock()
|
||||
.unwrap()
|
||||
.update_appointment(uuid, &bigger_appointment)
|
||||
.unwrap();
|
||||
|
||||
let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap();
|
||||
assert!(user_locators.contains(&appointment.locator()));
|
||||
assert_eq!(updated_slot_count, available_slots - 1);
|
||||
|
||||
loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap();
|
||||
assert_eq!(loaded_user.available_slots, updated_slot_count);
|
||||
|
||||
|
|
@ -607,26 +596,43 @@ mod tests {
|
|||
updated_slot_count = gatekeeper
|
||||
.add_update_appointment(user_id, uuid, &appointment)
|
||||
.unwrap();
|
||||
assert!(gatekeeper.registered_users.lock().unwrap()[&user_id]
|
||||
.appointments
|
||||
.contains_key(&uuid));
|
||||
// Simulate the watcher updating the appointment in the database.
|
||||
gatekeeper
|
||||
.dbm
|
||||
.lock()
|
||||
.unwrap()
|
||||
.update_appointment(uuid, &appointment)
|
||||
.unwrap();
|
||||
|
||||
let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap();
|
||||
assert!(user_locators.contains(&appointment.locator()));
|
||||
assert_eq!(updated_slot_count, available_slots);
|
||||
|
||||
loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap();
|
||||
assert_eq!(loaded_user.available_slots, updated_slot_count);
|
||||
|
||||
// Adding an appointment with a different uuid should not count as an update
|
||||
let new_uuid = generate_uuid();
|
||||
let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None);
|
||||
updated_slot_count = gatekeeper
|
||||
.add_update_appointment(user_id, new_uuid, &appointment)
|
||||
.add_update_appointment(user_id, uuid, &appointment)
|
||||
.unwrap();
|
||||
assert!(gatekeeper.registered_users.lock().unwrap()[&user_id]
|
||||
.appointments
|
||||
.contains_key(&new_uuid));
|
||||
// Simulate the watcher adding the appointment in the database.
|
||||
gatekeeper
|
||||
.dbm
|
||||
.lock()
|
||||
.unwrap()
|
||||
.store_appointment(uuid, &appointment)
|
||||
.unwrap();
|
||||
|
||||
let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap();
|
||||
assert!(user_locators.contains(&appointment.locator()));
|
||||
assert_eq!(updated_slot_count, available_slots - 1);
|
||||
|
||||
loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap();
|
||||
assert_eq!(loaded_user.available_slots, updated_slot_count);
|
||||
|
||||
// Finally, trying to add an appointment when the user has no enough slots should fail
|
||||
let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None);
|
||||
gatekeeper
|
||||
.registered_users
|
||||
.lock()
|
||||
|
|
@ -635,9 +641,10 @@ mod tests {
|
|||
.unwrap()
|
||||
.available_slots = 0;
|
||||
assert!(matches!(
|
||||
gatekeeper.add_update_appointment(user_id, generate_uuid(), &appointment),
|
||||
gatekeeper.add_update_appointment(user_id, uuid, &appointment),
|
||||
Err(NotEnoughSlots)
|
||||
));
|
||||
|
||||
// The entry in the database should remain unchanged in this case
|
||||
loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap();
|
||||
assert_eq!(loaded_user.available_slots, updated_slot_count);
|
||||
|
|
@ -683,144 +690,175 @@ mod tests {
|
|||
|
||||
// Initially, there are not outdated users, so querying any block height should return an empty map
|
||||
for i in 0..start_height {
|
||||
assert_eq!(gatekeeper.get_outdated_users(i).len(), 0);
|
||||
assert_eq!(gatekeeper.get_outdated_users(i), vec![]);
|
||||
}
|
||||
|
||||
// Adding a user whose subscription is outdated should return an entry
|
||||
let user_id = get_random_user_id();
|
||||
gatekeeper.add_update_user(user_id).unwrap();
|
||||
|
||||
// Add also an appointment so we can check the returned data
|
||||
let appointment = generate_dummy_appointment(None);
|
||||
let uuid = generate_uuid();
|
||||
gatekeeper
|
||||
.add_update_appointment(user_id, uuid, &appointment)
|
||||
.unwrap();
|
||||
|
||||
// Check that data is not yet outdated
|
||||
assert_eq!(gatekeeper.get_outdated_users(start_height).len(), 0);
|
||||
assert_eq!(gatekeeper.get_outdated_users(start_height), vec![]);
|
||||
|
||||
// Add an outdated user and check again
|
||||
gatekeeper.add_outdated_user(user_id, start_height, None);
|
||||
let outdated_users = gatekeeper.get_outdated_users(start_height);
|
||||
assert_eq!(outdated_users.len(), 1);
|
||||
assert_eq!(outdated_users[&user_id], HashSet::from_iter([uuid]));
|
||||
gatekeeper.add_outdated_user(user_id, start_height);
|
||||
assert_eq!(gatekeeper.get_outdated_users(start_height), vec![user_id]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_outdated_appointments() {
|
||||
let start_height = START_HEIGHT as u32 + EXPIRY_DELTA;
|
||||
let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(start_height as usize));
|
||||
|
||||
// get_outdated_appointments returns a list of appointments that were outdated at a given block height, indistinguishably of their user.
|
||||
|
||||
// If there are no outdated users, there cannot be outdated appointments
|
||||
for i in 0..start_height {
|
||||
assert_eq!(gatekeeper.get_outdated_appointments(i).len(), 0);
|
||||
}
|
||||
|
||||
// Adding data about different users and appointments should return a flattened list of appointments
|
||||
let user1_id = get_random_user_id();
|
||||
let user2_id = get_random_user_id();
|
||||
let uuid1 = generate_uuid();
|
||||
let uuid2 = generate_uuid();
|
||||
|
||||
// Manually set the user expiry for the test
|
||||
for (user_id, uuid) in [(user1_id, uuid1), (user2_id, uuid2)] {
|
||||
gatekeeper.add_outdated_user(user_id, start_height, Some(Vec::from_iter([uuid])));
|
||||
}
|
||||
|
||||
let outdated_appointments = gatekeeper.get_outdated_appointments(start_height);
|
||||
assert_eq!(outdated_appointments.len(), 2);
|
||||
assert!(outdated_appointments.contains(&uuid1));
|
||||
assert!(outdated_appointments.contains(&uuid2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_appointments_from_memory() {
|
||||
fn test_delete_appointments_without_refund() {
|
||||
let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT));
|
||||
let n_users = 100;
|
||||
let n_apps = 10;
|
||||
let mut uuids_to_delete = Vec::new();
|
||||
let mut rest = Vec::new();
|
||||
let mut trackers = Vec::new();
|
||||
let mut users_info = HashMap::new();
|
||||
|
||||
// delete_appointments will remove a list of appointments from the Gatekeeper (as long as they exist)
|
||||
let mut all_appointments = HashMap::new();
|
||||
let mut to_be_deleted = HashMap::new();
|
||||
let mut rest = HashMap::new();
|
||||
for i in 1..11 {
|
||||
for _ in 0..n_users {
|
||||
let user_id = get_random_user_id();
|
||||
let uuid = generate_uuid();
|
||||
all_appointments.insert(uuid, user_id);
|
||||
|
||||
if i % 2 == 0 {
|
||||
to_be_deleted.insert(uuid, user_id);
|
||||
} else {
|
||||
rest.insert(uuid, user_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Calling the method with unknown data should work but do nothing
|
||||
assert!(gatekeeper.registered_users.lock().unwrap().is_empty());
|
||||
assert!(gatekeeper
|
||||
.delete_appointments_from_memory(&all_appointments)
|
||||
.is_empty());
|
||||
|
||||
// If there's matching data in the gatekeeper it should be deleted
|
||||
for (uuid, user_id) in to_be_deleted.iter() {
|
||||
gatekeeper.add_update_user(*user_id).unwrap();
|
||||
gatekeeper
|
||||
.add_update_appointment(*user_id, *uuid, &generate_dummy_appointment(None))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Check before deleting
|
||||
assert_eq!(gatekeeper.registered_users.lock().unwrap().len(), 5);
|
||||
for (uuid, user_id) in to_be_deleted.iter() {
|
||||
assert!(gatekeeper.registered_users.lock().unwrap()[user_id]
|
||||
.appointments
|
||||
.contains_key(uuid));
|
||||
|
||||
// The slot count should be decreased now too (both in memory and in the database)
|
||||
assert_ne!(
|
||||
gatekeeper.registered_users.lock().unwrap()[user_id].available_slots,
|
||||
gatekeeper.subscription_slots
|
||||
);
|
||||
assert_ne!(
|
||||
gatekeeper.add_update_user(user_id).unwrap();
|
||||
for i in 0..n_apps {
|
||||
let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None);
|
||||
gatekeeper
|
||||
.add_update_appointment(user_id, uuid, &appointment)
|
||||
.unwrap();
|
||||
// Add the appointment to the database. This is normally done by the Watcher.
|
||||
gatekeeper
|
||||
.dbm
|
||||
.lock()
|
||||
.unwrap()
|
||||
.load_user(*user_id)
|
||||
.store_appointment(uuid, &appointment)
|
||||
.unwrap();
|
||||
if i % 2 == 0 {
|
||||
uuids_to_delete.push(uuid);
|
||||
} else {
|
||||
rest.push(uuid);
|
||||
}
|
||||
// Also trigger some of these appointments as trackers.
|
||||
if i % 5 == 0 {
|
||||
gatekeeper
|
||||
.dbm
|
||||
.lock()
|
||||
.unwrap()
|
||||
.store_tracker(
|
||||
uuid,
|
||||
&get_random_tracker(user_id, ConfirmationStatus::ConfirmedIn(42)),
|
||||
)
|
||||
.unwrap();
|
||||
trackers.push(uuid);
|
||||
}
|
||||
}
|
||||
users_info.insert(user_id, gatekeeper.get_user_info(user_id).unwrap().0);
|
||||
}
|
||||
|
||||
// Delete these appointments without refunding their owners.
|
||||
gatekeeper.delete_appointments(uuids_to_delete.clone(), false);
|
||||
|
||||
for uuid in uuids_to_delete.clone() {
|
||||
assert!(!gatekeeper.dbm.lock().unwrap().appointment_exists(uuid));
|
||||
}
|
||||
for uuid in rest {
|
||||
assert!(gatekeeper.dbm.lock().unwrap().appointment_exists(uuid));
|
||||
}
|
||||
for uuid in trackers {
|
||||
if uuids_to_delete.contains(&uuid) {
|
||||
// The tracker should be deleted as well.
|
||||
assert!(!gatekeeper.dbm.lock().unwrap().tracker_exists(uuid));
|
||||
} else {
|
||||
assert!(gatekeeper.dbm.lock().unwrap().tracker_exists(uuid));
|
||||
}
|
||||
}
|
||||
|
||||
for (user_id, user_info_before_deletion) in users_info {
|
||||
// Since `refund` was false, the users' slots should not have changed after deleting appointments.
|
||||
let (user_info_after_deletion, _) = gatekeeper.get_user_info(user_id).unwrap();
|
||||
assert_eq!(user_info_after_deletion, user_info_before_deletion);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_appointments_with_refund() {
|
||||
let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT));
|
||||
let n_users = 100;
|
||||
let n_apps = 10;
|
||||
let mut uuids_to_delete = Vec::new();
|
||||
let mut rest = Vec::new();
|
||||
let mut trackers = Vec::new();
|
||||
let mut users_remaining_slots = HashMap::new();
|
||||
|
||||
for _ in 0..n_users {
|
||||
let user_id = get_random_user_id();
|
||||
gatekeeper.add_update_user(user_id).unwrap();
|
||||
let mut user_remaining_slots =
|
||||
gatekeeper.get_user_info(user_id).unwrap().0.available_slots;
|
||||
for i in 0..n_apps {
|
||||
let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None);
|
||||
gatekeeper
|
||||
.add_update_appointment(user_id, uuid, &appointment)
|
||||
.unwrap();
|
||||
// Add the appointment to the database. This is normally done by the Watcher.
|
||||
gatekeeper
|
||||
.dbm
|
||||
.lock()
|
||||
.unwrap()
|
||||
.available_slots,
|
||||
gatekeeper.subscription_slots
|
||||
);
|
||||
}
|
||||
for (_, user_id) in rest.iter() {
|
||||
assert!(!gatekeeper
|
||||
.registered_users
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains_key(user_id));
|
||||
.store_appointment(uuid, &appointment)
|
||||
.unwrap();
|
||||
if i % 2 == 0 {
|
||||
// We don't reduce the remaining slots for the appointments which are
|
||||
// going to delete since we will refund their owners.
|
||||
uuids_to_delete.push(uuid);
|
||||
} else {
|
||||
rest.push(uuid);
|
||||
user_remaining_slots -= compute_appointment_slots(
|
||||
appointment.encrypted_blob().len(),
|
||||
ENCRYPTED_BLOB_MAX_SIZE,
|
||||
);
|
||||
}
|
||||
// Also trigger some of these appointments as trackers.
|
||||
if i % 5 == 0 {
|
||||
gatekeeper
|
||||
.dbm
|
||||
.lock()
|
||||
.unwrap()
|
||||
.store_tracker(
|
||||
uuid,
|
||||
&get_random_tracker(user_id, ConfirmationStatus::ConfirmedIn(42)),
|
||||
)
|
||||
.unwrap();
|
||||
trackers.push(uuid);
|
||||
}
|
||||
}
|
||||
users_remaining_slots.insert(user_id, user_remaining_slots);
|
||||
}
|
||||
|
||||
// And after
|
||||
gatekeeper.delete_appointments_from_memory(&all_appointments);
|
||||
for (uuid, user_id) in to_be_deleted.iter() {
|
||||
assert!(!gatekeeper.registered_users.lock().unwrap()[user_id]
|
||||
.appointments
|
||||
.contains_key(uuid));
|
||||
// Delete these appointments and refund their owners their slots back.
|
||||
gatekeeper.delete_appointments(uuids_to_delete.clone(), true);
|
||||
|
||||
// The slot count is back to default
|
||||
for uuid in uuids_to_delete.clone() {
|
||||
assert!(!gatekeeper.dbm.lock().unwrap().appointment_exists(uuid));
|
||||
}
|
||||
for uuid in rest {
|
||||
assert!(gatekeeper.dbm.lock().unwrap().appointment_exists(uuid));
|
||||
}
|
||||
for uuid in trackers {
|
||||
if uuids_to_delete.contains(&uuid) {
|
||||
// The tracker should be deleted as well.
|
||||
assert!(!gatekeeper.dbm.lock().unwrap().tracker_exists(uuid));
|
||||
} else {
|
||||
assert!(gatekeeper.dbm.lock().unwrap().tracker_exists(uuid));
|
||||
}
|
||||
}
|
||||
|
||||
for (user_id, correct_remaining_slots) in users_remaining_slots {
|
||||
let remaining_slots_from_db =
|
||||
gatekeeper.get_user_info(user_id).unwrap().0.available_slots;
|
||||
assert_eq!(remaining_slots_from_db, correct_remaining_slots);
|
||||
assert_eq!(
|
||||
gatekeeper.registered_users.lock().unwrap()[user_id].available_slots,
|
||||
gatekeeper.subscription_slots
|
||||
gatekeeper.registered_users.lock().unwrap()[&user_id].available_slots,
|
||||
correct_remaining_slots
|
||||
);
|
||||
}
|
||||
for (_, user_id) in rest.iter() {
|
||||
assert!(!gatekeeper
|
||||
.registered_users
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains_key(user_id));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -836,7 +874,7 @@ mod tests {
|
|||
let user3_id = get_random_user_id();
|
||||
|
||||
for user_id in &[user1_id, user2_id, user3_id] {
|
||||
gatekeeper.add_outdated_user(*user_id, chain.tip().height + 1, None)
|
||||
gatekeeper.add_outdated_user(*user_id, chain.tip().height + 1)
|
||||
}
|
||||
|
||||
// Connect a new block. Outdated users are deleted
|
||||
|
|
@ -849,10 +887,7 @@ mod tests {
|
|||
.lock()
|
||||
.unwrap()
|
||||
.contains_key(user_id));
|
||||
assert!(matches!(
|
||||
gatekeeper.dbm.lock().unwrap().load_user(*user_id),
|
||||
Err(DBError::NotFound)
|
||||
));
|
||||
assert!(gatekeeper.dbm.lock().unwrap().load_user(*user_id).is_none());
|
||||
}
|
||||
|
||||
// Check that the last_known_block_header has been properly updated
|
||||
|
|
|
|||
207
teos/src/main.rs
207
teos/src/main.rs
|
|
@ -3,27 +3,26 @@ use simple_logger::SimpleLogger;
|
|||
use std::fs;
|
||||
use std::io::ErrorKind;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use structopt::StructOpt;
|
||||
use tokio::task;
|
||||
use tonic::transport::{Certificate, Server, ServerTlsConfig};
|
||||
|
||||
use bitcoin::network::constants::Network;
|
||||
use bitcoin::network::Network;
|
||||
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
|
||||
use bitcoincore_rpc::{Auth, Client};
|
||||
use bitcoincore_rpc::{Auth, Client, RpcApi};
|
||||
use lightning_block_sync::init::validate_best_block_header;
|
||||
use lightning_block_sync::poll::{
|
||||
ChainPoller, Poll, Validate, ValidatedBlock, ValidatedBlockHeader,
|
||||
};
|
||||
use lightning_block_sync::{BlockSource, SpvClient, UnboundedCache};
|
||||
use lightning_block_sync::{BlockSource, BlockSourceError, SpvClient, UnboundedCache};
|
||||
|
||||
use teos::api::internal::InternalAPI;
|
||||
use teos::api::{http, tor::TorAPI};
|
||||
use teos::bitcoin_cli::BitcoindClient;
|
||||
use teos::carrier::Carrier;
|
||||
use teos::chain_monitor::ChainMonitor;
|
||||
use teos::config::{self, Config, Opt};
|
||||
use teos::config::{self, AuthMethod, Config, Opt};
|
||||
use teos::dbm::DBM;
|
||||
use teos::gatekeeper::Gatekeeper;
|
||||
use teos::protos as msgs;
|
||||
|
|
@ -41,22 +40,20 @@ async fn get_last_n_blocks<B, T>(
|
|||
poller: &mut ChainPoller<B, T>,
|
||||
mut last_known_block: ValidatedBlockHeader,
|
||||
n: usize,
|
||||
) -> Vec<ValidatedBlock>
|
||||
) -> Result<Vec<ValidatedBlock>, BlockSourceError>
|
||||
where
|
||||
B: DerefMut<Target = T> + Sized + Send + Sync,
|
||||
T: BlockSource,
|
||||
{
|
||||
let mut last_n_blocks = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
let block = poller.fetch_block(&last_known_block).await.unwrap();
|
||||
last_known_block = poller
|
||||
.look_up_previous_header(&last_known_block)
|
||||
.await
|
||||
.unwrap();
|
||||
log::debug!("Fetching block #{}", last_known_block.height);
|
||||
let block = poller.fetch_block(&last_known_block).await?;
|
||||
last_known_block = poller.look_up_previous_header(&last_known_block).await?;
|
||||
last_n_blocks.push(block);
|
||||
}
|
||||
|
||||
last_n_blocks
|
||||
Ok(last_n_blocks)
|
||||
}
|
||||
|
||||
fn create_new_tower_keypair(db: &DBM) -> (SecretKey, PublicKey) {
|
||||
|
|
@ -69,7 +66,7 @@ fn create_new_tower_keypair(db: &DBM) -> (SecretKey, PublicKey) {
|
|||
async fn main() {
|
||||
let opt = Opt::from_args();
|
||||
let path = config::data_dir_absolute_path(opt.data_dir.clone());
|
||||
|
||||
let conf_file_path = path.join("teos.toml");
|
||||
// Create data dir if it does not exist
|
||||
fs::create_dir_all(&path).unwrap_or_else(|e| {
|
||||
eprintln!("Cannot create data dir: {e:?}");
|
||||
|
|
@ -77,7 +74,7 @@ async fn main() {
|
|||
});
|
||||
|
||||
// Load conf (from file or defaults) and patch it with the command line parameters received (if any)
|
||||
let mut conf = config::from_file::<Config>(path.join("teos.toml"));
|
||||
let mut conf = config::from_file::<Config>(&conf_file_path);
|
||||
let is_default = conf.is_default();
|
||||
conf.patch_with_options(opt);
|
||||
conf.verify().unwrap_or_else(|e| {
|
||||
|
|
@ -103,18 +100,27 @@ async fn main() {
|
|||
.init()
|
||||
.unwrap();
|
||||
|
||||
if is_default {
|
||||
log::info!("Loading default configuration")
|
||||
} else {
|
||||
log::info!("Loading configuration from file")
|
||||
}
|
||||
|
||||
// Create network dir
|
||||
let path_network = path.join(conf.btc_network.clone());
|
||||
fs::create_dir_all(&path_network).unwrap_or_else(|e| {
|
||||
eprintln!("Cannot create network dir: {e:?}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
// Log default data dir
|
||||
log::info!("Default data directory: {:?}", &path);
|
||||
|
||||
// Log datadir path
|
||||
log::info!("Using data directory: {:?}", &path_network);
|
||||
|
||||
// Log config file path based on whether the config file is found or not
|
||||
if is_default {
|
||||
log::info!("Config file: {:?} (not found, skipping)", &conf_file_path);
|
||||
} else {
|
||||
log::info!("Config file: {:?}", &conf_file_path);
|
||||
conf.log_non_default_options();
|
||||
}
|
||||
|
||||
let dbm = Arc::new(Mutex::new(
|
||||
DBM::new(path_network.join("teos_db.sql3")).unwrap(),
|
||||
));
|
||||
|
|
@ -126,24 +132,29 @@ async fn main() {
|
|||
if conf.overwrite_key {
|
||||
log::info!("Overwriting tower keys");
|
||||
create_new_tower_keypair(&locked_db)
|
||||
} else if let Some(sk) = locked_db.load_tower_key() {
|
||||
(sk, PublicKey::from_secret_key(&Secp256k1::new(), &sk))
|
||||
} else {
|
||||
match locked_db.load_tower_key() {
|
||||
Ok(sk) => (sk, PublicKey::from_secret_key(&Secp256k1::new(), &sk)),
|
||||
Err(_) => {
|
||||
log::info!("Tower keys not found. Creating a fresh set");
|
||||
create_new_tower_keypair(&locked_db)
|
||||
}
|
||||
}
|
||||
log::info!("Tower keys not found. Creating a fresh set");
|
||||
create_new_tower_keypair(&locked_db)
|
||||
}
|
||||
};
|
||||
log::info!("tower_id: {tower_pk}");
|
||||
|
||||
let btc_rpc_auth = match conf.get_auth_method() {
|
||||
AuthMethod::CookieFile => {
|
||||
Auth::CookieFile(config::data_dir_absolute_path(conf.btc_rpc_cookie))
|
||||
}
|
||||
AuthMethod::UserPass => Auth::UserPass(conf.btc_rpc_user, conf.btc_rpc_password),
|
||||
// Notice an invalid conf would have failed on `Config::verify()`
|
||||
_ => unreachable!("A verified conf will only have one of these two auth methods"),
|
||||
};
|
||||
|
||||
// Initialize our bitcoind client
|
||||
let (bitcoin_cli, bitcoind_reachable) = match BitcoindClient::new(
|
||||
&conf.btc_rpc_connect,
|
||||
conf.btc_rpc_port,
|
||||
&conf.btc_rpc_user,
|
||||
&conf.btc_rpc_password,
|
||||
btc_rpc_auth.clone(),
|
||||
&conf.btc_network,
|
||||
)
|
||||
.await
|
||||
|
|
@ -172,22 +183,61 @@ async fn main() {
|
|||
let rpc = Arc::new(
|
||||
Client::new(
|
||||
&format!("{schema}{}:{}", conf.btc_rpc_connect, conf.btc_rpc_port),
|
||||
Auth::UserPass(conf.btc_rpc_user.clone(), conf.btc_rpc_password.clone()),
|
||||
btc_rpc_auth,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let mut derefed = bitcoin_cli.deref();
|
||||
// Load last known block from DB if found. Poll it from Bitcoind otherwise.
|
||||
let last_known_block = dbm.lock().unwrap().load_last_known_block();
|
||||
let tip = if let Ok(block_hash) = last_known_block {
|
||||
derefed
|
||||
let tip = if let Some(block_hash) = last_known_block {
|
||||
let mut last_known_header = derefed
|
||||
.get_header(&block_hash, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.validate(block_hash)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
log::info!(
|
||||
"Last known block: {} (height: {})",
|
||||
last_known_header.header.block_hash(),
|
||||
last_known_header.height
|
||||
);
|
||||
|
||||
// If we are running in pruned mode some data may be missing (if we happen to have been offline for a while)
|
||||
if let Some(prune_height) = rpc.get_blockchain_info().unwrap().prune_height {
|
||||
if last_known_header.height - IRREVOCABLY_RESOLVED + 1 < prune_height as u32 {
|
||||
log::warn!(
|
||||
"Cannot load blocks in the range {}-{}. Chain has gone too far out of sync",
|
||||
last_known_header.height - IRREVOCABLY_RESOLVED + 1,
|
||||
last_known_header.height
|
||||
);
|
||||
if conf.force_update {
|
||||
log::info!("Forcing a backend update");
|
||||
// We want to grab the first IRREVOCABLY_RESOLVED we know about for the initial cache
|
||||
// So we can perform transitions from there onwards.
|
||||
let target_height = prune_height + IRREVOCABLY_RESOLVED as u64;
|
||||
let target_hash = rpc.get_block_hash(target_height).unwrap();
|
||||
last_known_header = derefed
|
||||
.get_header(
|
||||
&rpc.get_block_hash(target_height).unwrap(),
|
||||
Some(target_height as u32),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.validate(target_hash)
|
||||
.unwrap();
|
||||
} else {
|
||||
log::error!(
|
||||
"The underlying chain has gone too far out of sync. The tower block cache cannot be initialized. Run with --forceupdate to force update. THIS WILL, POTENTIALLY, MAKE THE TOWER MISS SOME OF ITS APPOINTMENTS"
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
last_known_header
|
||||
} else {
|
||||
validate_best_block_header(&mut derefed).await.unwrap()
|
||||
validate_best_block_header(&derefed).await.unwrap()
|
||||
};
|
||||
|
||||
// DISCUSS: This is not really required (and only triggered in regtest). This is only in place so the caches can be
|
||||
|
|
@ -202,17 +252,11 @@ async fn main() {
|
|||
std::process::exit(1);
|
||||
}
|
||||
|
||||
log::info!("Last known block: {}", tip.header.block_hash());
|
||||
|
||||
// This is how chain poller names bitcoin networks.
|
||||
let btc_network = match conf.btc_network.as_str() {
|
||||
"main" => "bitcoin",
|
||||
"test" => "testnet",
|
||||
any => any,
|
||||
};
|
||||
|
||||
let mut poller = ChainPoller::new(&mut derefed, Network::from_str(btc_network).unwrap());
|
||||
let last_n_blocks = get_last_n_blocks(&mut poller, tip, IRREVOCABLY_RESOLVED as usize).await;
|
||||
log::info!(
|
||||
"Current chain tip: {} (height: {})",
|
||||
tip.header.block_hash(),
|
||||
tip.height
|
||||
);
|
||||
|
||||
// Build components
|
||||
let gatekeeper = Arc::new(Gatekeeper::new(
|
||||
|
|
@ -223,23 +267,38 @@ async fn main() {
|
|||
dbm.clone(),
|
||||
));
|
||||
|
||||
let carrier = Carrier::new(rpc, bitcoind_reachable.clone(), tip.height);
|
||||
let responder = Arc::new(Responder::new(
|
||||
&last_n_blocks,
|
||||
tip.height,
|
||||
carrier,
|
||||
gatekeeper.clone(),
|
||||
dbm.clone(),
|
||||
));
|
||||
let watcher = Arc::new(Watcher::new(
|
||||
gatekeeper.clone(),
|
||||
responder.clone(),
|
||||
&last_n_blocks[0..6],
|
||||
tip.height,
|
||||
tower_sk,
|
||||
TowerId(tower_pk),
|
||||
dbm.clone(),
|
||||
));
|
||||
let mut poller = ChainPoller::new(
|
||||
&mut derefed,
|
||||
Network::from_core_arg(&conf.btc_network).unwrap(),
|
||||
);
|
||||
let (responder, watcher) = {
|
||||
let last_n_blocks = get_last_n_blocks(&mut poller, tip, IRREVOCABLY_RESOLVED as usize)
|
||||
.await.unwrap_or_else(|e| {
|
||||
// I'm pretty sure this can only happen if we are pulling blocks from the target to the prune height, and by the time we get to
|
||||
// the end at least one has been pruned.
|
||||
log::error!("Couldn't load the latest {IRREVOCABLY_RESOLVED} blocks. Please try again (Error: {})", e.into_inner());
|
||||
std::process::exit(1);
|
||||
}
|
||||
);
|
||||
|
||||
let responder = Arc::new(Responder::new(
|
||||
&last_n_blocks,
|
||||
tip.height,
|
||||
Carrier::new(rpc, bitcoind_reachable.clone(), tip.height),
|
||||
gatekeeper.clone(),
|
||||
dbm.clone(),
|
||||
));
|
||||
let watcher = Arc::new(Watcher::new(
|
||||
gatekeeper.clone(),
|
||||
responder.clone(),
|
||||
&last_n_blocks[0..6],
|
||||
tip.height,
|
||||
tower_sk,
|
||||
TowerId(tower_pk),
|
||||
dbm.clone(),
|
||||
));
|
||||
(responder, watcher)
|
||||
};
|
||||
|
||||
if watcher.is_fresh() & responder.is_fresh() & gatekeeper.is_fresh() {
|
||||
log::info!("Fresh bootstrap");
|
||||
|
|
@ -248,14 +307,14 @@ async fn main() {
|
|||
}
|
||||
|
||||
let (shutdown_trigger, shutdown_signal_rpc_api) = triggered::trigger();
|
||||
let shutdown_signal_internal_rpc_api = shutdown_signal_rpc_api.clone();
|
||||
let shutdown_signal_internal_api = shutdown_signal_rpc_api.clone();
|
||||
let shutdown_signal_http = shutdown_signal_rpc_api.clone();
|
||||
let shutdown_signal_cm = shutdown_signal_rpc_api.clone();
|
||||
let shutdown_signal_tor = shutdown_signal_rpc_api.clone();
|
||||
|
||||
// The ordering here actually matters. Listeners are called by order, and we want the gatekeeper to be called
|
||||
// last, so both the Watcher and the Responder can query the necessary data from it during data deletion.
|
||||
let listener = &(watcher.clone(), &(responder, gatekeeper));
|
||||
// first so it updates the users' states and both the Watcher and the Responder operate only on registered users.
|
||||
let listener = &(gatekeeper, &(watcher.clone(), responder));
|
||||
let cache = &mut UnboundedCache::new();
|
||||
let spv_client = SpvClient::new(tip, poller, cache, listener);
|
||||
let mut chain_monitor = ChainMonitor::new(
|
||||
|
|
@ -300,24 +359,20 @@ async fn main() {
|
|||
None
|
||||
};
|
||||
|
||||
let rpc_api = Arc::new(InternalAPI::new(
|
||||
let internal_api = Arc::new(InternalAPI::new(
|
||||
watcher,
|
||||
addresses,
|
||||
bitcoind_reachable.clone(),
|
||||
shutdown_trigger,
|
||||
));
|
||||
let internal_rpc_api = rpc_api.clone();
|
||||
let internal_api_cloned = internal_api.clone();
|
||||
|
||||
let rpc_api_addr = format!("{}:{}", conf.rpc_bind, conf.rpc_port)
|
||||
.parse()
|
||||
.unwrap();
|
||||
let internal_rpc_api_addr = format!("{}:{}", conf.internal_api_bind, conf.internal_api_port)
|
||||
let internal_api_addr = format!("{}:{}", conf.internal_api_bind, conf.internal_api_port)
|
||||
.parse()
|
||||
.unwrap();
|
||||
let internal_rpc_api_uri = format!(
|
||||
"http://{}:{}",
|
||||
conf.internal_api_bind, conf.internal_api_port
|
||||
);
|
||||
|
||||
// Generate mtls certificates to data directory so the admin can securely connect
|
||||
// to the server to perform administrative tasks.
|
||||
|
|
@ -335,7 +390,7 @@ async fn main() {
|
|||
Server::builder()
|
||||
.tls_config(tls)
|
||||
.expect("couldn't configure tls")
|
||||
.add_service(PrivateTowerServicesServer::new(rpc_api))
|
||||
.add_service(PrivateTowerServicesServer::new(internal_api))
|
||||
.serve_with_shutdown(rpc_api_addr, shutdown_signal_rpc_api)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -343,8 +398,8 @@ async fn main() {
|
|||
|
||||
let public_api_task = task::spawn(async move {
|
||||
Server::builder()
|
||||
.add_service(PublicTowerServicesServer::new(internal_rpc_api))
|
||||
.serve_with_shutdown(internal_rpc_api_addr, shutdown_signal_internal_rpc_api)
|
||||
.add_service(PublicTowerServicesServer::new(internal_api_cloned))
|
||||
.serve_with_shutdown(internal_api_addr, shutdown_signal_internal_api)
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
|
|
@ -352,7 +407,7 @@ async fn main() {
|
|||
let (http_service_ready, ready_signal_http) = triggered::trigger();
|
||||
let http_api_task = task::spawn(http::serve(
|
||||
http_api_addr,
|
||||
internal_rpc_api_uri,
|
||||
internal_api_addr,
|
||||
http_service_ready,
|
||||
shutdown_signal_http,
|
||||
));
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -8,6 +8,7 @@
|
|||
*/
|
||||
|
||||
use rand::Rng;
|
||||
use std::ops::Deref;
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use std::thread;
|
||||
|
||||
|
|
@ -17,22 +18,24 @@ use jsonrpc_http_server::{CloseHandle, Server, ServerBuilder};
|
|||
|
||||
use bitcoincore_rpc::{Auth, Client as BitcoindClient};
|
||||
|
||||
use bitcoin::blockdata::block::{Block, BlockHeader};
|
||||
use bitcoin::block::Block;
|
||||
use bitcoin::blockdata::constants::genesis_block;
|
||||
use bitcoin::blockdata::script::{Builder, Script};
|
||||
use bitcoin::blockdata::script::{Builder, ScriptBuf};
|
||||
use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxIn, TxOut};
|
||||
use bitcoin::hash_types::BlockHash;
|
||||
use bitcoin::hash_types::Txid;
|
||||
use bitcoin::hashes::Hash;
|
||||
use bitcoin::network::constants::Network;
|
||||
use bitcoin::util::hash::bitcoin_merkle_root;
|
||||
use bitcoin::util::uint::Uint256;
|
||||
use bitcoin::merkle_tree::calculate_root;
|
||||
use bitcoin::pow::Work;
|
||||
use bitcoin::Amount;
|
||||
use bitcoin::Network;
|
||||
use bitcoin::Witness;
|
||||
use lightning_block_sync::poll::{
|
||||
ChainPoller, Poll, Validate, ValidatedBlock, ValidatedBlockHeader,
|
||||
};
|
||||
use lightning_block_sync::{
|
||||
AsyncBlockSourceResult, BlockHeaderData, BlockSource, BlockSourceError, UnboundedCache,
|
||||
AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource, BlockSourceError,
|
||||
UnboundedCache,
|
||||
};
|
||||
|
||||
use teos_common::constants::IRREVOCABLY_RESOLVED;
|
||||
|
|
@ -153,8 +156,11 @@ impl Blockchain {
|
|||
fn at_height_unvalidated(&self, height: usize) -> BlockHeaderData {
|
||||
assert!(!self.blocks.is_empty());
|
||||
assert!(height < self.blocks.len());
|
||||
let height_bytes = height.to_be_bytes();
|
||||
let mut padded_bytes = [0u8; 32];
|
||||
padded_bytes[32 - height_bytes.len()..].copy_from_slice(&height_bytes);
|
||||
BlockHeaderData {
|
||||
chainwork: self.blocks[0].header.work() + Uint256::from_u64(height as u64).unwrap(),
|
||||
chainwork: self.blocks[0].header.work() + Work::from_be_bytes(padded_bytes),
|
||||
height: height as u32,
|
||||
header: self.blocks[height].header,
|
||||
}
|
||||
|
|
@ -184,7 +190,7 @@ impl Blockchain {
|
|||
}
|
||||
|
||||
pub fn generate(&mut self, txs: Option<Vec<Transaction>>) -> Block {
|
||||
let bits = BlockHeader::compact_target_from_u256(&Uint256::from_be_bytes([0xff; 32]));
|
||||
let bits = bitcoin::Target::from_be_bytes([0xff; 32]).to_compact_lossy();
|
||||
|
||||
let prev_block = self.blocks.last().unwrap();
|
||||
let prev_blockhash = prev_block.block_hash();
|
||||
|
|
@ -199,17 +205,17 @@ impl Blockchain {
|
|||
}
|
||||
None => vec![get_random_tx()],
|
||||
};
|
||||
let hashes = txdata.iter().map(|obj| obj.txid().as_hash());
|
||||
let mut header = BlockHeader {
|
||||
version: 0,
|
||||
let hashes = txdata.iter().map(|tx| tx.compute_txid().to_raw_hash());
|
||||
let mut header = bitcoin::block::Header {
|
||||
version: bitcoin::block::Version::from_consensus(0),
|
||||
prev_blockhash,
|
||||
merkle_root: bitcoin_merkle_root(hashes).unwrap().into(),
|
||||
merkle_root: calculate_root(hashes).unwrap().into(),
|
||||
time,
|
||||
bits,
|
||||
nonce: 0,
|
||||
};
|
||||
|
||||
while header.validate_pow(&header.target()).is_err() {
|
||||
while header.validate_pow(header.target()).is_err() {
|
||||
header.nonce += 1;
|
||||
}
|
||||
|
||||
|
|
@ -245,7 +251,7 @@ impl BlockSource for Blockchain {
|
|||
})
|
||||
}
|
||||
|
||||
fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<'a, Block> {
|
||||
fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<BlockData> {
|
||||
Box::pin(async move {
|
||||
for (height, block) in self.blocks.iter().enumerate() {
|
||||
if block.header.block_hash() == *header_hash {
|
||||
|
|
@ -254,8 +260,7 @@ impl BlockSource for Blockchain {
|
|||
return Err(BlockSourceError::persistent("block not found"));
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(block.clone());
|
||||
return Ok(BlockData::FullBlock(block.clone()));
|
||||
}
|
||||
}
|
||||
Err(BlockSourceError::transient("block not found"))
|
||||
|
|
@ -289,20 +294,20 @@ pub(crate) fn get_random_tx() -> Transaction {
|
|||
let prev_txid_bytes = get_random_bytes(32);
|
||||
|
||||
Transaction {
|
||||
version: 2,
|
||||
lock_time: 0,
|
||||
version: bitcoin::transaction::Version(2),
|
||||
lock_time: bitcoin::locktime::absolute::LockTime::from_height(0).unwrap(),
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(
|
||||
Txid::from_slice(&prev_txid_bytes).unwrap(),
|
||||
rng.gen_range(0..200),
|
||||
),
|
||||
script_sig: Script::new(),
|
||||
script_sig: ScriptBuf::new(),
|
||||
witness: Witness::new(),
|
||||
sequence: 0,
|
||||
sequence: bitcoin::Sequence(0),
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
script_pubkey: Builder::new().push_int(1).into_script(),
|
||||
value: rng.gen_range(0..21000000000),
|
||||
value: Amount::from_sat(rng.gen_range(0..21_000_000_000)),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
|
@ -323,7 +328,7 @@ pub(crate) fn generate_dummy_appointment_with_user(
|
|||
let mut app = generate_dummy_appointment(dispute_txid);
|
||||
app.user_id = user_id;
|
||||
|
||||
(UUID::new(app.locator(), user_id), app)
|
||||
(app.uuid(), app)
|
||||
}
|
||||
|
||||
pub(crate) fn get_random_breach() -> Breach {
|
||||
|
|
@ -341,17 +346,15 @@ pub(crate) fn get_random_tracker(
|
|||
TransactionTracker::new(breach, user_id, status)
|
||||
}
|
||||
|
||||
pub(crate) fn store_appointment_and_fks_to_db(
|
||||
dbm: &DBM,
|
||||
uuid: UUID,
|
||||
appointment: &ExtendedAppointment,
|
||||
) {
|
||||
pub(crate) fn store_appointment_and_its_user(dbm: &DBM, appointment: &ExtendedAppointment) {
|
||||
dbm.store_user(
|
||||
appointment.user_id,
|
||||
&UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY),
|
||||
)
|
||||
.unwrap();
|
||||
dbm.store_appointment(uuid, appointment).unwrap();
|
||||
// It's ok if the user is already stored.
|
||||
.ok();
|
||||
dbm.store_appointment(appointment.uuid(), appointment)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub(crate) async fn get_last_n_blocks(chain: &mut Blockchain, n: usize) -> Vec<ValidatedBlock> {
|
||||
|
|
@ -369,6 +372,17 @@ pub(crate) async fn get_last_n_blocks(chain: &mut Blockchain, n: usize) -> Vec<V
|
|||
last_n_blocks
|
||||
}
|
||||
|
||||
pub(crate) fn get_full_blocks(last_n_blocks: &[ValidatedBlock]) -> Vec<Block> {
|
||||
last_n_blocks.iter().map(get_full_block).collect()
|
||||
}
|
||||
|
||||
pub(crate) fn get_full_block(block: &ValidatedBlock) -> Block {
|
||||
match block.deref() {
|
||||
BlockData::FullBlock(b) => b.clone(),
|
||||
_ => panic!("Expected FullBlock"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum MockedServerQuery {
|
||||
Regular,
|
||||
InMempoool,
|
||||
|
|
@ -407,7 +421,7 @@ pub(crate) async fn create_responder(
|
|||
let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new()));
|
||||
let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, height);
|
||||
|
||||
Responder::new(&last_n_blocks, height, carrier, gatekeeper, dbm)
|
||||
Responder::new(last_n_blocks.as_slice(), height, carrier, gatekeeper, dbm)
|
||||
}
|
||||
|
||||
pub(crate) async fn create_watcher(
|
||||
|
|
@ -426,7 +440,7 @@ pub(crate) async fn create_watcher(
|
|||
Watcher::new(
|
||||
gatekeeper,
|
||||
responder,
|
||||
&last_n_blocks,
|
||||
last_n_blocks.as_slice(),
|
||||
chain.get_block_count(),
|
||||
tower_sk,
|
||||
tower_id,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* https://github.com/ElementsProject/lightning/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
use rcgen::{Certificate, KeyPair, RcgenError};
|
||||
use rcgen::{Certificate, Error as RcgenError, KeyPair};
|
||||
use std::convert::TryFrom;
|
||||
use std::path::Path;
|
||||
|
||||
|
|
@ -30,21 +30,20 @@ impl From<std::io::Error> for GenCertificateFailure {
|
|||
|
||||
/// Just a wrapper around a certificate and an associated keypair.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Identity {
|
||||
struct Identity {
|
||||
pub key: Vec<u8>,
|
||||
pub certificate: Vec<u8>,
|
||||
}
|
||||
|
||||
impl TryFrom<&Identity> for Certificate {
|
||||
impl TryFrom<&Identity> for (Certificate, KeyPair) {
|
||||
type Error = RcgenError;
|
||||
|
||||
fn try_from(id: &Identity) -> Result<Certificate, RcgenError> {
|
||||
let keystr = String::from_utf8_lossy(&id.key);
|
||||
let key = KeyPair::from_pem(&keystr)?;
|
||||
let certstr = String::from_utf8_lossy(&id.certificate);
|
||||
let params = rcgen::CertificateParams::from_ca_cert_pem(&certstr, key)?;
|
||||
let cert = Certificate::from_params(params)?;
|
||||
Ok(cert)
|
||||
fn try_from(id: &Identity) -> Result<(Certificate, KeyPair), RcgenError> {
|
||||
let key = KeyPair::from_pem(&String::from_utf8_lossy(&id.key))?;
|
||||
let params =
|
||||
rcgen::CertificateParams::from_ca_cert_pem(&String::from_utf8_lossy(&id.certificate))?;
|
||||
let cert = params.self_signed(&key)?;
|
||||
Ok((cert, key))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,30 +71,30 @@ fn generate_or_load_identity(
|
|||
// Did we have to generate a new key? In that case we also need to regenerate the certificate.
|
||||
if !key_path.exists() || !cert_path.exists() {
|
||||
log::debug!("Generating a new keypair in {key_path:?}, it didn't exist",);
|
||||
let keypair = KeyPair::generate(&rcgen::PKCS_ECDSA_P256_SHA256)?;
|
||||
let keypair = KeyPair::generate()?;
|
||||
std::fs::write(&key_path, keypair.serialize_pem())?;
|
||||
log::debug!("Generating a new certificate for key {key_path:?} at {cert_path:?}",);
|
||||
|
||||
// Configure the certificate we want.
|
||||
let subject_alt_names = vec!["cln".to_string(), "localhost".to_string()];
|
||||
let mut params = rcgen::CertificateParams::new(subject_alt_names);
|
||||
params.key_pair = Some(keypair);
|
||||
params.alg = &rcgen::PKCS_ECDSA_P256_SHA256;
|
||||
let subject_alt_names = vec!["teos".to_string(), "localhost".to_string()];
|
||||
let mut params = rcgen::CertificateParams::new(subject_alt_names)?;
|
||||
if parent.is_none() {
|
||||
params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
|
||||
} else {
|
||||
params.is_ca = rcgen::IsCa::SelfSignedOnly;
|
||||
params.is_ca = rcgen::IsCa::NoCa;
|
||||
}
|
||||
params
|
||||
.distinguished_name
|
||||
.push(rcgen::DnType::CommonName, name);
|
||||
|
||||
let cert = Certificate::from_params(params)?;
|
||||
std::fs::write(
|
||||
&cert_path,
|
||||
match parent {
|
||||
None => cert.serialize_pem()?,
|
||||
Some(ca) => cert.serialize_pem_with_signer(&Certificate::try_from(ca)?)?,
|
||||
None => params.self_signed(&keypair)?.pem(),
|
||||
Some(ca) => {
|
||||
let (ca_cert, ca_key) = <(Certificate, KeyPair)>::try_from(ca)?;
|
||||
params.signed_by(&keypair, &ca_cert, &ca_key)?.pem()
|
||||
}
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
use std::collections::{HashMap, VecDeque};
|
||||
use std::fmt;
|
||||
use std::hash::Hash;
|
||||
use std::ops::Deref;
|
||||
|
||||
use bitcoin::block::Header;
|
||||
use bitcoin::hash_types::BlockHash;
|
||||
use bitcoin::{BlockHeader, Transaction, Txid};
|
||||
use bitcoin::{Transaction, Txid};
|
||||
use lightning_block_sync::poll::ValidatedBlock;
|
||||
|
||||
use teos_common::appointment::Locator;
|
||||
|
||||
/// A trait implemented by types that can be used as key in a [TxIndex].
|
||||
pub trait Key: Hash {
|
||||
pub trait Key: Hash + Eq {
|
||||
fn from_txid(txid: Txid) -> Self;
|
||||
}
|
||||
|
||||
|
|
@ -79,8 +81,8 @@ impl Value for Transaction {
|
|||
/// Data structure used to index locators computed from parsed blocks.
|
||||
///
|
||||
/// Holds up to `size` blocks with their corresponding computed [Locator]s.
|
||||
#[derive(Debug)]
|
||||
pub struct TxIndex<K, V> {
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct TxIndex<K: Key, V: Value> {
|
||||
/// A [K]:[V] map.
|
||||
index: HashMap<K, V>,
|
||||
/// Vector of block hashes covered by the index.
|
||||
|
|
@ -95,7 +97,7 @@ pub struct TxIndex<K, V> {
|
|||
|
||||
impl<K, V> TxIndex<K, V>
|
||||
where
|
||||
K: Key + std::cmp::Eq + Copy,
|
||||
K: Key + Copy,
|
||||
V: Value + Clone,
|
||||
Self: Sized,
|
||||
{
|
||||
|
|
@ -110,29 +112,38 @@ where
|
|||
};
|
||||
|
||||
for block in last_n_blocks.iter().rev() {
|
||||
if let Some(prev_block_hash) = tx_index.blocks.back() {
|
||||
if block.header.prev_blockhash != *prev_block_hash {
|
||||
panic!("last_n_blocks contains unchained blocks");
|
||||
match block.deref() {
|
||||
lightning_block_sync::BlockData::HeaderOnly(_) => {
|
||||
panic!("Expected FullBlock")
|
||||
}
|
||||
};
|
||||
lightning_block_sync::BlockData::FullBlock(block) => {
|
||||
if let Some(prev_block_hash) = tx_index.blocks.back() {
|
||||
if block.header.prev_blockhash != *prev_block_hash {
|
||||
panic!("last_n_blocks contains unchained blocks");
|
||||
}
|
||||
};
|
||||
|
||||
let map = block
|
||||
.txdata
|
||||
.iter()
|
||||
.map(|tx| {
|
||||
(
|
||||
K::from_txid(tx.txid()),
|
||||
match V::get_type() {
|
||||
Type::Transaction => V::from_data(Data::Transaction(tx.clone())),
|
||||
Type::BlockHash => {
|
||||
V::from_data(Data::BlockHash(block.header.block_hash()))
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let map = block
|
||||
.txdata
|
||||
.iter()
|
||||
.map(|tx| {
|
||||
(
|
||||
K::from_txid(tx.compute_txid()),
|
||||
match V::get_type() {
|
||||
Type::Transaction => {
|
||||
V::from_data(Data::Transaction(tx.clone()))
|
||||
}
|
||||
Type::BlockHash => {
|
||||
V::from_data(Data::BlockHash(block.header.block_hash()))
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
tx_index.update(block.header, &map);
|
||||
tx_index.update(block.header, &map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tx_index
|
||||
|
|
@ -143,11 +154,6 @@ where
|
|||
self.index.get(k)
|
||||
}
|
||||
|
||||
/// Checks whether the index contains a certain key.
|
||||
pub fn contains_key(&self, k: &K) -> bool {
|
||||
self.index.contains_key(k)
|
||||
}
|
||||
|
||||
/// Checks if the index if full.
|
||||
pub fn is_full(&self) -> bool {
|
||||
self.blocks.len() > self.size
|
||||
|
|
@ -160,7 +166,7 @@ where
|
|||
}
|
||||
|
||||
/// Updates the index by adding data from a new block. Removes the oldest block if the index is full afterwards.
|
||||
pub fn update(&mut self, block_header: BlockHeader, data: &HashMap<K, V>) {
|
||||
pub fn update(&mut self, block_header: Header, data: &HashMap<K, V>) {
|
||||
self.blocks.push_back(block_header.block_hash());
|
||||
|
||||
let ks = data
|
||||
|
|
@ -175,7 +181,7 @@ where
|
|||
|
||||
if self.is_full() {
|
||||
// Avoid logging during bootstrap
|
||||
log::info!("New block added to index: {}", block_header.block_hash());
|
||||
log::debug!("New block added to index: {}", block_header.block_hash());
|
||||
self.tip += 1;
|
||||
self.remove_oldest_block();
|
||||
}
|
||||
|
|
@ -204,11 +210,11 @@ where
|
|||
let ks = self.tx_in_block.remove(&h).unwrap();
|
||||
self.index.retain(|k, _| !ks.contains(k));
|
||||
|
||||
log::info!("Oldest block removed from index: {h}");
|
||||
log::debug!("Oldest block removed from index: {h}");
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: std::fmt::Debug, V: std::fmt::Debug> fmt::Display for TxIndex<K, V> {
|
||||
impl<K: std::fmt::Debug + Key, V: std::fmt::Debug + Value> fmt::Display for TxIndex<K, V> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
|
|
@ -223,8 +229,9 @@ mod tests {
|
|||
use super::*;
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::test_utils::{get_last_n_blocks, Blockchain};
|
||||
use crate::test_utils::{get_full_block, get_full_blocks, get_last_n_blocks, Blockchain};
|
||||
|
||||
use bitcoin::hashes::serde_macros::serde_details::SerdeHash;
|
||||
use bitcoin::Block;
|
||||
|
||||
impl<K, V> TxIndex<K, V>
|
||||
|
|
@ -240,6 +247,10 @@ mod tests {
|
|||
pub fn blocks(&self) -> &VecDeque<BlockHash> {
|
||||
&self.blocks
|
||||
}
|
||||
|
||||
pub fn contains_key(&self, k: &K) -> bool {
|
||||
self.index.contains_key(k)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -247,10 +258,7 @@ mod tests {
|
|||
let height = 10;
|
||||
let mut chain = Blockchain::default().with_height(height as usize);
|
||||
let last_six_blocks = get_last_n_blocks(&mut chain, 6).await;
|
||||
let blocks: Vec<Block> = last_six_blocks
|
||||
.iter()
|
||||
.map(|block| block.deref().clone())
|
||||
.collect();
|
||||
let blocks: Vec<Block> = get_full_blocks(&last_six_blocks);
|
||||
|
||||
let cache: TxIndex<Locator, Transaction> = TxIndex::new(&last_six_blocks, height);
|
||||
assert_eq!(blocks.len(), cache.size);
|
||||
|
|
@ -259,7 +267,7 @@ mod tests {
|
|||
|
||||
let mut locators = Vec::new();
|
||||
for tx in block.txdata.iter() {
|
||||
let locator = Locator::new(tx.txid());
|
||||
let locator = Locator::new(tx.compute_txid());
|
||||
assert!(cache.contains_key(&locator));
|
||||
locators.push(locator);
|
||||
}
|
||||
|
|
@ -276,19 +284,22 @@ mod tests {
|
|||
let last_n_blocks = get_last_n_blocks(&mut chain, cache_size).await;
|
||||
|
||||
// last_n_blocks is ordered from latest to earliest
|
||||
let first_block = last_n_blocks.get(cache_size - 1).unwrap();
|
||||
let last_block = last_n_blocks.get(0).unwrap();
|
||||
let mid = last_n_blocks.get(cache_size / 2).unwrap();
|
||||
let first_block = get_full_block(last_n_blocks.get(cache_size - 1).unwrap());
|
||||
let last_block = get_full_block(last_n_blocks.first().unwrap());
|
||||
let mid_block = get_full_block(last_n_blocks.get(cache_size / 2).unwrap());
|
||||
|
||||
let cache: TxIndex<Locator, Transaction> = TxIndex::new(&last_n_blocks, height as u32);
|
||||
|
||||
assert_eq!(
|
||||
cache.get_height(&first_block.block_hash()).unwrap(),
|
||||
cache.get_height(&first_block.header.block_hash()).unwrap(),
|
||||
height - cache_size + 1
|
||||
);
|
||||
assert_eq!(cache.get_height(&last_block.block_hash()).unwrap(), height);
|
||||
assert_eq!(
|
||||
cache.get_height(&mid.block_hash()).unwrap(),
|
||||
cache.get_height(&last_block.header.block_hash()).unwrap(),
|
||||
height
|
||||
);
|
||||
assert_eq!(
|
||||
cache.get_height(&mid_block.header.block_hash()).unwrap(),
|
||||
height - cache_size / 2
|
||||
);
|
||||
}
|
||||
|
|
@ -303,8 +314,8 @@ mod tests {
|
|||
height as u32,
|
||||
);
|
||||
|
||||
let fake_hash = BlockHash::default();
|
||||
assert!(matches!(cache.get_height(&fake_hash), None));
|
||||
let fake_hash = &BlockHash::from_slice_delegated(&[0; 32]).unwrap();
|
||||
assert!(cache.get_height(fake_hash).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -316,36 +327,42 @@ mod tests {
|
|||
// Store the last block to use it for an update and the first to check eviction
|
||||
// Notice that the list of blocks is ordered from last to first.
|
||||
let last_block = last_n_blocks.remove(0);
|
||||
let first_block = last_n_blocks.last().unwrap().deref().clone();
|
||||
let first_block = last_n_blocks.last().unwrap();
|
||||
|
||||
// Init the cache with the 6 block before the last
|
||||
let mut cache = TxIndex::new(&last_n_blocks, height);
|
||||
|
||||
// Update the cache with the last block
|
||||
let locator_tx_map = last_block
|
||||
let full_block = get_full_block(&last_block);
|
||||
let locator_tx_map = full_block
|
||||
.txdata
|
||||
.iter()
|
||||
.map(|tx| (Locator::new(tx.txid()), tx.clone()))
|
||||
.map(|tx| (Locator::new(tx.compute_txid()), tx.clone()))
|
||||
.collect();
|
||||
|
||||
cache.update(last_block.deref().header, &locator_tx_map);
|
||||
let header = full_block.header;
|
||||
cache.update(header, &locator_tx_map);
|
||||
|
||||
// Check that the new data is in the cache
|
||||
assert!(cache.blocks().contains(&last_block.block_hash()));
|
||||
assert!(cache.blocks().contains(&header.block_hash()));
|
||||
|
||||
for (locator, _) in locator_tx_map.iter() {
|
||||
assert!(cache.contains_key(locator));
|
||||
}
|
||||
|
||||
let block_hash = full_block.header.block_hash();
|
||||
assert_eq!(
|
||||
cache.tx_in_block[&last_block.block_hash()],
|
||||
cache.tx_in_block[&block_hash],
|
||||
locator_tx_map.keys().cloned().collect::<Vec<Locator>>()
|
||||
);
|
||||
|
||||
// Check that the data from the first block has been evicted
|
||||
assert!(!cache.blocks().contains(&first_block.block_hash()));
|
||||
for tx in first_block.txdata.iter() {
|
||||
assert!(!cache.contains_key(&Locator::new(tx.txid())));
|
||||
}
|
||||
assert!(!cache.tx_in_block.contains_key(&first_block.block_hash()));
|
||||
let first_full_block = get_full_block(first_block);
|
||||
let tx = first_full_block.txdata[0].clone();
|
||||
assert!(!cache.contains_key(&Locator::new(tx.compute_txid())));
|
||||
|
||||
let block_hash = first_full_block.header.block_hash();
|
||||
assert!(!cache.tx_in_block.contains_key(&block_hash));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -379,7 +396,7 @@ mod tests {
|
|||
// Check that the block data is not in the cache anymore
|
||||
assert_eq!(cache.blocks().len(), cache.size - i - 1);
|
||||
assert!(!cache.blocks().contains(&header.block_hash()));
|
||||
assert!(cache.tx_in_block.get(&header.block_hash()).is_none());
|
||||
assert!(!cache.tx_in_block.contains_key(&header.block_hash()));
|
||||
for locator in locators.iter() {
|
||||
assert!(!cache.contains_key(locator));
|
||||
}
|
||||
|
|
|
|||
1270
teos/src/watcher.rs
1270
teos/src/watcher.rs
File diff suppressed because it is too large
Load diff
|
|
@ -3,7 +3,7 @@ name = "watchtower-plugin"
|
|||
version = "0.2.0"
|
||||
authors = ["Sergi Delgado Segura <sergi.delgado.s@gmail.com>"]
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "watchtower-client"
|
||||
|
|
@ -21,16 +21,16 @@ log = "0.4.16"
|
|||
rusqlite = { version = "0.26.0", features = [ "bundled", "limits" ] }
|
||||
serde = "1.0.130"
|
||||
serde_json = { version = "1.0", features = [ "preserve_order" ] }
|
||||
tonic = { version = "^0.5", features = [ "tls", "transport" ] }
|
||||
tonic = { version = "0.11", features = [ "tls", "transport" ] }
|
||||
tokio = { version = "1.5", features = [ "rt-multi-thread", "fs" ] }
|
||||
|
||||
# Bitcoin and Lightning
|
||||
bitcoin = "0.28.0"
|
||||
cln-plugin = "0.1.2"
|
||||
bitcoin = "0.32.0"
|
||||
cln-plugin = "0.3.0"
|
||||
|
||||
# Local
|
||||
teos-common = { path = "../teos-common" }
|
||||
|
||||
[dev-dependencies]
|
||||
httpmock = "0.6"
|
||||
tempdir = "0.3.7"
|
||||
mockito = "0.32.4"
|
||||
tempdir = "0.3.7"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ The plugin has the following methods:
|
|||
- `gettowerinfo <tower_id>`: gets all the locally stored data about a given tower.
|
||||
- `retrytower <tower_id>`: tries to send pending appointment to a (previously) unreachable tower.
|
||||
- `abandontower <tower_id>`: deletes all data associated with a given tower.
|
||||
- `pingtower <tower_id>`: Polls the tower to check if it is online.
|
||||
- `listtowers`: lists all registered towers.
|
||||
- `getappointment <tower_id> <locator>`: queries a given tower about an appointment.
|
||||
- `getsubscriptioninfo <tower_id>`: gets the subscription information by querying the tower.
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ pub const RPC_RETRY_TOWER_DESC: &str =
|
|||
"Retries to send pending appointment to an unreachable tower";
|
||||
pub const RPC_ABANDON_TOWER: &str = "abandontower";
|
||||
pub const RPC_ABANDON_TOWER_DESC: &str = "Forgets about a tower and wipes all local data";
|
||||
pub const RPC_PING: &str = "pingtower";
|
||||
pub const RPC_PING_DESC: &str = "Polls the tower to check if it is online";
|
||||
|
||||
/// Collections of hook names
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,8 @@ impl RegisterParams {
|
|||
impl TryFrom<serde_json::Value> 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<Self, Self::Error> {
|
||||
match value {
|
||||
serde_json::Value::String(s) => {
|
||||
|
|
@ -193,6 +195,8 @@ pub struct GetAppointmentParams {
|
|||
impl TryFrom<serde_json::Value> 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<Self, Self::Error> {
|
||||
match value {
|
||||
serde_json::Value::Array(a) => {
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ impl DBM {
|
|||
///
|
||||
/// Loads the key with higher id from the database. Old keys are not overwritten just in case a recovery is needed,
|
||||
/// but they are not accessible from the API either.
|
||||
pub fn load_client_key(&self) -> Result<SecretKey, Error> {
|
||||
pub fn load_client_key(&self) -> Option<SecretKey> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare(
|
||||
|
|
@ -137,7 +137,7 @@ impl DBM {
|
|||
let sk: String = row.get(0).unwrap();
|
||||
Ok(SecretKey::from_str(&sk).unwrap())
|
||||
})
|
||||
.map_err(|_| Error::NotFound)
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Stores a tower record into the database alongside the corresponding registration receipt.
|
||||
|
|
@ -171,7 +171,7 @@ impl DBM {
|
|||
/// Tower records are composed from the tower information and the appointment data. The latter is split in:
|
||||
/// accepted appointments (represented by appointment receipts), pending appointments and invalid appointments.
|
||||
/// In the case that the tower has misbehaved, then a misbehaving proof is also attached to the record.
|
||||
pub fn load_tower_record(&self, tower_id: TowerId) -> Result<TowerInfo, Error> {
|
||||
pub fn load_tower_record(&self, tower_id: TowerId) -> Option<TowerInfo> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("SELECT t.net_addr, t.available_slots, r.subscription_start, r.subscription_expiry
|
||||
|
|
@ -197,16 +197,16 @@ impl DBM {
|
|||
self.load_appointments(tower_id, AppointmentStatus::Invalid),
|
||||
))
|
||||
})
|
||||
.map_err(|_| Error::NotFound)?;
|
||||
.ok()?;
|
||||
|
||||
if let Ok(proof) = self.load_misbehaving_proof(tower_id) {
|
||||
if let Some(proof) = self.load_misbehaving_proof(tower_id) {
|
||||
tower.status = TowerStatus::Misbehaving;
|
||||
tower.set_misbehaving_proof(proof);
|
||||
} else if !tower.pending_appointments.is_empty() {
|
||||
tower.status = TowerStatus::TemporaryUnreachable;
|
||||
}
|
||||
|
||||
Ok(tower)
|
||||
Some(tower)
|
||||
}
|
||||
|
||||
/// Loads the latest registration receipt for a given tower.
|
||||
|
|
@ -216,7 +216,7 @@ impl DBM {
|
|||
&self,
|
||||
tower_id: TowerId,
|
||||
user_id: UserId,
|
||||
) -> Result<RegistrationReceipt, Error> {
|
||||
) -> Option<RegistrationReceipt> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare(
|
||||
|
|
@ -228,20 +228,17 @@ impl DBM {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let receipt = stmt
|
||||
.query_row([tower_id.to_vec()], |row| {
|
||||
let slots: u32 = row.get(0).unwrap();
|
||||
let start: u32 = row.get(1).unwrap();
|
||||
let expiry: u32 = row.get(2).unwrap();
|
||||
let signature: String = row.get(3).unwrap();
|
||||
stmt.query_row([tower_id.to_vec()], |row| {
|
||||
let slots: u32 = row.get(0).unwrap();
|
||||
let start: u32 = row.get(1).unwrap();
|
||||
let expiry: u32 = row.get(2).unwrap();
|
||||
let signature: String = row.get(3).unwrap();
|
||||
|
||||
Ok(RegistrationReceipt::with_signature(
|
||||
user_id, slots, start, expiry, signature,
|
||||
))
|
||||
})
|
||||
.map_err(|_| Error::NotFound)?;
|
||||
|
||||
Ok(receipt)
|
||||
Ok(RegistrationReceipt::with_signature(
|
||||
user_id, slots, start, expiry, signature,
|
||||
))
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Removes a tower record from the database.
|
||||
|
|
@ -333,7 +330,7 @@ impl DBM {
|
|||
&self,
|
||||
tower_id: TowerId,
|
||||
locator: Locator,
|
||||
) -> Result<AppointmentReceipt, Error> {
|
||||
) -> Option<AppointmentReceipt> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("SELECT start_block, user_signature, tower_signature FROM appointment_receipts WHERE tower_id = ?1 and locator = ?2")
|
||||
|
|
@ -350,7 +347,7 @@ impl DBM {
|
|||
tower_sig,
|
||||
))
|
||||
})
|
||||
.map_err(|_| Error::NotFound)
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Loads the appointment receipts associated to a given tower.
|
||||
|
|
@ -406,7 +403,7 @@ impl DBM {
|
|||
}
|
||||
|
||||
/// Loads an appointment from the database.
|
||||
pub fn load_appointment(&self, locator: Locator) -> Result<Appointment, Error> {
|
||||
pub fn load_appointment(&self, locator: Locator) -> Option<Appointment> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare("SELECT encrypted_blob, to_self_delay FROM appointments WHERE locator = ?")
|
||||
|
|
@ -418,7 +415,7 @@ impl DBM {
|
|||
|
||||
Ok(Appointment::new(locator, encrypted_blob, to_self_delay))
|
||||
})
|
||||
.map_err(|_| Error::NotFound)
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Stores an appointment into the database.
|
||||
|
|
@ -598,7 +595,7 @@ impl DBM {
|
|||
}
|
||||
|
||||
/// Loads the misbehaving proof for a given tower from the database (if found).
|
||||
fn load_misbehaving_proof(&self, tower_id: TowerId) -> Result<MisbehaviorProof, Error> {
|
||||
fn load_misbehaving_proof(&self, tower_id: TowerId) -> Option<MisbehaviorProof> {
|
||||
let mut misbehaving_stmt = self
|
||||
.connection
|
||||
.prepare("SELECT locator, recovered_id FROM misbehaving_proofs WHERE tower_id = ?")
|
||||
|
|
@ -633,7 +630,7 @@ impl DBM {
|
|||
.unwrap();
|
||||
MisbehaviorProof::new(locator, receipt, recovered_id)
|
||||
})
|
||||
.map_err(|_| Error::NotFound)
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Checks whether a misbehaving proof exists for a given tower.
|
||||
|
|
@ -792,10 +789,7 @@ mod tests {
|
|||
|
||||
// If the tower does not exists, `load_tower` will fail.
|
||||
let tower_id = get_random_user_id();
|
||||
assert!(matches!(
|
||||
dbm.load_tower_record(tower_id),
|
||||
Err(Error::NotFound)
|
||||
));
|
||||
assert!(dbm.load_tower_record(tower_id).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -917,10 +911,9 @@ mod tests {
|
|||
|
||||
// If there is no appointment receipt for the given (locator, tower_id) pair, Error::NotFound is returned
|
||||
// Try first with both being unknown
|
||||
assert!(matches!(
|
||||
dbm.load_appointment_receipt(tower_id, appointment.locator),
|
||||
Err(Error::NotFound)
|
||||
));
|
||||
assert!(dbm
|
||||
.load_appointment_receipt(tower_id, appointment.locator)
|
||||
.is_none());
|
||||
|
||||
// Add the tower but not the appointment and try again
|
||||
let net_addr = "talaia.watch";
|
||||
|
|
@ -928,10 +921,9 @@ mod tests {
|
|||
dbm.store_tower_record(tower_id, net_addr, &receipt)
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
dbm.load_appointment_receipt(tower_id, appointment.locator),
|
||||
Err(Error::NotFound)
|
||||
));
|
||||
assert!(dbm
|
||||
.load_appointment_receipt(tower_id, appointment.locator)
|
||||
.is_none());
|
||||
|
||||
// Add both
|
||||
let tower_summary = TowerSummary::new(
|
||||
|
|
@ -1045,7 +1037,7 @@ mod tests {
|
|||
|
||||
let locator = generate_random_appointment(None).locator;
|
||||
let loaded_appointment = dbm.load_appointment(locator);
|
||||
assert!(matches!(loaded_appointment, Err(Error::NotFound)));
|
||||
assert!(loaded_appointment.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1284,10 +1276,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_store_load_non_existing_misbehaving_proof() {
|
||||
let dbm = DBM::in_memory().unwrap();
|
||||
assert!(matches!(
|
||||
dbm.load_misbehaving_proof(get_random_user_id()),
|
||||
Err(Error::NotFound)
|
||||
));
|
||||
assert!(dbm.load_misbehaving_proof(get_random_user_id()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1340,7 +1329,7 @@ mod tests {
|
|||
fn test_store_load_client_key() {
|
||||
let dbm = DBM::in_memory().unwrap();
|
||||
|
||||
assert!(matches!(dbm.load_client_key(), Err(Error::NotFound)));
|
||||
assert!(dbm.load_client_key().is_none());
|
||||
for _ in 0..7 {
|
||||
let sk = get_random_keypair().0;
|
||||
dbm.store_client_key(&sk).unwrap();
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ use serde_json::json;
|
|||
use tokio::io::{stdin, stdout};
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
use cln_plugin::options::{ConfigOption, Value};
|
||||
use cln_plugin::options::config_type::DefaultInteger;
|
||||
use cln_plugin::options::ConfigOption;
|
||||
use cln_plugin::{anyhow, Builder, Error, Plugin};
|
||||
|
||||
use teos_common::appointment::{Appointment, Locator};
|
||||
|
|
@ -20,13 +21,39 @@ use teos_common::{cryptography, errors};
|
|||
|
||||
use watchtower_plugin::convert::{CommitmentRevocation, GetAppointmentParams, RegisterParams};
|
||||
use watchtower_plugin::net::http::{
|
||||
self, post_request, process_post_response, AddAppointmentError, ApiResponse, RequestError,
|
||||
self, get_request, post_request, process_post_response, AddAppointmentError, ApiResponse,
|
||||
RequestError,
|
||||
};
|
||||
use watchtower_plugin::net::ProxyInfo;
|
||||
use watchtower_plugin::retrier::RetryManager;
|
||||
use watchtower_plugin::wt_client::{RevocationData, WTClient};
|
||||
use watchtower_plugin::{constants, TowerStatus};
|
||||
|
||||
const DEV_WT_MAX_RETRY_INTERVAL_CONFIG: ConfigOption<DefaultInteger> =
|
||||
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<DefaultInteger> = 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<DefaultInteger> = 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<DefaultInteger> = ConfigOption::new_i64_with_default(
|
||||
constants::WT_PORT,
|
||||
constants::DEFAULT_WT_PORT,
|
||||
constants::WT_PORT_DESC,
|
||||
);
|
||||
|
||||
fn to_cln_error(e: RequestError) -> Error {
|
||||
let e = match e {
|
||||
RequestError::ConnectionError(e) => anyhow!(e),
|
||||
|
|
@ -75,12 +102,12 @@ async fn register(
|
|||
// which is not available in the current version of `cln-plugin` (but already on master). Add it for the next release.
|
||||
|
||||
let port = params.port.unwrap_or(
|
||||
u16::try_from(plugin.option(constants::WT_PORT).unwrap().as_i64().unwrap())
|
||||
u16::try_from(plugin.option(&WT_PORT_CONFG).unwrap())
|
||||
.map_err(|_| anyhow!("{} out of range", constants::WT_PORT))?,
|
||||
);
|
||||
|
||||
let tower_net_addr = {
|
||||
if !host.starts_with("http://") {
|
||||
if !host.starts_with("http://") && !host.starts_with("https://") {
|
||||
host = format!("http://{host}")
|
||||
}
|
||||
NetAddr::new(format!("{host}:{port}"))
|
||||
|
|
@ -136,11 +163,13 @@ async fn get_registration_receipt(
|
|||
let tower_id = TowerId::try_from(v).map_err(|x| anyhow!(x))?;
|
||||
let state = plugin.state().lock().unwrap();
|
||||
|
||||
let response = state.get_registration_receipt(tower_id).map_err(|_| {
|
||||
anyhow!("Cannot find {tower_id} within the known towers. Have you registered?")
|
||||
})?;
|
||||
|
||||
Ok(json!(response))
|
||||
if let Some(response) = state.get_registration_receipt(tower_id) {
|
||||
Ok(json!(response))
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Cannot find {tower_id} within the known towers. Have you registered?"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the subscription information directly form the tower.
|
||||
|
|
@ -159,7 +188,7 @@ async fn get_subscription_info(
|
|||
}
|
||||
}?;
|
||||
|
||||
let signature = cryptography::sign("get subscription info".as_bytes(), &user_sk).unwrap();
|
||||
let signature = cryptography::sign("get subscription info".as_bytes(), &user_sk);
|
||||
|
||||
let response: common_msgs::GetSubscriptionInfoResponse = process_post_response(
|
||||
post_request(
|
||||
|
|
@ -204,8 +233,7 @@ async fn get_appointment(
|
|||
let signature = cryptography::sign(
|
||||
format!("get appointment {}", params.locator).as_bytes(),
|
||||
&user_sk,
|
||||
)
|
||||
.unwrap();
|
||||
);
|
||||
|
||||
let response: ApiResponse<common_msgs::GetAppointmentResponse> = process_post_response(
|
||||
post_request(
|
||||
|
|
@ -244,24 +272,20 @@ async fn get_appointment_receipt(
|
|||
let params = GetAppointmentParams::try_from(v).map_err(|x| anyhow!(x))?;
|
||||
let state = plugin.state().lock().unwrap();
|
||||
|
||||
let response = state
|
||||
.get_appointment_receipt(params.tower_id, params.locator)
|
||||
.map_err(|_| {
|
||||
if state.towers.contains_key(¶ms.tower_id) {
|
||||
anyhow!(
|
||||
"Cannot find {} within {}. Did you send that appointment?",
|
||||
params.locator,
|
||||
params.tower_id
|
||||
)
|
||||
} else {
|
||||
anyhow!(
|
||||
"Cannot find {} within the known towers. Have you registered?",
|
||||
params.tower_id
|
||||
)
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(json!(response))
|
||||
if let Some(r) = state.get_appointment_receipt(params.tower_id, params.locator) {
|
||||
Ok(json!(r))
|
||||
} else if state.towers.contains_key(¶ms.tower_id) {
|
||||
Err(anyhow!(
|
||||
"Cannot find {} within {}. Did you send that appointment?",
|
||||
params.locator,
|
||||
params.tower_id
|
||||
))
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Cannot find {} within the known towers. Have you registered?",
|
||||
params.tower_id
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists all the registered towers.
|
||||
|
|
@ -283,15 +307,50 @@ async fn get_tower_info(
|
|||
) -> Result<serde_json::Value, Error> {
|
||||
let state = plugin.state().lock().unwrap();
|
||||
let tower_id = TowerId::try_from(v).map_err(|e| anyhow!(e))?;
|
||||
let tower_info = state.load_tower_info(tower_id).map_err(|_| {
|
||||
anyhow!("Cannot find {tower_id} within the known towers. Have you registered?")
|
||||
})?;
|
||||
|
||||
// Notice we need to check the status in memory since we cannot distinguish between unreachable and temporary unreachable
|
||||
// by just checking the data in the database.
|
||||
Ok(json!(
|
||||
tower_info.with_status(state.get_tower_status(&tower_id).unwrap())
|
||||
))
|
||||
if let Some(tower_info) = state.load_tower_info(tower_id) {
|
||||
// Notice we need to check the status in memory since we cannot distinguish between unreachable and temporary unreachable
|
||||
// by just checking the data in the database.
|
||||
Ok(json!(
|
||||
tower_info.with_status(state.get_tower_status(&tower_id).unwrap())
|
||||
))
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Cannot find {tower_id} within the known towers. Have you registered?",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn ping(
|
||||
plugin: Plugin<Arc<Mutex<WTClient>>>,
|
||||
v: serde_json::Value,
|
||||
) -> Result<serde_json::Value, Error> {
|
||||
let (tower_net_addr, proxy) = {
|
||||
// Check if the tower_id is known to the plugin
|
||||
let tower_id = TowerId::try_from(v).map_err(|e| anyhow!(e))?;
|
||||
let state = plugin.state().lock().unwrap();
|
||||
(
|
||||
state
|
||||
.towers
|
||||
.get(&tower_id)
|
||||
.ok_or(anyhow!("Unknown tower_id"))?
|
||||
.net_addr
|
||||
.clone(),
|
||||
state.proxy.clone(),
|
||||
)
|
||||
};
|
||||
let response = get_request(&tower_net_addr, Endpoint::Ping, &proxy)
|
||||
.await
|
||||
.map_err(to_cln_error)?;
|
||||
|
||||
if response.status().is_success() {
|
||||
Ok(json!("Tower is reachable"))
|
||||
} else {
|
||||
Err(anyhow!(format!(
|
||||
"Tower cannot be reached (Error: {})",
|
||||
response.status()
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Triggers a manual retry of a tower, tries to send all pending appointments to it.
|
||||
|
|
@ -351,7 +410,7 @@ async fn abandon_tower(
|
|||
) -> Result<serde_json::Value, Error> {
|
||||
let tower_id = TowerId::try_from(v).map_err(|e| anyhow!(e))?;
|
||||
let mut state = plugin.state().lock().unwrap();
|
||||
if state.towers.get(&tower_id).is_some() {
|
||||
if state.towers.contains_key(&tower_id) {
|
||||
state.remove_tower(tower_id).unwrap();
|
||||
Ok(json!(format!("{tower_id} successfully abandoned")))
|
||||
} else {
|
||||
|
|
@ -388,8 +447,7 @@ async fn on_commitment_revocation(
|
|||
let signature = cryptography::sign(
|
||||
&appointment.to_vec(),
|
||||
&plugin.state().lock().unwrap().user_sk,
|
||||
)
|
||||
.unwrap();
|
||||
);
|
||||
|
||||
// Looks like we cannot iterate through towers given a locked state is not Send (due to the async call),
|
||||
// so we need to clone the bare minimum.
|
||||
|
|
@ -500,26 +558,10 @@ async fn main() -> Result<(), Error> {
|
|||
};
|
||||
|
||||
let builder = Builder::new(stdin(), stdout())
|
||||
.option(ConfigOption::new(
|
||||
constants::WT_PORT,
|
||||
Value::Integer(constants::DEFAULT_WT_PORT),
|
||||
constants::WT_PORT_DESC,
|
||||
))
|
||||
.option(ConfigOption::new(
|
||||
constants::WT_MAX_RETRY_TIME,
|
||||
Value::Integer(constants::DEFAULT_WT_MAX_RETRY_TIME),
|
||||
constants::WT_MAX_RETRY_TIME_DESC,
|
||||
))
|
||||
.option(ConfigOption::new(
|
||||
constants::WT_AUTO_RETRY_DELAY,
|
||||
Value::Integer(constants::DEFAULT_WT_AUTO_RETRY_DELAY),
|
||||
constants::WT_AUTO_RETRY_DELAY_DESC,
|
||||
))
|
||||
.option(ConfigOption::new(
|
||||
constants::DEV_WT_MAX_RETRY_INTERVAL,
|
||||
Value::Integer(constants::DEFAULT_DEV_WT_MAX_RETRY_INTERVAL),
|
||||
constants::DEV_WT_MAX_RETRY_INTERVAL_DESC,
|
||||
))
|
||||
.option(WT_PORT_CONFG)
|
||||
.option(WT_MAX_RETRY_TIME_CONFIG)
|
||||
.option(WT_AUTO_RETRY_DELAY_CONFIG)
|
||||
.option(DEV_WT_MAX_RETRY_INTERVAL_CONFIG)
|
||||
.rpcmethod(
|
||||
constants::RPC_REGISTER_TOWER,
|
||||
constants::RPC_REGISTER_TOWER_DESC,
|
||||
|
|
@ -555,6 +597,7 @@ async fn main() -> Result<(), Error> {
|
|||
constants::RPC_GET_TOWER_INFO_DESC,
|
||||
get_tower_info,
|
||||
)
|
||||
.rpcmethod(constants::RPC_PING, constants::RPC_PING_DESC, ping)
|
||||
.rpcmethod(
|
||||
constants::RPC_RETRY_TOWER,
|
||||
constants::RPC_RETRY_TOWER_DESC,
|
||||
|
|
@ -594,40 +637,21 @@ async fn main() -> Result<(), Error> {
|
|||
.await,
|
||||
));
|
||||
|
||||
let max_elapsed_time = u16::try_from(
|
||||
midstate
|
||||
.option(constants::WT_MAX_RETRY_TIME)
|
||||
.unwrap()
|
||||
.as_i64()
|
||||
.unwrap(),
|
||||
)
|
||||
.map_err(|e| {
|
||||
log::error!("{} out of range", constants::WT_MAX_RETRY_TIME);
|
||||
e
|
||||
})?;
|
||||
let max_elapsed_time = u16::try_from(midstate.option(&WT_MAX_RETRY_TIME_CONFIG).unwrap())
|
||||
.inspect_err(|_| {
|
||||
log::error!("{} out of range", constants::WT_MAX_RETRY_TIME);
|
||||
})?;
|
||||
|
||||
let auto_retry_delay = u32::try_from(
|
||||
midstate
|
||||
.option(constants::WT_AUTO_RETRY_DELAY)
|
||||
.unwrap()
|
||||
.as_i64()
|
||||
.unwrap(),
|
||||
)
|
||||
.map_err(|e| {
|
||||
log::error!("{} out of range", constants::WT_AUTO_RETRY_DELAY);
|
||||
e
|
||||
})?;
|
||||
let auto_retry_delay = u32::try_from(midstate.option(&WT_AUTO_RETRY_DELAY_CONFIG).unwrap())
|
||||
.inspect_err(|_| {
|
||||
log::error!("{} out of range", constants::WT_AUTO_RETRY_DELAY);
|
||||
})?;
|
||||
|
||||
let max_interval_time = u16::try_from(
|
||||
midstate
|
||||
.option(constants::DEV_WT_MAX_RETRY_INTERVAL)
|
||||
.unwrap()
|
||||
.as_i64()
|
||||
.unwrap(),
|
||||
midstate.option(&DEV_WT_MAX_RETRY_INTERVAL_CONFIG).unwrap(),
|
||||
)
|
||||
.map_err(|e| {
|
||||
.inspect_err(|_| {
|
||||
log::error!("{} out of range", constants::DEV_WT_MAX_RETRY_INTERVAL);
|
||||
e
|
||||
})?;
|
||||
|
||||
let plugin = midstate.start(wt_client.clone()).await?;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use reqwest::Response;
|
||||
use reqwest::{Method, Response};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
|
||||
use teos_common::appointment::Appointment;
|
||||
|
|
@ -154,12 +154,13 @@ pub async fn send_appointment(
|
|||
}
|
||||
}
|
||||
|
||||
/// Generic function to post different types of requests to the tower.
|
||||
pub async fn post_request<S: Serialize>(
|
||||
/// A generic function to send a request to a tower.
|
||||
async fn request<S: Serialize>(
|
||||
tower_net_addr: &NetAddr,
|
||||
endpoint: Endpoint,
|
||||
data: S,
|
||||
proxy: &Option<ProxyInfo>,
|
||||
method: Method,
|
||||
data: Option<S>,
|
||||
) -> Result<Response, RequestError> {
|
||||
let client = if let Some(proxy) = proxy {
|
||||
if proxy.always_use || tower_net_addr.is_onion() {
|
||||
|
|
@ -183,23 +184,42 @@ pub async fn post_request<S: Serialize>(
|
|||
reqwest::Client::new()
|
||||
};
|
||||
|
||||
client
|
||||
.post(format!("{}{}", tower_net_addr.net_addr(), endpoint.path()))
|
||||
.json(&data)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::debug!("An error ocurred when sending data to the tower: {e}");
|
||||
if e.is_connect() | e.is_timeout() {
|
||||
RequestError::ConnectionError(
|
||||
"Cannot connect to the tower. Connection refused".to_owned(),
|
||||
)
|
||||
} else {
|
||||
RequestError::Unexpected(
|
||||
"Unexpected error ocurred (see logs for more info)".to_owned(),
|
||||
)
|
||||
}
|
||||
})
|
||||
let mut request_builder = client.request(
|
||||
method,
|
||||
format!("{}{}", tower_net_addr.net_addr(), endpoint.path()),
|
||||
);
|
||||
|
||||
if let Some(data) = data {
|
||||
request_builder = request_builder.json(&data);
|
||||
}
|
||||
|
||||
request_builder.send().await.map_err(|e| {
|
||||
log::debug!("An error ocurred when sending data to the tower: {e}");
|
||||
if e.is_connect() | e.is_timeout() {
|
||||
RequestError::ConnectionError(
|
||||
"Cannot connect to the tower. Connection refused".to_owned(),
|
||||
)
|
||||
} else {
|
||||
RequestError::Unexpected("Unexpected error ocurred (see logs for more info)".to_owned())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn post_request<S: Serialize>(
|
||||
tower_net_addr: &NetAddr,
|
||||
endpoint: Endpoint,
|
||||
data: S,
|
||||
proxy: &Option<ProxyInfo>,
|
||||
) -> Result<Response, RequestError> {
|
||||
request(tower_net_addr, endpoint, proxy, Method::POST, Some(data)).await
|
||||
}
|
||||
|
||||
pub async fn get_request(
|
||||
tower_net_addr: &NetAddr,
|
||||
endpoint: Endpoint,
|
||||
proxy: &Option<ProxyInfo>,
|
||||
) -> Result<Response, RequestError> {
|
||||
request::<()>(tower_net_addr, endpoint, proxy, Method::GET, None).await
|
||||
}
|
||||
|
||||
/// Generic function to process the response of a given post request.
|
||||
|
|
@ -218,7 +238,6 @@ pub async fn process_post_response<T: DeserializeOwned>(
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use httpmock::prelude::*;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::test_utils::get_dummy_add_appointment_response;
|
||||
|
|
@ -253,24 +272,25 @@ mod tests {
|
|||
let mut registration_receipt = get_random_registration_receipt();
|
||||
registration_receipt.sign(&tower_sk);
|
||||
|
||||
let server = MockServer::start();
|
||||
let api_mock = server.mock(|when, then| {
|
||||
when.method(POST).path(Endpoint::Register.path());
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!(registration_receipt));
|
||||
});
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let api_mock = server
|
||||
.mock("POST", Endpoint::Register.path().as_str())
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(json!(registration_receipt).to_string())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let receipt = register(
|
||||
TowerId(tower_pk),
|
||||
registration_receipt.user_id(),
|
||||
&NetAddr::new(server.base_url()),
|
||||
&NetAddr::new(server.url()),
|
||||
&None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
api_mock.assert();
|
||||
api_mock.assert_async().await;
|
||||
assert_eq!(receipt, registration_receipt);
|
||||
}
|
||||
|
||||
|
|
@ -290,24 +310,25 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_register_deserialize_error() {
|
||||
let server = MockServer::start();
|
||||
let api_mock = server.mock(|when, then| {
|
||||
when.method(POST).path(Endpoint::Register.path());
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!([]));
|
||||
});
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let api_mock = server
|
||||
.mock("POST", Endpoint::Register.path().as_str())
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(json!([]).to_string())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let error = register(
|
||||
get_random_user_id(),
|
||||
get_random_user_id(),
|
||||
&NetAddr::new(server.base_url()),
|
||||
&NetAddr::new(server.url()),
|
||||
&None,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
api_mock.assert();
|
||||
api_mock.assert_async().await;
|
||||
assert!(matches!(error, RequestError::DeserializeError { .. }))
|
||||
}
|
||||
|
||||
|
|
@ -322,17 +343,18 @@ mod tests {
|
|||
let add_appointment_response =
|
||||
get_dummy_add_appointment_response(appointment.locator, &appointment_receipt);
|
||||
|
||||
let server = MockServer::start();
|
||||
let api_mock = server.mock(|when, then| {
|
||||
when.method(POST).path(Endpoint::AddAppointment.path());
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!(add_appointment_response));
|
||||
});
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let api_mock = server
|
||||
.mock("POST", Endpoint::AddAppointment.path().as_str())
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(json!(add_appointment_response).to_string())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let (response, receipt) = add_appointment(
|
||||
TowerId(tower_pk),
|
||||
&NetAddr::new(server.base_url()),
|
||||
&NetAddr::new(server.url()),
|
||||
&None,
|
||||
&appointment,
|
||||
appointment_receipt.user_signature(),
|
||||
|
|
@ -340,7 +362,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
api_mock.assert();
|
||||
api_mock.assert_async().await;
|
||||
assert_eq!(response, add_appointment_response.available_slots);
|
||||
assert_eq!(receipt, appointment_receipt);
|
||||
}
|
||||
|
|
@ -354,17 +376,18 @@ mod tests {
|
|||
let add_appointment_response =
|
||||
get_dummy_add_appointment_response(appointment.locator, &appointment_receipt);
|
||||
|
||||
let server = MockServer::start();
|
||||
let api_mock = server.mock(|when, then| {
|
||||
when.method(POST).path(Endpoint::AddAppointment.path());
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!(add_appointment_response));
|
||||
});
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let api_mock = server
|
||||
.mock("POST", Endpoint::AddAppointment.path().as_str())
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(json!(add_appointment_response).to_string())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let (response, receipt) = send_appointment(
|
||||
TowerId(tower_pk),
|
||||
&NetAddr::new(server.base_url()),
|
||||
&NetAddr::new(server.url()),
|
||||
&None,
|
||||
&appointment,
|
||||
appointment_receipt.user_signature(),
|
||||
|
|
@ -372,7 +395,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
api_mock.assert();
|
||||
api_mock.assert_async().await;
|
||||
assert_eq!(response, add_appointment_response);
|
||||
assert_eq!(receipt, appointment_receipt);
|
||||
}
|
||||
|
|
@ -386,18 +409,19 @@ mod tests {
|
|||
let add_appointment_response =
|
||||
get_dummy_add_appointment_response(appointment.locator, &appointment_receipt);
|
||||
|
||||
let server = MockServer::start();
|
||||
let api_mock = server.mock(|when, then| {
|
||||
when.method(POST).path(Endpoint::AddAppointment.path());
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!(add_appointment_response));
|
||||
});
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let api_mock = server
|
||||
.mock("POST", Endpoint::AddAppointment.path().as_str())
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(json!(add_appointment_response).to_string())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let tower_id = get_random_user_id();
|
||||
let error = send_appointment(
|
||||
tower_id,
|
||||
&NetAddr::new(server.base_url()),
|
||||
&NetAddr::new(server.url()),
|
||||
&None,
|
||||
&appointment,
|
||||
appointment_receipt.user_signature(),
|
||||
|
|
@ -405,7 +429,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap_err();
|
||||
|
||||
api_mock.assert();
|
||||
api_mock.assert_async().await;
|
||||
if let AddAppointmentError::SignatureError(proof) = error {
|
||||
assert_eq!(
|
||||
MisbehaviorProof::new(
|
||||
|
|
@ -441,17 +465,18 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_send_appointment_deserialize_error() {
|
||||
let server = MockServer::start();
|
||||
let api_mock = server.mock(|when, then| {
|
||||
when.method(POST).path(Endpoint::AddAppointment.path());
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!([]));
|
||||
});
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let api_mock = server
|
||||
.mock("POST", Endpoint::AddAppointment.path().as_str())
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(json!([]).to_string())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let error = send_appointment(
|
||||
get_random_user_id(),
|
||||
&NetAddr::new(server.base_url()),
|
||||
&NetAddr::new(server.url()),
|
||||
&None,
|
||||
&generate_random_appointment(None),
|
||||
"user_sig",
|
||||
|
|
@ -459,7 +484,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap_err();
|
||||
|
||||
api_mock.assert();
|
||||
api_mock.assert_async().await;
|
||||
if let AddAppointmentError::RequestError(e) = error {
|
||||
assert!(matches!(e, RequestError::DeserializeError { .. }))
|
||||
} else {
|
||||
|
|
@ -474,17 +499,18 @@ mod tests {
|
|||
error_code: 1,
|
||||
};
|
||||
|
||||
let server = MockServer::start();
|
||||
let api_mock = server.mock(|when, then| {
|
||||
when.method(POST).path(Endpoint::AddAppointment.path());
|
||||
then.status(400)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!(api_error));
|
||||
});
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let api_mock = server
|
||||
.mock("POST", Endpoint::AddAppointment.path().as_str())
|
||||
.with_status(400)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(json!(api_error).to_string())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let error = send_appointment(
|
||||
get_random_user_id(),
|
||||
&NetAddr::new(server.base_url()),
|
||||
&NetAddr::new(server.url()),
|
||||
&None,
|
||||
&generate_random_appointment(None),
|
||||
"user_sig",
|
||||
|
|
@ -492,60 +518,160 @@ mod tests {
|
|||
.await
|
||||
.unwrap_err();
|
||||
|
||||
api_mock.assert();
|
||||
api_mock.assert_async().await;
|
||||
assert!(matches!(error, AddAppointmentError::ApiError { .. }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
|
||||
// Test with POST
|
||||
let api_mock_post = server
|
||||
.mock("POST", Endpoint::Register.path().as_str())
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let response_post = request(
|
||||
&NetAddr::new(server.url()),
|
||||
Endpoint::Register,
|
||||
&None,
|
||||
Method::POST,
|
||||
Some(json!("")),
|
||||
)
|
||||
.await;
|
||||
|
||||
api_mock_post.assert_async().await;
|
||||
assert!(matches!(response_post, Ok(Response { .. })));
|
||||
|
||||
// Test with GET
|
||||
let api_mock_get = server
|
||||
.mock("GET", Endpoint::Ping.path().as_str())
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let response_get = request::<()>(
|
||||
&NetAddr::new(server.url()),
|
||||
Endpoint::Ping,
|
||||
&None,
|
||||
Method::GET,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
api_mock_get.assert_async().await;
|
||||
assert!(matches!(response_get, Ok(Response { .. })));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_request_connection_error() {
|
||||
assert!(request(
|
||||
&NetAddr::new("http://unreachable_url".to_owned()),
|
||||
Endpoint::Register,
|
||||
&None,
|
||||
Method::POST,
|
||||
Some(json!("")),
|
||||
)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.is_connection());
|
||||
|
||||
assert!(request(
|
||||
&NetAddr::new("http://unreachable_url".to_owned()),
|
||||
Endpoint::Ping,
|
||||
&None,
|
||||
Method::GET,
|
||||
None::<&str>,
|
||||
)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.is_connection());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_request() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let api_mock = server
|
||||
.mock("GET", Endpoint::Ping.path().as_str())
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.create_async()
|
||||
.await;
|
||||
let response = get_request(&NetAddr::new(server.url()), Endpoint::Ping, &None).await;
|
||||
|
||||
api_mock.assert_async().await;
|
||||
|
||||
assert!(matches!(response, Ok(Response { .. })));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_request_connection_error() {
|
||||
assert!(get_request(
|
||||
&NetAddr::new("http://unreachable_url".to_owned()),
|
||||
Endpoint::Ping,
|
||||
&None,
|
||||
)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.is_connection());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_post_request() {
|
||||
let server = MockServer::start();
|
||||
let api_mock = server.mock(|when, then| {
|
||||
when.method(POST);
|
||||
then.status(200).header("content-type", "application/json");
|
||||
});
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let api_mock = server
|
||||
.mock("POST", Endpoint::Register.path().as_str())
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let response = post_request(
|
||||
&NetAddr::new(server.base_url()),
|
||||
&NetAddr::new(server.url()),
|
||||
Endpoint::Register,
|
||||
json!(""),
|
||||
&None,
|
||||
)
|
||||
.await;
|
||||
|
||||
api_mock.assert_async().await;
|
||||
assert!(matches!(response, Ok(Response { .. })));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_post_request_connection_error() {
|
||||
assert!(post_request(
|
||||
&NetAddr::new("http://unreachable_url".to_owned()),
|
||||
Endpoint::Register,
|
||||
json!(""),
|
||||
&None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
api_mock.assert();
|
||||
assert!(matches!(response, Response { .. }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_post_request_connection_error() {
|
||||
assert!(matches!(
|
||||
post_request(
|
||||
&NetAddr::new("http://unreachable_url".to_owned()),
|
||||
Endpoint::Register,
|
||||
json!(""),
|
||||
&None,
|
||||
)
|
||||
.await
|
||||
.unwrap_err(),
|
||||
RequestError::ConnectionError { .. }
|
||||
));
|
||||
.unwrap_err()
|
||||
.is_connection());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_process_post_response_json_error() {
|
||||
// `process_post_response` is a pass-trough function that maps json deserialization errors from `post_request`.
|
||||
// So just testing that specific case should be enough.
|
||||
let server = MockServer::start();
|
||||
let api_mock = server.mock(|when, then| {
|
||||
when.method(POST);
|
||||
then.status(200).header("content-type", "application/json");
|
||||
});
|
||||
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let api_mock = server
|
||||
.mock("POST", Endpoint::GetAppointment.path().as_str())
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// Any expected response work here as long as it cannot be properly deserialized
|
||||
let error = process_post_response::<ApiResponse<common_msgs::GetAppointmentResponse>>(
|
||||
post_request(
|
||||
&NetAddr::new(server.base_url()),
|
||||
&NetAddr::new(server.url()),
|
||||
Endpoint::GetAppointment,
|
||||
json!(""),
|
||||
&None,
|
||||
|
|
@ -555,7 +681,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap_err();
|
||||
|
||||
api_mock.assert();
|
||||
api_mock.assert_async().await;
|
||||
assert!(matches!(error, RequestError::DeserializeError { .. }));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ impl RetryManager {
|
|||
} else if let Some(retrier) = self.retriers.get(&tower_id) {
|
||||
if retrier.is_idle() {
|
||||
if !data.is_none() {
|
||||
log::error!("Data was send to an idle retier. This should have never happened. Please report! ({data:?})");
|
||||
log::error!("Data was send to an idle retrier. This should have never happened. Please report! ({data:?})");
|
||||
continue;
|
||||
}
|
||||
log::info!(
|
||||
|
|
@ -432,7 +432,7 @@ impl Retrier {
|
|||
// Create a new scope so we can get all the data only locking the WTClient once.
|
||||
let (tower_id, status, net_addr, user_id, user_sk, proxy) = {
|
||||
let wt_client = self.wt_client.lock().unwrap();
|
||||
if wt_client.towers.get(&self.tower_id).is_none() {
|
||||
if !wt_client.towers.contains_key(&self.tower_id) {
|
||||
return Err(Error::permanent(RetryError::Abandoned));
|
||||
}
|
||||
|
||||
|
|
@ -491,7 +491,7 @@ impl Retrier {
|
|||
&net_addr,
|
||||
&proxy,
|
||||
&appointment,
|
||||
&cryptography::sign(&appointment.to_vec(), &user_sk).unwrap(),
|
||||
&cryptography::sign(&appointment.to_vec(), &user_sk),
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
|
@ -576,13 +576,13 @@ impl Retrier {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use httpmock::prelude::*;
|
||||
use serde_json::json;
|
||||
use tempdir::TempDir;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
|
||||
use teos_common::errors;
|
||||
use teos_common::net::http::Endpoint;
|
||||
use teos_common::protos::AddAppointmentRequest;
|
||||
use teos_common::receipts::{AppointmentReceipt, RegistrationReceipt};
|
||||
use teos_common::test_utils::{
|
||||
generate_random_appointment, get_random_registration_receipt, get_random_user_id,
|
||||
|
|
@ -600,6 +600,18 @@ mod tests {
|
|||
const MAX_INTERVAL_TIME: u16 = 1;
|
||||
const MAX_RUN_TIME: f64 = 0.2;
|
||||
|
||||
macro_rules! wait_until {
|
||||
() => {};
|
||||
($cond:expr $(,)?) => {
|
||||
loop {
|
||||
if $cond {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs_f64(0.1)).await;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Retrier {
|
||||
fn empty(wt_client: Arc<Mutex<WTClient>>, tower_id: TowerId) -> Self {
|
||||
Self {
|
||||
|
|
@ -612,15 +624,14 @@ mod tests {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
// TODO: It'll be nice to toggle the mock on and off instead of having it always on. Not sure MockServer allows that though:
|
||||
// https://github.com/alexliesenfeld/httpmock/issues/67
|
||||
async fn test_manage_retry_reachable() {
|
||||
let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap();
|
||||
let (tx, rx) = unbounded_channel();
|
||||
let wt_client = Arc::new(Mutex::new(
|
||||
WTClient::new(tmp_path.path().to_path_buf(), tx.clone()).await,
|
||||
));
|
||||
let server = MockServer::start();
|
||||
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
|
||||
// Add a tower with pending appointments
|
||||
let (tower_sk, tower_pk) = cryptography::get_random_keypair();
|
||||
|
|
@ -629,7 +640,7 @@ mod tests {
|
|||
wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.add_update_tower(tower_id, &server.base_url(), &receipt)
|
||||
.add_update_tower(tower_id, &server.url(), &receipt)
|
||||
.unwrap();
|
||||
|
||||
// Add appointment to pending
|
||||
|
|
@ -641,19 +652,23 @@ mod tests {
|
|||
|
||||
// Prepare the mock response
|
||||
let mut add_appointment_receipt = AppointmentReceipt::new(
|
||||
cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk).unwrap(),
|
||||
cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk),
|
||||
42,
|
||||
);
|
||||
add_appointment_receipt.sign(&tower_sk);
|
||||
let add_appointment_response =
|
||||
get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt);
|
||||
let api_mock = server.mock(|when, then| {
|
||||
when.method(POST).path(Endpoint::AddAppointment.path());
|
||||
then.status(200)
|
||||
.delay(Duration::from_secs_f64(API_DELAY))
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!(add_appointment_response));
|
||||
});
|
||||
|
||||
let api_mock = server
|
||||
.mock("POST", Endpoint::AddAppointment.path().as_str())
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body_from_request(move |_| {
|
||||
std::thread::sleep(Duration::from_secs_f64(API_DELAY));
|
||||
json!(add_appointment_response).to_string().into()
|
||||
})
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// Start the task and send the tower to the channel for retry
|
||||
tx.send((tower_id, RevocationData::Fresh(appointment.locator)))
|
||||
|
|
@ -681,23 +696,23 @@ mod tests {
|
|||
.unwrap()
|
||||
.is_running());
|
||||
|
||||
// Wait for the remaining time and re-check
|
||||
tokio::time::sleep(Duration::from_secs_f64(MAX_RUN_TIME + HALF_API_DELAY)).await;
|
||||
|
||||
let state = wt_client.lock().unwrap();
|
||||
assert_eq!(
|
||||
state.get_tower_status(&tower_id).unwrap(),
|
||||
TowerStatus::Reachable
|
||||
);
|
||||
assert!(!state.retriers.contains_key(&tower_id));
|
||||
assert!(!state
|
||||
.towers
|
||||
.get(&tower_id)
|
||||
wait_until!(wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.pending_appointments
|
||||
.contains(&appointment.locator));
|
||||
.get_retrier_status(&tower_id)
|
||||
.is_none());
|
||||
|
||||
api_mock.assert();
|
||||
{
|
||||
let state = wt_client.lock().unwrap();
|
||||
assert!(state.get_tower_status(&tower_id).unwrap().is_reachable());
|
||||
assert!(!state
|
||||
.towers
|
||||
.get(&tower_id)
|
||||
.unwrap()
|
||||
.pending_appointments
|
||||
.contains(&appointment.locator));
|
||||
}
|
||||
api_mock.assert_async().await;
|
||||
|
||||
task.abort();
|
||||
}
|
||||
|
|
@ -759,39 +774,38 @@ mod tests {
|
|||
.unwrap()
|
||||
.is_running());
|
||||
|
||||
// Wait until the task gives up and check again (this gives up due to accumulation of transient errors, so the retiers will be idle).
|
||||
// Notice we'd normally wait for MAX_ELAPSED_TIME + MAX_RUN_TIME (the maximum time a Retrier can be working plus the marginal time of the last retry).
|
||||
// However, we've already waited for MAX_RUN_TIME right before to check the tower was temporary unreachable, so we don't need to account for that again.
|
||||
tokio::time::sleep(Duration::from_secs(MAX_ELAPSED_TIME as u64)).await;
|
||||
assert!(wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_tower_status(&tower_id)
|
||||
.unwrap()
|
||||
.is_unreachable());
|
||||
assert!(wt_client
|
||||
// Wait until the task gives up and check again (this gives up due to accumulation of transient errors, so the retriers will be idle).
|
||||
wait_until!(wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_retrier_status(&tower_id)
|
||||
.unwrap()
|
||||
.is_idle());
|
||||
|
||||
assert!(wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_tower_status(&tower_id)
|
||||
.unwrap()
|
||||
.is_unreachable());
|
||||
|
||||
// Add a proper server and check that the auto-retry works
|
||||
// Prepare the mock response
|
||||
let server = MockServer::start();
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
let mut add_appointment_receipt = AppointmentReceipt::new(
|
||||
cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk).unwrap(),
|
||||
cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk),
|
||||
42,
|
||||
);
|
||||
add_appointment_receipt.sign(&tower_sk);
|
||||
let add_appointment_response =
|
||||
get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt);
|
||||
let api_mock = server.mock(|when, then| {
|
||||
when.method(POST).path(Endpoint::AddAppointment.path());
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!(add_appointment_response));
|
||||
});
|
||||
let api_mock = server
|
||||
.mock("POST", Endpoint::AddAppointment.path().as_str())
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(json!(add_appointment_response).to_string())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// Update the tower details
|
||||
wt_client
|
||||
|
|
@ -799,7 +813,7 @@ mod tests {
|
|||
.unwrap()
|
||||
.add_update_tower(
|
||||
tower_id,
|
||||
&server.base_url(),
|
||||
&server.url(),
|
||||
&get_registration_receipt_from_previous(&receipt),
|
||||
)
|
||||
.unwrap();
|
||||
|
|
@ -824,8 +838,7 @@ mod tests {
|
|||
.pending_appointments
|
||||
.contains(&appointment.locator));
|
||||
assert!(!wt_client.lock().unwrap().retriers.contains_key(&tower_id));
|
||||
|
||||
api_mock.assert();
|
||||
api_mock.assert_async().await;
|
||||
|
||||
task.abort();
|
||||
}
|
||||
|
|
@ -837,7 +850,7 @@ mod tests {
|
|||
let wt_client = Arc::new(Mutex::new(
|
||||
WTClient::new(tmp_path.path().to_path_buf(), tx.clone()).await,
|
||||
));
|
||||
let server = MockServer::start();
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
|
||||
// Add a tower with pending appointments
|
||||
let (_, tower_pk) = cryptography::get_random_keypair();
|
||||
|
|
@ -846,7 +859,7 @@ mod tests {
|
|||
wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.add_update_tower(tower_id, &server.base_url(), &receipt)
|
||||
.add_update_tower(tower_id, &server.url(), &receipt)
|
||||
.unwrap();
|
||||
|
||||
// Add appointment to pending
|
||||
|
|
@ -857,16 +870,21 @@ mod tests {
|
|||
.add_pending_appointment(tower_id, &appointment);
|
||||
|
||||
// Prepare the mock response
|
||||
let api_mock = server.mock(|when, then| {
|
||||
when.method(POST).path(Endpoint::AddAppointment.path());
|
||||
then.status(400)
|
||||
.delay(Duration::from_secs_f64(API_DELAY))
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!(ApiError {
|
||||
let api_mock = server
|
||||
.mock("POST", Endpoint::AddAppointment.path().as_str())
|
||||
.with_status(400)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body_from_request(|_| {
|
||||
std::thread::sleep(Duration::from_secs_f64(API_DELAY));
|
||||
json!(ApiError {
|
||||
error: "error_msg".to_owned(),
|
||||
error_code: 1,
|
||||
}));
|
||||
});
|
||||
})
|
||||
.to_string()
|
||||
.into()
|
||||
})
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// Start the task and send the tower to the channel for retry
|
||||
tx.send((tower_id, RevocationData::Fresh(appointment.locator)))
|
||||
|
|
@ -895,16 +913,18 @@ mod tests {
|
|||
.is_running());
|
||||
|
||||
// Wait for the remaining time and re-check
|
||||
tokio::time::sleep(Duration::from_secs_f64(MAX_RUN_TIME + HALF_API_DELAY)).await;
|
||||
assert_eq!(
|
||||
wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_tower_status(&tower_id)
|
||||
.unwrap(),
|
||||
TowerStatus::Reachable
|
||||
);
|
||||
assert!(!wt_client.lock().unwrap().retriers.contains_key(&tower_id));
|
||||
wait_until!(wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_retrier_status(&tower_id)
|
||||
.is_none());
|
||||
|
||||
assert!(wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_tower_status(&tower_id)
|
||||
.unwrap()
|
||||
.is_reachable());
|
||||
assert!(!wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
|
|
@ -921,7 +941,7 @@ mod tests {
|
|||
.unwrap()
|
||||
.invalid_appointments
|
||||
.contains(&appointment.locator));
|
||||
api_mock.assert();
|
||||
api_mock.assert_async().await;
|
||||
|
||||
task.abort();
|
||||
}
|
||||
|
|
@ -933,7 +953,7 @@ mod tests {
|
|||
let wt_client = Arc::new(Mutex::new(
|
||||
WTClient::new(tmp_path.path().to_path_buf(), tx.clone()).await,
|
||||
));
|
||||
let server = MockServer::start();
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
|
||||
// Add a tower with pending appointments
|
||||
let (_, tower_pk) = cryptography::get_random_keypair();
|
||||
|
|
@ -942,7 +962,7 @@ mod tests {
|
|||
wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.add_update_tower(tower_id, &server.base_url(), &receipt)
|
||||
.add_update_tower(tower_id, &server.url(), &receipt)
|
||||
.unwrap();
|
||||
|
||||
// Add appointment to pending
|
||||
|
|
@ -954,20 +974,23 @@ mod tests {
|
|||
|
||||
// Prepare the mock response
|
||||
let mut add_appointment_receipt = AppointmentReceipt::new(
|
||||
cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk).unwrap(),
|
||||
cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk),
|
||||
42,
|
||||
);
|
||||
// Sign with a random key so it counts as misbehaving
|
||||
add_appointment_receipt.sign(&cryptography::get_random_keypair().0);
|
||||
let add_appointment_response =
|
||||
get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt);
|
||||
let api_mock = server.mock(|when, then| {
|
||||
when.method(POST).path(Endpoint::AddAppointment.path());
|
||||
then.status(200)
|
||||
.delay(Duration::from_secs_f64(API_DELAY))
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!(add_appointment_response));
|
||||
});
|
||||
let api_mock = server
|
||||
.mock("POST", Endpoint::AddAppointment.path().as_str())
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body_from_request(move |_| {
|
||||
std::thread::sleep(Duration::from_secs_f64(API_DELAY));
|
||||
json!(add_appointment_response).to_string().into()
|
||||
})
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// Start the task and send the tower to the channel for retry
|
||||
tx.send((tower_id, RevocationData::Fresh(appointment.locator)))
|
||||
|
|
@ -995,19 +1018,21 @@ mod tests {
|
|||
.unwrap()
|
||||
.is_running());
|
||||
|
||||
// Wait for the remaining time and re-check
|
||||
tokio::time::sleep(Duration::from_secs_f64(HALF_API_DELAY + MAX_RUN_TIME)).await;
|
||||
// Wait until the tower is no longer being retried.
|
||||
wait_until!(wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_retrier_status(&tower_id)
|
||||
.is_none());
|
||||
|
||||
// The tower should have a misbehaving status.
|
||||
assert!(wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_tower_status(&tower_id)
|
||||
.unwrap()
|
||||
.is_misbehaving());
|
||||
|
||||
// Retriers are wiped every polling interval, so we'll need to wait a bit more to check it
|
||||
tokio::time::sleep(Duration::from_secs(POLLING_TIME)).await;
|
||||
assert!(!wt_client.lock().unwrap().retriers.contains_key(&tower_id));
|
||||
api_mock.assert();
|
||||
api_mock.assert_async().await;
|
||||
|
||||
task.abort();
|
||||
}
|
||||
|
|
@ -1019,7 +1044,7 @@ mod tests {
|
|||
let wt_client = Arc::new(Mutex::new(
|
||||
WTClient::new(tmp_path.path().to_path_buf(), tx.clone()).await,
|
||||
));
|
||||
let server = MockServer::start();
|
||||
let server = mockito::Server::new_async().await;
|
||||
|
||||
// Add a tower with pending appointments
|
||||
let (_, tower_pk) = cryptography::get_random_keypair();
|
||||
|
|
@ -1028,7 +1053,7 @@ mod tests {
|
|||
wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.add_update_tower(tower_id, &server.base_url(), &receipt)
|
||||
.add_update_tower(tower_id, &server.url(), &receipt)
|
||||
.unwrap();
|
||||
|
||||
// Remove the tower (to simulate it has been abandoned)
|
||||
|
|
@ -1049,7 +1074,6 @@ mod tests {
|
|||
.manage_retry()
|
||||
.await
|
||||
});
|
||||
|
||||
assert!(!wt_client.lock().unwrap().towers.contains_key(&tower_id));
|
||||
|
||||
task.abort();
|
||||
|
|
@ -1062,7 +1086,7 @@ mod tests {
|
|||
let wt_client = Arc::new(Mutex::new(
|
||||
WTClient::new(tmp_path.path().to_path_buf(), tx.clone()).await,
|
||||
));
|
||||
let server = MockServer::start();
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
|
||||
// Add a tower with pending appointments
|
||||
let (tower_sk, tower_pk) = cryptography::get_random_keypair();
|
||||
|
|
@ -1073,7 +1097,7 @@ mod tests {
|
|||
wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.add_update_tower(tower_id, &server.base_url(), ®istration_receipt)
|
||||
.add_update_tower(tower_id, &server.url(), ®istration_receipt)
|
||||
.unwrap();
|
||||
|
||||
// Add appointment to pending
|
||||
|
|
@ -1083,32 +1107,37 @@ mod tests {
|
|||
.unwrap()
|
||||
.add_pending_appointment(tower_id, &appointment);
|
||||
|
||||
// Mock the add_appointment response (this is right, so after the re-registration the appointments are accepted)
|
||||
// Mock the registration and add_appointment response (this is right, so after the re-registration the appointments are accepted)
|
||||
let mut re_registration_receipt =
|
||||
get_registration_receipt_from_previous(®istration_receipt);
|
||||
re_registration_receipt.sign(&tower_sk);
|
||||
|
||||
let mut add_appointment_receipt = AppointmentReceipt::new(
|
||||
cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk).unwrap(),
|
||||
cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk),
|
||||
42,
|
||||
);
|
||||
add_appointment_receipt.sign(&tower_sk);
|
||||
let add_appointment_response =
|
||||
get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt);
|
||||
let add_appointment_mock = server.mock(|when, then| {
|
||||
when.method(POST).path(Endpoint::AddAppointment.path());
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!(add_appointment_response));
|
||||
});
|
||||
|
||||
// Mock the re-registration
|
||||
let mut re_registration_receipt =
|
||||
get_registration_receipt_from_previous(®istration_receipt);
|
||||
re_registration_receipt.sign(&tower_sk);
|
||||
let register_mock = server.mock(|when, then| {
|
||||
when.method(POST).path(Endpoint::Register.path());
|
||||
then.status(200)
|
||||
.delay(Duration::from_secs_f64(API_DELAY))
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!(re_registration_receipt));
|
||||
});
|
||||
let api_mock = server
|
||||
.mock("POST", mockito::Matcher::Any)
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body_from_request(move |request| {
|
||||
let response = if request.path() == Endpoint::Register.path().as_str() {
|
||||
std::thread::sleep(Duration::from_secs_f64(API_DELAY));
|
||||
json!(re_registration_receipt).to_string()
|
||||
} else if request.path() == Endpoint::AddAppointment.path().as_str() {
|
||||
json!(add_appointment_response).to_string()
|
||||
} else {
|
||||
panic!("Wrong endpoint hit")
|
||||
};
|
||||
response.into()
|
||||
})
|
||||
.create_async()
|
||||
.await
|
||||
.expect(2);
|
||||
|
||||
// Set the status as SubscriptionError so we simulate the retrier faced this in a previous round
|
||||
wt_client
|
||||
|
|
@ -1143,16 +1172,20 @@ mod tests {
|
|||
.is_running());
|
||||
|
||||
// Wait for the remaining time and re-check
|
||||
tokio::time::sleep(Duration::from_secs_f64(MAX_RUN_TIME + HALF_API_DELAY)).await;
|
||||
let state = wt_client.lock().unwrap();
|
||||
assert!(!state.retriers.contains_key(&tower_id));
|
||||
wait_until!(wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_retrier_status(&tower_id)
|
||||
.is_none());
|
||||
|
||||
let tower = state.towers.get(&tower_id).unwrap();
|
||||
assert!(tower.status.is_reachable());
|
||||
assert!(tower.pending_appointments.is_empty());
|
||||
{
|
||||
let state = wt_client.lock().unwrap();
|
||||
let tower = state.towers.get(&tower_id).unwrap();
|
||||
assert!(tower.status.is_reachable());
|
||||
assert!(tower.pending_appointments.is_empty());
|
||||
}
|
||||
api_mock.assert_async().await;
|
||||
|
||||
register_mock.assert();
|
||||
add_appointment_mock.assert();
|
||||
task.abort();
|
||||
}
|
||||
|
||||
|
|
@ -1206,9 +1239,15 @@ mod tests {
|
|||
MAX_ELAPSED_TIME as f64 + MAX_RUN_TIME,
|
||||
))
|
||||
.await;
|
||||
let state = wt_client.lock().unwrap();
|
||||
assert!(state.get_retrier_status(&tower_id).unwrap().is_idle());
|
||||
|
||||
wait_until!(wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_retrier_status(&tower_id)
|
||||
.unwrap()
|
||||
.is_idle());
|
||||
|
||||
let state = wt_client.lock().unwrap();
|
||||
let tower = state.towers.get(&tower_id).unwrap();
|
||||
assert!(tower.pending_appointments.contains(&appointment.locator));
|
||||
assert_eq!(tower.status, TowerStatus::Unreachable);
|
||||
|
|
@ -1216,12 +1255,12 @@ mod tests {
|
|||
|
||||
// With the retrier idling all fresh data sent to it will be stored but it won't trigger a retry.
|
||||
// (we can check the data was stored later on)
|
||||
let new_appointment = generate_random_appointment(None);
|
||||
let appointment2 = generate_random_appointment(None);
|
||||
wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.add_pending_appointment(tower_id, &new_appointment);
|
||||
tx.send((tower_id, RevocationData::Fresh(new_appointment.locator)))
|
||||
.add_pending_appointment(tower_id, &appointment2);
|
||||
tx.send((tower_id, RevocationData::Fresh(appointment2.locator)))
|
||||
.unwrap();
|
||||
|
||||
{
|
||||
|
|
@ -1232,22 +1271,40 @@ mod tests {
|
|||
assert_eq!(tower.status, TowerStatus::Unreachable);
|
||||
}
|
||||
|
||||
let mut add_appointment_receipt = AppointmentReceipt::new(
|
||||
cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk).unwrap(),
|
||||
// Create the receipts, the responses and set the mocks
|
||||
let mut appointment_receipt = AppointmentReceipt::new(
|
||||
cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk),
|
||||
42,
|
||||
);
|
||||
let mut appointment2_receipt = AppointmentReceipt::new(
|
||||
cryptography::sign(&appointment2.to_vec(), &wt_client.lock().unwrap().user_sk),
|
||||
42,
|
||||
);
|
||||
appointment_receipt.sign(&tower_sk);
|
||||
appointment2_receipt.sign(&tower_sk);
|
||||
|
||||
// Mock a proper response
|
||||
let server = MockServer::start();
|
||||
add_appointment_receipt.sign(&tower_sk);
|
||||
let add_appointment_response =
|
||||
get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt);
|
||||
let api_mock = server.mock(|when, then| {
|
||||
when.method(POST).path(Endpoint::AddAppointment.path());
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!(add_appointment_response));
|
||||
});
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
|
||||
let api_mock = server
|
||||
.mock("POST", Endpoint::AddAppointment.path().as_str())
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body_from_request(move |request| {
|
||||
let body = serde_json::from_slice::<AddAppointmentRequest>(request.body().unwrap())
|
||||
.unwrap();
|
||||
|
||||
let response = if body.appointment.unwrap().locator == appointment.locator.to_vec()
|
||||
{
|
||||
get_dummy_add_appointment_response(appointment.locator, &appointment_receipt)
|
||||
} else {
|
||||
get_dummy_add_appointment_response(appointment2.locator, &appointment2_receipt)
|
||||
};
|
||||
json!(response).to_string().into()
|
||||
})
|
||||
.expect(2)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// Patch the tower address
|
||||
wt_client
|
||||
|
|
@ -1256,7 +1313,7 @@ mod tests {
|
|||
.towers
|
||||
.get_mut(&tower_id)
|
||||
.unwrap()
|
||||
.set_net_addr(server.base_url());
|
||||
.set_net_addr(server.url());
|
||||
|
||||
// Check pending data is still there now, and is it not once the retrier succeeds
|
||||
assert_eq!(
|
||||
|
|
@ -1274,24 +1331,19 @@ mod tests {
|
|||
// Send a retry flag to the retrier to force a retry.
|
||||
tx.send((tower_id, RevocationData::None)).unwrap();
|
||||
|
||||
// After retrying the pending pool has been emptied, meaning that both appointments went trough
|
||||
tokio::time::sleep(Duration::from_secs_f64(POLLING_TIME as f64 + MAX_RUN_TIME)).await;
|
||||
// FIXME: Here we should be able to check this, however, due to httpmock limitations, we cannot return a response based on the request.
|
||||
// Therefore, both requests will be responded with the same data. Given pending_appointments is a HashSet, we cannot even know which request
|
||||
// will be sent first (sets are initialized with a random state, which decided the order or iteration).
|
||||
// https://github.com/alexliesenfeld/httpmock/issues/49
|
||||
// assert!(!wt_client.lock().unwrap().retriers.contains_key(&tower_id));
|
||||
// assert!(wt_client
|
||||
// .lock()
|
||||
// .unwrap()
|
||||
// .towers
|
||||
// .get(&tower_id)
|
||||
// .unwrap()
|
||||
// .pending_appointments
|
||||
// .is_empty());
|
||||
assert!(!wt_client.lock().unwrap().retriers.contains_key(&tower_id));
|
||||
assert!(wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.towers
|
||||
.get(&tower_id)
|
||||
.unwrap()
|
||||
.pending_appointments
|
||||
.is_empty());
|
||||
api_mock.assert_async().await;
|
||||
|
||||
// This is not much tbh, but looks like its the best we can do at the moment without experiencing random errors.
|
||||
// Depending on what appointment is sent first the api will be hit either one or two times.
|
||||
assert!(api_mock.hits() >= 1 && api_mock.hits() <= 2);
|
||||
task.abort();
|
||||
}
|
||||
|
||||
|
|
@ -1303,14 +1355,14 @@ mod tests {
|
|||
let wt_client = Arc::new(Mutex::new(
|
||||
WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await,
|
||||
));
|
||||
let server = MockServer::start();
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
|
||||
// The tower we'd like to retry sending appointments to has to exist within the plugin
|
||||
let receipt = get_random_registration_receipt();
|
||||
wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.add_update_tower(tower_id, &server.base_url(), &receipt)
|
||||
.add_update_tower(tower_id, &server.url(), &receipt)
|
||||
.unwrap();
|
||||
|
||||
// Add appointment to pending
|
||||
|
|
@ -1322,24 +1374,25 @@ mod tests {
|
|||
|
||||
// Prepare the mock response
|
||||
let mut add_appointment_receipt = AppointmentReceipt::new(
|
||||
cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk).unwrap(),
|
||||
cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk),
|
||||
42,
|
||||
);
|
||||
add_appointment_receipt.sign(&tower_sk);
|
||||
let add_appointment_response =
|
||||
get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt);
|
||||
let api_mock = server.mock(|when, then| {
|
||||
when.method(POST).path(Endpoint::AddAppointment.path());
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!(add_appointment_response));
|
||||
});
|
||||
let api_mock = server
|
||||
.mock("POST", Endpoint::AddAppointment.path().as_str())
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(json!(add_appointment_response).to_string())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// Since we are retrying manually, we need to add the data to pending appointments manually too
|
||||
let retrier = Retrier::new(wt_client, tower_id, HashSet::from([appointment.locator]));
|
||||
let r = retrier.run().await;
|
||||
assert_eq!(r, Ok(()));
|
||||
api_mock.assert();
|
||||
api_mock.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -1350,14 +1403,14 @@ mod tests {
|
|||
let wt_client = Arc::new(Mutex::new(
|
||||
WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await,
|
||||
));
|
||||
let server = MockServer::start();
|
||||
let server = mockito::Server::new_async().await;
|
||||
|
||||
// The tower we'd like to retry sending appointments to has to exist within the plugin
|
||||
let receipt = get_random_registration_receipt();
|
||||
wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.add_update_tower(tower_id, &server.base_url(), &receipt)
|
||||
.add_update_tower(tower_id, &server.url(), &receipt)
|
||||
.unwrap();
|
||||
|
||||
// If there are no pending appointments the method will simply return
|
||||
|
|
@ -1373,14 +1426,14 @@ mod tests {
|
|||
let wt_client = Arc::new(Mutex::new(
|
||||
WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await,
|
||||
));
|
||||
let server = MockServer::start();
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
|
||||
// The tower we'd like to retry sending appointments to has to exist within the plugin
|
||||
let receipt = get_random_registration_receipt();
|
||||
wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.add_update_tower(tower_id, &server.base_url(), &receipt)
|
||||
.add_update_tower(tower_id, &server.url(), &receipt)
|
||||
.unwrap();
|
||||
|
||||
// Add appointment to pending
|
||||
|
|
@ -1392,18 +1445,19 @@ mod tests {
|
|||
|
||||
// Prepare the mock response
|
||||
let mut add_appointment_receipt = AppointmentReceipt::new(
|
||||
cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk).unwrap(),
|
||||
cryptography::sign(&appointment.to_vec(), &wt_client.lock().unwrap().user_sk),
|
||||
42,
|
||||
);
|
||||
add_appointment_receipt.sign(&cryptography::get_random_keypair().0);
|
||||
let add_appointment_response =
|
||||
get_dummy_add_appointment_response(appointment.locator, &add_appointment_receipt);
|
||||
let api_mock = server.mock(|when, then| {
|
||||
when.method(POST).path(Endpoint::AddAppointment.path());
|
||||
then.status(200)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!(add_appointment_response));
|
||||
});
|
||||
let api_mock = server
|
||||
.mock("POST", Endpoint::AddAppointment.path().as_str())
|
||||
.with_status(200)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(json!(add_appointment_response).to_string())
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// Since we are retrying manually, we need to add the data to pending appointments manually too
|
||||
let retrier = Retrier::new(wt_client, tower_id, HashSet::from([appointment.locator]));
|
||||
|
|
@ -1412,7 +1466,7 @@ mod tests {
|
|||
r,
|
||||
Err(Error::Permanent(RetryError::Misbehaving { .. },))
|
||||
));
|
||||
api_mock.assert();
|
||||
api_mock.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -1454,25 +1508,29 @@ mod tests {
|
|||
let wt_client = Arc::new(Mutex::new(
|
||||
WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await,
|
||||
));
|
||||
let server = MockServer::start();
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
|
||||
// The tower we'd like to retry sending appointments to has to exist within the plugin
|
||||
let receipt = get_random_registration_receipt();
|
||||
wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.add_update_tower(tower_id, &server.base_url(), &receipt)
|
||||
.add_update_tower(tower_id, &server.url(), &receipt)
|
||||
.unwrap();
|
||||
|
||||
let api_mock = server.mock(|when, then| {
|
||||
when.method(POST).path(Endpoint::AddAppointment.path());
|
||||
then.status(400)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!(ApiError {
|
||||
let api_mock = server
|
||||
.mock("POST", Endpoint::AddAppointment.path().as_str())
|
||||
.with_status(400)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(
|
||||
json!(ApiError {
|
||||
error: "error_msg".to_owned(),
|
||||
error_code: errors::INVALID_SIGNATURE_OR_SUBSCRIPTION_ERROR,
|
||||
}));
|
||||
});
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// Add some pending appointments and try again (with an unreachable tower).
|
||||
let appointment = generate_random_appointment(None);
|
||||
|
|
@ -1492,7 +1550,7 @@ mod tests {
|
|||
..
|
||||
})
|
||||
));
|
||||
api_mock.assert();
|
||||
api_mock.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -1503,25 +1561,29 @@ mod tests {
|
|||
let wt_client = Arc::new(Mutex::new(
|
||||
WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await,
|
||||
));
|
||||
let server = MockServer::start();
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
|
||||
// The tower we'd like to retry sending appointments to has to exist within the plugin
|
||||
let receipt = get_random_registration_receipt();
|
||||
wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.add_update_tower(tower_id, &server.base_url(), &receipt)
|
||||
.add_update_tower(tower_id, &server.url(), &receipt)
|
||||
.unwrap();
|
||||
|
||||
let api_mock = server.mock(|when, then| {
|
||||
when.method(POST).path(Endpoint::AddAppointment.path());
|
||||
then.status(400)
|
||||
.header("content-type", "application/json")
|
||||
.json_body(json!(ApiError {
|
||||
let api_mock = server
|
||||
.mock("POST", Endpoint::AddAppointment.path().as_str())
|
||||
.with_status(400)
|
||||
.with_header("content-type", "application/json")
|
||||
.with_body(
|
||||
json!(ApiError {
|
||||
error: "error_msg".to_owned(),
|
||||
error_code: 1,
|
||||
}));
|
||||
});
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
// Add some pending appointments and try again (with an unreachable tower).
|
||||
let appointment = generate_random_appointment(None);
|
||||
|
|
@ -1538,8 +1600,6 @@ mod tests {
|
|||
);
|
||||
let r = retrier.run().await;
|
||||
|
||||
assert_eq!(r, Ok(()));
|
||||
api_mock.assert();
|
||||
assert!(wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
|
|
@ -1548,6 +1608,8 @@ mod tests {
|
|||
.unwrap()
|
||||
.invalid_appointments
|
||||
.contains(&appointment.locator));
|
||||
assert!(r.is_ok());
|
||||
api_mock.assert_async().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -1558,14 +1620,13 @@ mod tests {
|
|||
let wt_client = Arc::new(Mutex::new(
|
||||
WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await,
|
||||
));
|
||||
let server = MockServer::start();
|
||||
|
||||
// The tower we'd like to retry sending appointments to has to exist within the plugin
|
||||
let receipt = get_random_registration_receipt();
|
||||
wt_client
|
||||
.lock()
|
||||
.unwrap()
|
||||
.add_update_tower(tower_id, &server.base_url(), &receipt)
|
||||
.add_update_tower(tower_id, "http://tower.adrress", &receipt)
|
||||
.unwrap();
|
||||
|
||||
// Remove the tower (to simulate it has been abandoned)
|
||||
|
|
|
|||
|
|
@ -95,17 +95,17 @@ impl WTClient {
|
|||
});
|
||||
|
||||
let dbm = DBM::new(&data_dir.join("watchtowers_db.sql3")).unwrap();
|
||||
let (user_sk, user_id) = match dbm.load_client_key() {
|
||||
Ok(sk) => (
|
||||
|
||||
let (user_sk, user_id) = if let Some(sk) = dbm.load_client_key() {
|
||||
(
|
||||
sk,
|
||||
UserId(PublicKey::from_secret_key(&Secp256k1::new(), &sk)),
|
||||
),
|
||||
Err(_) => {
|
||||
log::info!("Watchtower client keys not found. Creating a fresh set");
|
||||
let (sk, pk) = cryptography::get_random_keypair();
|
||||
dbm.store_client_key(&sk).unwrap();
|
||||
(sk, UserId(pk))
|
||||
}
|
||||
)
|
||||
} else {
|
||||
log::info!("Watchtower client keys not found. Creating a fresh set");
|
||||
let (sk, pk) = cryptography::get_random_keypair();
|
||||
dbm.store_client_key(&sk).unwrap();
|
||||
(sk, UserId(pk))
|
||||
};
|
||||
|
||||
let towers = dbm.load_towers();
|
||||
|
|
@ -180,15 +180,12 @@ impl WTClient {
|
|||
}
|
||||
|
||||
/// Gets the latest registration receipt of a given tower.
|
||||
pub fn get_registration_receipt(
|
||||
&self,
|
||||
tower_id: TowerId,
|
||||
) -> Result<RegistrationReceipt, DBError> {
|
||||
pub fn get_registration_receipt(&self, tower_id: TowerId) -> Option<RegistrationReceipt> {
|
||||
self.dbm.load_registration_receipt(tower_id, self.user_id)
|
||||
}
|
||||
|
||||
/// Loads a tower record from the database.
|
||||
pub fn load_tower_info(&self, tower_id: TowerId) -> Result<TowerInfo, DBError> {
|
||||
pub fn load_tower_info(&self, tower_id: TowerId) -> Option<TowerInfo> {
|
||||
self.dbm.load_tower_record(tower_id)
|
||||
}
|
||||
|
||||
|
|
@ -240,7 +237,7 @@ impl WTClient {
|
|||
&self,
|
||||
tower_id: TowerId,
|
||||
locator: Locator,
|
||||
) -> Result<AppointmentReceipt, DBError> {
|
||||
) -> Option<AppointmentReceipt> {
|
||||
self.dbm.load_appointment_receipt(tower_id, locator)
|
||||
}
|
||||
|
||||
|
|
@ -810,10 +807,7 @@ mod tests {
|
|||
|
||||
// Remove the tower and check it is not there anymore
|
||||
wt_client.remove_tower(tower_id).unwrap();
|
||||
assert!(matches!(
|
||||
wt_client.load_tower_info(tower_id),
|
||||
Err(DBError::NotFound)
|
||||
));
|
||||
assert!(wt_client.load_tower_info(tower_id).is_none());
|
||||
assert!(!wt_client.towers.contains_key(&tower_id));
|
||||
|
||||
// Try again but this time with an associated appointment to check that it also gets removed
|
||||
|
|
@ -836,10 +830,7 @@ mod tests {
|
|||
|
||||
// Remove and check both the tower and the appointment
|
||||
wt_client.remove_tower(tower_id).unwrap();
|
||||
assert!(matches!(
|
||||
wt_client.load_tower_info(tower_id),
|
||||
Err(DBError::NotFound)
|
||||
));
|
||||
assert!(wt_client.load_tower_info(tower_id).is_none());
|
||||
assert!(!wt_client.towers.contains_key(&tower_id));
|
||||
assert!(!wt_client.dbm.appointment_receipt_exists(locator, tower_id));
|
||||
}
|
||||
|
|
@ -890,10 +881,7 @@ mod tests {
|
|||
|
||||
// Remove tower1 and check that the appointment receipt can still be found for tower2
|
||||
wt_client.remove_tower(tower1_id).unwrap();
|
||||
assert!(matches!(
|
||||
wt_client.load_tower_info(tower1_id),
|
||||
Err(DBError::NotFound)
|
||||
));
|
||||
assert!(wt_client.load_tower_info(tower1_id).is_none());
|
||||
|
||||
assert!(!wt_client.dbm.appointment_receipt_exists(locator, tower1_id));
|
||||
assert!(wt_client.dbm.appointment_receipt_exists(locator, tower2_id));
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from pathlib import Path
|
|||
import subprocess
|
||||
|
||||
from pyln.testing.fixtures import * # noqa: F401,F403
|
||||
from pyln.testing.utils import DEVELOPER, BITCOIND_CONFIG, TailableProc
|
||||
from pyln.testing.utils import BITCOIND_CONFIG, TailableProc
|
||||
|
||||
WT_PLUGIN = Path("~/.cargo/bin/watchtower-client").expanduser()
|
||||
TEOSD_CONFIG = {
|
||||
|
|
@ -116,19 +116,6 @@ def pytest_runtest_makereport(item, call):
|
|||
setattr(item, "rep_" + rep.when, rep)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line("markers", "developer: only run when developer is flagged on")
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
for mark in item.iter_markers(name="developer"):
|
||||
if not DEVELOPER:
|
||||
if len(mark.args):
|
||||
pytest.skip("!DEVELOPER: {}".format(mark.args[0]))
|
||||
else:
|
||||
pytest.skip("!DEVELOPER: Requires DEVELOPER=1")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
def log_name(request):
|
||||
# Here logging is used, you can use whatever you want to use for logs
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ black = "^22.6.0"
|
|||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^7.1.2"
|
||||
pytest-timeout = "^2.1.0"
|
||||
pyln-testing = "^0.12.1"
|
||||
pyln-testing = "^24.2.1"
|
||||
pyln-client = "^24.2.1"
|
||||
|
||||
|
||||
[build-system]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import pytest
|
||||
from pyln.client import RpcError
|
||||
from conftest import WT_PLUGIN
|
||||
|
||||
|
||||
|
|
@ -16,7 +15,6 @@ def change_endianness(x):
|
|||
return b[::-1].hex()
|
||||
|
||||
|
||||
@pytest.mark.developer("Requires dev_sign_last_tx")
|
||||
def test_watchtower(node_factory, bitcoind, teosd):
|
||||
"""
|
||||
Test watchtower hook.
|
||||
|
|
@ -27,7 +25,13 @@ def test_watchtower(node_factory, bitcoind, teosd):
|
|||
commitment transaction.
|
||||
"""
|
||||
|
||||
l1, l2 = node_factory.line_graph(2, opts=[{"allow_broken_log": True}, {"plugin": WT_PLUGIN}])
|
||||
l1, l2 = node_factory.line_graph(
|
||||
2,
|
||||
opts=[
|
||||
{"broken_log": r"Could not find resolution for output [0-9]?: did \*we\* cheat\?"},
|
||||
{"plugin": WT_PLUGIN},
|
||||
],
|
||||
)
|
||||
|
||||
# We need to register l2 with the tower
|
||||
tower_id = teosd.cli.gettowerinfo()["tower_id"]
|
||||
|
|
@ -48,7 +52,7 @@ def test_watchtower(node_factory, bitcoind, teosd):
|
|||
locator = change_endianness(dispute_txid[32:])
|
||||
|
||||
# Make sure l2's normal penalty_tx doesn't reach the network
|
||||
l2.daemon.rpcproxy.mock_rpc("sendrawtransaction", lambda: None)
|
||||
l2.daemon.rpcproxy.mock_rpc("sendrawtransaction", lambda _: {"result": None, "error": None, "id": "pytest"})
|
||||
l2.start()
|
||||
|
||||
# The tower will react once the dispute gets confirmed. For now it is still watching for it
|
||||
|
|
@ -60,7 +64,7 @@ def test_watchtower(node_factory, bitcoind, teosd):
|
|||
penalty_txid = bitcoind.rpc.getrawmempool()[0]
|
||||
|
||||
# The channel still exists between the two peers, but it's on chain
|
||||
assert l1.rpc.listpeers()["peers"][0]["channels"][0]["state"] == "ONCHAIN"
|
||||
assert l1.rpc.listpeerchannels()["channels"][0]["state"] == "ONCHAIN"
|
||||
assert l2.rpc.getappointment(tower_id, locator)["status"] == "dispute_responded"
|
||||
|
||||
# Generate blocks until the penalty gets irrevocably resolved
|
||||
|
|
@ -90,7 +94,6 @@ def test_unreachable_watchtower(node_factory, bitcoind, teosd):
|
|||
{},
|
||||
{
|
||||
"plugin": WT_PLUGIN,
|
||||
"allow_broken_log": True,
|
||||
"dev-watchtower-max-retry-interval": max_interval_time,
|
||||
},
|
||||
],
|
||||
|
|
@ -123,7 +126,7 @@ def test_auto_retry_watchtower(node_factory, bitcoind, teosd):
|
|||
{},
|
||||
{
|
||||
"plugin": WT_PLUGIN,
|
||||
"allow_broken_log": True,
|
||||
"broken_log": r"plugin-watchtower-client: Data was send to an idle retrier. This should have never happened. Please report!.*",
|
||||
"watchtower-max-retry-time": 1,
|
||||
"watchtower-auto-retry-delay": 1,
|
||||
},
|
||||
|
|
@ -141,7 +144,7 @@ def test_auto_retry_watchtower(node_factory, bitcoind, teosd):
|
|||
l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"])
|
||||
|
||||
# Wait until the tower has been flagged as unreachable
|
||||
l2.daemon.wait_for_log(f"Starting to idle")
|
||||
l2.daemon.wait_for_log("Starting to idle")
|
||||
assert l2.rpc.gettowerinfo(tower_id)["status"] == "unreachable"
|
||||
assert l2.rpc.gettowerinfo(tower_id)["pending_appointments"]
|
||||
|
||||
|
|
@ -161,7 +164,6 @@ def test_manually_retry_watchtower(node_factory, bitcoind, teosd):
|
|||
{},
|
||||
{
|
||||
"plugin": WT_PLUGIN,
|
||||
"allow_broken_log": True,
|
||||
"watchtower-max-retry-time": 0,
|
||||
},
|
||||
],
|
||||
|
|
@ -178,7 +180,7 @@ def test_manually_retry_watchtower(node_factory, bitcoind, teosd):
|
|||
l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"])
|
||||
|
||||
# Wait until the tower has been flagged as unreachable
|
||||
l2.daemon.wait_for_log(f"Starting to idle")
|
||||
l2.daemon.wait_for_log("Starting to idle")
|
||||
assert l2.rpc.gettowerinfo(tower_id)["status"] == "unreachable"
|
||||
assert l2.rpc.gettowerinfo(tower_id)["pending_appointments"]
|
||||
|
||||
|
|
@ -193,7 +195,7 @@ def test_manually_retry_watchtower(node_factory, bitcoind, teosd):
|
|||
|
||||
|
||||
def test_misbehaving_watchtower(node_factory, bitcoind, teosd, directory):
|
||||
l1, l2 = node_factory.line_graph(2, opts=[{}, {"plugin": WT_PLUGIN, "allow_broken_log": True}])
|
||||
l1, l2 = node_factory.line_graph(2, opts=[{}, {"plugin": WT_PLUGIN}])
|
||||
|
||||
# We need to register l2 with the tower
|
||||
tower_id = teosd.cli.gettowerinfo()["tower_id"]
|
||||
|
|
@ -210,7 +212,7 @@ def test_misbehaving_watchtower(node_factory, bitcoind, teosd, directory):
|
|||
|
||||
|
||||
def test_get_appointment(node_factory, bitcoind, teosd, directory):
|
||||
l1, l2 = node_factory.line_graph(2, opts=[{"allow_broken_log": True}, {"plugin": WT_PLUGIN}])
|
||||
l1, l2 = node_factory.line_graph(2, opts=[{}, {"plugin": WT_PLUGIN}])
|
||||
|
||||
# We need to register l2 with the tower
|
||||
tower_id = teosd.cli.gettowerinfo()["tower_id"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue