Compare commits

..

No commits in common. "master" and "v0.1.1" have entirely different histories.

67 changed files with 4907 additions and 12385 deletions

View file

@ -1,10 +1,6 @@
name: Continuous Integration Checks
on:
push:
branches:
- master
pull_request:
on: [push, pull_request]
jobs:
build:
@ -20,15 +16,13 @@ jobs:
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout source code
uses: actions/checkout@v4
uses: actions/checkout@v2
- name: Install Rust ${{ matrix.toolchain }} toolchain
uses: dtolnay/rust-toolchain@master
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.toolchain }}
- name: Install Protoc
uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
override: true
profile: minimal
- name: Build on Rust ${{ matrix.toolchain }}
run: |
cargo build ${{ matrix.arguments }} --verbose --color always
@ -40,43 +34,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
uses: actions/checkout@v2
- name: Install Rust stable toolchain
uses: dtolnay/rust-toolchain@master
uses: actions-rs/toolchain@v1
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
python-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Run black
uses: psf/black@stable
with:
src: "./watchtower-plugin/tests"
options: "--check -l 120"
check-flake:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Check Nix flake inputs
uses: DeterminateSystems/flake-checker-action@v12
- name: Install Nix
uses: cachix/install-nix-action@v31
- name: Check flake
run: nix flake check
cargo clippy --all-features --all-targets --color always -- --deny warnings

View file

@ -1,73 +1,39 @@
name: CI tests for CLN watchtower-plugin
on:
push:
branches:
- master
pull_request:
on: [push, pull_request]
env:
bitcoind_version: "27.0"
cln_version: "24.11.1"
bitcoind_version: 0.20.1
cln_version: 0.11.0.1
jobs:
check-cln-cache:
runs-on: ubuntu-latest
outputs:
cache-hit: ${{ steps.cache.outputs.cache-hit }}
steps:
- name: Check CLN cache
id: cache
uses: actions/cache@v4
with:
path: lightning
key: ${{ runner.os }}-build-cache-cln-dev-v${{ env.cln_version }}
cache-cln:
runs-on: ubuntu-latest
needs: check-cln-cache
if: ${{ needs.check-cln-cache.outputs.cache-hit != 'true' }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.9'
check-latest: true
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- name: Create CLN cache
uses: actions/cache@v4
id: cache-cln
uses: actions/cache@v3
env:
cache-name: cache-cln-dev
with:
path: lightning
key: ${{ runner.os }}-build-cache-cln-dev-v${{ env.cln_version }}
key: ${{ runner.os }}-build-${{ env.cache-name }}-v${{ env.cln_version }}
- name: Compile CLN
env:
PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring
if: ${{ steps.cache-cln.outputs.cache-hit != 'true' }}
run: |
sudo apt-get update && sudo apt-get install -y gettext
sudo apt-get update && sudo apt-get install gettext
git clone https://github.com/ElementsProject/lightning.git && cd lightning && git checkout v${{ env.cln_version }}
pip install --user poetry && poetry install
./configure && poetry run make -j 8
./configure --enable-developer && poetry run make
cln-plugin:
needs: cache-cln
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.9'
check-latest: true
- uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
components: rustfmt, clippy
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- name: Install bitcoind
run: |
wget https://bitcoincore.org/bin/bitcoin-core-${{ env.bitcoind_version }}/bitcoin-${{ env.bitcoind_version }}-x86_64-linux-gnu.tar.gz
@ -75,22 +41,24 @@ jobs:
ln -s $(pwd)/bitcoin-${{ env.bitcoind_version }}/bin/bitcoin* /usr/local/bin
- name: Load CLN cache
id: cache-cln
uses: actions/cache@v4
uses: actions/cache@v3
env:
cache-name: cache-cln-dev
with:
path: lightning
key: ${{ runner.os }}-build-cache-cln-dev-v${{ env.cln_version }}
key: ${{ runner.os }}-build-${{ env.cache-name }}-v${{ env.cln_version }}
- name: Link CLN
run: |
cd lightning && sudo PATH=$PATH make install
cd lightning && sudo make install
- name: Install teos and the plugin
run: |
cargo install --locked --path teos
cargo install --locked --path watchtower-plugin
cargo install --path teos
cargo install --path watchtower-plugin
- name: Add test dependencies
run: |
cd watchtower-plugin/tests
pip install --user poetry && poetry install --no-root
pip install --user poetry && poetry install
- name: Run tests
run: |
cd watchtower-plugin/tests
VALGRIND=0 SLOW_MACHINE=1 poetry run pytest test.py --log-cli-level=INFO -s
DEVELOPER=1 SLOW_MACHINE=1 poetry run pytest test.py -s

4
.gitignore vendored
View file

@ -1,5 +1,5 @@
target
__pycache__
Cargo.lock
.vscode
.idea
result
.idea

View file

@ -72,35 +72,6 @@ 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.
## 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
## 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).

4100
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,7 @@
[workspace]
resolver = "2"
members = [
"teos",
"teos-common",
"watchtower-plugin"
]
]

View file

@ -6,7 +6,7 @@
- `bitcoind`
### Minimum Supported Rust Version (MSRV)
Refer to [toolchain](./rust-toolchain.toml)
FIXME: Define MSRV
### Installing Rust
Refer to [rust-lang.org](https://www.rust-lang.org/tools/install).
@ -19,6 +19,7 @@ You can get Bitcoin Core from [bitcoincore.org](https://bitcoincore.org/en/downl
Bitcoin needs to be running with the following options enabled:
- `txindex` to be able to look for non-wallet transactions
- `server` to run rpc commands
Here's an example of a `bitcoin.conf` you can use for mainnet. **DO NOT USE THE PROVIDED RPC USER AND PASSWORD.**
@ -30,6 +31,9 @@ rpcuser=user
rpcpassword=passwd
rpcservertimeout=600
# [blockchain]
txindex=1
# [others]
daemon=1
debug=1

View file

@ -3,9 +3,9 @@
The tower can be installed and tested using cargo:
```
git clone https://github.com/talaia-labs/rust-teos.git
git clone https://github.com/sr-gi/rust-teos.git
cd rust-teos
cargo install --locked --path teos
cargo install --path teos
```
You can run tests with:
@ -14,8 +14,4 @@ You can run tests with:
cargo test
```
Please refer to the cargo documentation for more detailed instructions.
# Systemd setup for backend
Refer to [contrib](contrib/init/README.md) for a detailed explanation of how to set up your systemd service for `teosd`.
Please refer to the cargo documentation for more detailed instructions.

View file

@ -1,12 +1,9 @@
**THIS IS CURRENTLY WIP**
# The Eye of Satoshi (rust-teos)
The Eye of Satoshi is a Lightning watchtower compliant with [BOLT13](https://github.com/sr-gi/bolt13), written in Rust.
[![discord](https://img.shields.io/discord/991334710611550208?logo=discord&style=plastic)](https://discord.gg/EyVbrNMDUP)
[![build](https://img.shields.io/github/actions/workflow/status/talaia-labs/rust-teos/build.yaml?logo=github&style=plastic)](https://github.com/talaia-labs/rust-teos/actions/workflows/build.yaml)
[![release](https://img.shields.io/github/v/release/talaia-labs/rust-teos?style=plastic)](https://github.com/talaia-labs/rust-teos/releases/latest)
`rust-teos` consists of two main crates:
- `teos`: including the tower's main functionality (server-side) and a CLI. Compiling this crate will generate two binaries: `teosd` and `teos-cli`.
@ -23,8 +20,6 @@ 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:
@ -71,13 +66,13 @@ For regtest, it should look like:
btc_network = regtest
```
### Running `teosd` with Tor
### Running `teosd` with tor
This requires a Tor daemon running on the same machine as `teosd` and a control port open on that daemon.
This requires a tor daemon running on the same machine as `teosd` and a control port open on that daemon.
Download Tor from the [torproject site](https://www.torproject.org/download/).
Download tor from the [torproject site](https://www.torproject.org/download/).
To open Tor's control port, you add the following to the Tor config file ([source](https://2019.www.torproject.org/docs/faq.html.en#torrc)):
To open tor's control port, you add the following to the tor config file ([source](https://2019.www.torproject.org/docs/faq.html.en#torrc)):
```
## The port on which Tor will listen for local connections from Tor
@ -90,7 +85,7 @@ CookieAuthentication 1
CookieAuthFileGroupReadable 1
```
Once the Tor daemon is running, and the control port is open, make sure to enable `--torsupport` when running `teosd`.
Once the tor daemon is running, and the control port is open, make sure to enable the `tor_support` flag `teosd`.
### Tower id and signing key

View file

@ -1,10 +0,0 @@
---
plugin:
name: rust-teos
version: 0.2.0
lang: rust
install: |
cargo build --release --locked --package watchtower-plugin
cp target/release/watchtower-client .
cargo clean
main: watchtower-client

View file

@ -1,42 +0,0 @@
**This document guides you into how to set-up a systemd service to run `teosd`.**
Since the teos service requires bitcoin to run, it is strongly recommended to also create a [system service for bitcoin](https://github.com/bitcoin/bitcoin/blob/master/contrib/init/bitcoind.service).
Once you have set the bitcoin service, proceed to copy [teosd.service](teosd.service) to the systemd folder, that is, if running from this folder:
```
cp teosd.service /etc/systemd/system
```
You can also create a file called `teosd.service` in the systemd folder and copy the content of [teosd.service](teosd.service) to it:
```
sudo vim /etc/systemd/system/teosd.service
```
Notice the provided service file is using `teos` both as user and group for the service, so you may want to update that if that is not the configuration you are intending to use. Here are the lines to be updated:
```
[Service]
ExecStart=/home/<user>/.cargo/bin/teosd
SyslogIdentifier=<user>
# Directory creation and permissions
####################################
User=<user>
Group=<group>
```
The next step is enabling the service. You can do so by running:
```
sudo systemctl enable teosd.service
```
Finally, you can start the service by running:
```
sudo systemctl start teosd.service
```
From that point on, the tower will be run every time your system is turned on, and restarted if needed.

View file

@ -1,46 +0,0 @@
[Unit]
Description=The Eye of Satoshi daemon
Requires=bitcoind.service
After=bitcoind.service
Wants=network-online.target
After=network-online.target
[Service]
ExecStart=/home/teos/.cargo/bin/teosd
StandardOutput=journal
StandardError=journal
SyslogIdentifier=teos
# Process management
####################
Type=simple
Restart=on-failure
TimeoutSec=300
RestartSec=60
# Directory creation and permissions
####################################
User=teos
Group=teos
# Hardening measures
####################
# Provide a private /tmp and /var/tmp.
PrivateTmp=true
# Mount /usr, /boot/ and /etc read-only for the process.
ProtectSystem=full
# Disallow the process and all of its children to gain
# new privileges through execve().
NoNewPrivileges=true
# Use a new /dev namespace only populated with API pseudo devices
# such as /dev/null, /dev/zero and /dev/random.
PrivateDevices=true
# Deny the creation of writable and executable memory mappings.
MemoryDenyWriteExecute=true
[Install]
WantedBy=multi-user.target

View file

@ -1,49 +0,0 @@
# Use the rust image as the base image for the build stage
FROM rust:latest AS builder
# Copy the rust-teos source code
COPY . /tmp/rust-teos
# Install the dependencies required for building rust-teos
RUN apt-get update\
&& apt-get -y --no-install-recommends install libffi-dev libssl-dev musl-tools pkg-config
RUN cd /tmp/rust-teos \
&& rustup target add x86_64-unknown-linux-musl \
# Rustfmt is needed to format the grpc stubs generated by tonic
&& rustup component add rustfmt \
# Cross compile with musl as the target, so teosd can run on alpine
&& RUSTFLAGS='-C target-feature=+crt-static' cargo build --manifest-path=teos/Cargo.toml --locked --release --target x86_64-unknown-linux-musl
# Use a new stage with a smaller base image to reduce image size
FROM alpine:latest
RUN apk update && apk upgrade
# UID and GID for the teosd user
ENV TEOS_UID=1001 TEOS_GID=1001
# Copy the teos binaries from the build stage to the new stage
COPY --from=builder \
/tmp/rust-teos/target/x86_64-unknown-linux-musl/release/teosd \
/tmp/rust-teos/target/x86_64-unknown-linux-musl/release/teos-cli /usr/local/bin/
# Copy the entrypoint script to the container
COPY docker/entrypoint.sh /entrypoint.sh
# Set the entrypoint script as executable and add running user
RUN chmod +x /entrypoint.sh \
&& addgroup -g ${TEOS_GID} -S teos \
&& adduser -S -G teos -u ${TEOS_UID} teos
# Expose the default port used by teosd
EXPOSE 9814/tcp
# Switch user so that we don't run stuff as root
USER teos
# Create the teos data directory
RUN mkdir /home/teos/.teos
# Start teosd when the container starts
ENTRYPOINT [ "/entrypoint.sh" ]

View file

@ -1,113 +0,0 @@
## Running `teosd` in a docker container
A `teos` image can be built from the Dockerfile located in `docker`. You can create the image by running:
cd rust-teos
docker build -f docker/Dockerfile -t teos .
Then we can create a container by running:
docker run -it teos
One way to feed `teos` custom config options is to set environment variables:
docker run -it -e <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`.

View file

@ -1,66 +0,0 @@
#!/bin/sh
# Define the start command
START_COMMAND="teosd"
# Set the API bind address
if [[ ! -z ${API_BIND} ]]; then
START_COMMAND="$START_COMMAND --apibind $API_BIND"
fi
# Set the API port
if [[ ! -z ${API_PORT} ]]; then
START_COMMAND="$START_COMMAND --apiport $API_PORT"
fi
# Set the RPC bind address
if [[ ! -z ${RPC_BIND} ]]; then
START_COMMAND="$START_COMMAND --rpcbind $RPC_BIND"
fi
# Set the RPC port
if [[ ! -z ${RPC_PORT} ]]; then
START_COMMAND="$START_COMMAND --rpcport $RPC_PORT"
fi
# Set the Bitcoin network
if [[ ! -z ${BTC_NETWORK} ]]; then
START_COMMAND="$START_COMMAND --btcnetwork $BTC_NETWORK"
fi
# Set the Bitcoin RPC credentials
if [[ ! -z ${BTC_RPC_USER} ]]; then
START_COMMAND="$START_COMMAND --btcrpcuser $BTC_RPC_USER"
fi
if [[ ! -z ${BTC_RPC_PASSWORD} ]]; then
START_COMMAND="$START_COMMAND --btcrpcpassword $BTC_RPC_PASSWORD"
fi
# Set the Bitcoin RPC connection details
if [[ ! -z ${BTC_RPC_CONNECT} ]]; then
START_COMMAND="$START_COMMAND --btcrpcconnect $BTC_RPC_CONNECT"
fi
if [[ ! -z ${BTC_RPC_PORT} ]]; then
START_COMMAND="$START_COMMAND --btcrpcport $BTC_RPC_PORT"
fi
if [ "${DEBUG}" == "true" ]; then
START_COMMAND="$START_COMMAND --debug"
fi
if [ "${DEPS_DEBUG}" == "true" ]; then
START_COMMAND="$START_COMMAND --depsdebug"
fi
if [ "${OVERWRITE_KEY}" == "true" ]; then
START_COMMAND="$START_COMMAND --overwritekey"
fi
if [ "${FORCE_UPDATE}" == "true" ]; then
START_COMMAND="$START_COMMAND --forceupdate"
fi
# Start the TEOS daemon
$START_COMMAND

116
flake.lock generated
View file

@ -1,116 +0,0 @@
{
"nodes": {
"crane": {
"locked": {
"lastModified": 1758758545,
"narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=",
"owner": "ipetkov",
"repo": "crane",
"rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1758782550,
"narHash": "sha256-olCvyP5r6+HQTl2EUudtjlA5UammsBpkzAl0l9+utZc=",
"owner": "nix-community",
"repo": "fenix",
"rev": "32f4e350c03cc5762be811e9c700e8696cd13c02",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1758589230,
"narHash": "sha256-zMTCFGe8aVGTEr2RqUi/QzC1nOIQ0N1HRsbqB4f646k=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d1d883129b193f0b495d75c148c2c3a7d95789a0",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"crane": "crane",
"fenix": "fenix",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1758620797,
"narHash": "sha256-Ly4rHgrixFMBnkbMursVt74mxnntnE6yVdF5QellJ+A=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "905641f3520230ad6ef421bcf5da9c6b49f2479b",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

114
flake.nix
View file

@ -1,114 +0,0 @@
{
description = "Build teos (The Eye of Satoshi) server and plugin";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
crane.url = "github:ipetkov/crane";
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
nixpkgs,
crane,
fenix,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs) lib;
craneLib = (crane.mkLib pkgs).overrideToolchain fenix.packages.${system}.stable.minimalToolchain;
env = {
PROTOC = "${pkgs.protobuf}/bin/protoc";
PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig";
LD_LIBRARY_PATH = lib.makeLibraryPath [ pkgs.openssl.out ];
};
commonArgs = {
inherit env;
strictDeps = true;
nativeBuildInputs = [
pkgs.pkg-config
pkgs.rustfmt # needed for tonic build
pkgs.cacert
pkgs.openssl.dev
];
buildInputs =
[ ]
++ lib.optionals pkgs.stdenv.isDarwin [
# Additional darwin specific inputs can be set here
pkgs.libiconv
];
};
fileSetForCrate =
crate:
lib.fileset.toSource {
root = ./.;
fileset = lib.fileset.unions [
./Cargo.toml
./Cargo.lock
./teos-common
./teos
./watchtower-plugin
crate
];
};
plugin = craneLib.buildPackage (
commonArgs
// {
pname = "watchtower-plugin";
cargoExtraArgs = "-p watchtower-plugin";
src = fileSetForCrate ./watchtower-plugin;
inherit (craneLib.crateNameFromCargoToml { cargoToml = ./watchtower-plugin/Cargo.toml; }) version;
}
);
teos = craneLib.buildPackage (
commonArgs
// {
pname = "teos";
cargoExtraArgs = "-p teos";
src = fileSetForCrate ./teos;
inherit (craneLib.crateNameFromCargoToml { cargoToml = ./teos/Cargo.toml; }) version;
}
);
in
{
packages = {
inherit plugin teos;
default = teos;
};
apps = {
plugin = flake-utils.lib.mkApp { drv = plugin; };
teos = flake-utils.lib.mkApp { drv = teos; };
};
formatter = pkgs.nixfmt-tree;
checks = {
inherit teos plugin;
};
devShells.default = craneLib.devShell {
inherit env;
packages = commonArgs.buildInputs ++ commonArgs.nativeBuildInputs;
};
}
);
}

View file

@ -1,6 +0,0 @@
[toolchain]
channel = "1.81.0"
components = [
"rustfmt",
"clippy",
]

View file

@ -1,27 +1,27 @@
[package]
name = "teos-common"
version = "0.2.0"
version = "0.0.1"
authors = ["Sergi Delgado Segura <sergi.delgado.s@gmail.com>"]
edition = "2021"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# General
hex = { version = "0.4.3", features = [ "serde" ] }
prost = "0.12"
prost = "0.9"
rusqlite = { version = "0.26.0", features = [ "bundled", "limits" ] }
serde = "1.0.130"
serde_json = "1.0"
tonic = "0.11"
tonic = "0.6"
# Crypto
rand = "0.8.4"
chacha20poly1305 = "0.8.0"
# Bitcoin and Lightning
bitcoin = { version = "0.32.0", features = [ "serde" ] }
lightning = "0.1.0"
bitcoin = { version = "0.27", features = [ "use-serde" ] }
lightning = "0.0.105"
[build-dependencies]
tonic-build = "0.11"
tonic-build = "0.6"

View file

@ -6,14 +6,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.field_attribute("appointment_data", "#[serde(rename = \"appointment\")]")
.field_attribute("user_id", "#[serde(with = \"hex::serde\")]")
.field_attribute("locator", "#[serde(with = \"hex::serde\")]")
.field_attribute(
"locators",
"#[serde(with = \"crate::ser::serde_vec_bytes\")]",
)
.field_attribute("encrypted_blob", "#[serde(with = \"hex::serde\")]")
.field_attribute("dispute_txid", "#[serde(with = \"crate::ser::serde_be\")]")
.field_attribute("penalty_txid", "#[serde(with = \"crate::ser::serde_be\")]")
.field_attribute("penalty_rawtx", "#[serde(with = \"hex::serde\")]")
.field_attribute(
"GetAppointmentResponse.status",
"#[serde(with = \"crate::ser::serde_status\")]",

View file

@ -1,6 +1,8 @@
//! Logic related to appointments shared between users and the towers.
use hex;
use serde::{Deserialize, Serialize};
use std::array::TryFromSliceError;
use std::{convert::TryInto, fmt};
@ -95,7 +97,7 @@ impl std::str::FromStr for AppointmentStatus {
"being_watched" => Ok(AppointmentStatus::BeingWatched),
"dispute_responded" => Ok(AppointmentStatus::DisputeResponded),
"not_found" => Ok(AppointmentStatus::NotFound),
_ => Err(format!("Unknown status: {s}")),
_ => Err(format!("Unknown status: {}", s)),
}
}
}
@ -107,7 +109,7 @@ impl fmt::Display for AppointmentStatus {
AppointmentStatus::DisputeResponded => "dispute_responded",
AppointmentStatus::NotFound => "not_found",
};
write!(f, "{s}")
write!(f, "{}", s)
}
}

View file

@ -1,9 +1,10 @@
//! 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};
@ -19,7 +20,7 @@ pub enum DecryptingError {
}
/// Shadows [message_signing::sign].
pub fn sign(msg: &[u8], sk: &SecretKey) -> String {
pub fn sign(msg: &[u8], sk: &SecretKey) -> Result<String, Error> {
message_signing::sign(msg, sk)
}
@ -46,8 +47,8 @@ pub fn encrypt(
) -> Result<Vec<u8>, chacha20poly1305::aead::Error> {
// Defaults is [0; 12]
let nonce = Nonce::default();
let k = sha256::Hash::hash(secret.as_byte_array());
let key = Key::from_slice(k.as_byte_array());
let _k = sha256::Hash::hash(secret);
let key = Key::from_slice(&_k);
let cypher = ChaCha20Poly1305::new(key);
cypher.encrypt(&nonce, consensus::serialize(message).as_ref())
@ -63,8 +64,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.as_byte_array());
let key = Key::from_slice(k.as_byte_array());
let _k = sha256::Hash::hash(secret);
let key = Key::from_slice(&_k);
let cypher = ChaCha20Poly1305::new(key);
@ -80,13 +81,17 @@ 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);
(&mut rng).sample_iter(uniform_u8).take(size).collect()
let v: Vec<u8> = (&mut rng).sample_iter(uniform_u8).take(size).collect();
v
}
/// Gets a key pair generated in a pseudorandom way.
pub fn get_random_keypair() -> (SecretKey, PublicKey) {
let raw_sk = get_random_bytes(32);
loop {
if let Ok(sk) = SecretKey::from_slice(&get_random_bytes(32)) {
if let Ok(sk) = SecretKey::from_slice(&raw_sk) {
return (sk, PublicKey::from_secret_key(&Secp256k1::new(), &sk));
}
}
@ -94,8 +99,6 @@ 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;
@ -109,8 +112,8 @@ mod tests {
let expected_enc_blob = Vec::from_hex(ENC_BLOB).unwrap();
let tx_bytes = Vec::from_hex(HEX_TX).unwrap();
let tx: Transaction = consensus::deserialize(&tx_bytes).unwrap();
let txid = bitcoin::Txid::from_str(HEX_TXID).unwrap();
let tx = consensus::deserialize(&tx_bytes).unwrap();
let txid = Txid::from_hex(HEX_TXID).unwrap();
assert_eq!(encrypt(&tx, &txid).unwrap(), expected_enc_blob);
}
@ -119,7 +122,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 = bitcoin::Txid::from_str(HEX_TXID).unwrap();
let txid = Txid::from_hex(HEX_TXID).unwrap();
assert_eq!(decrypt(&encrypted_blob, &txid).unwrap(), expected_tx);
}
}

View file

@ -2,8 +2,6 @@
//!
//! Functionality shared between users and towers.
// FIXME: This is a temporary fix. See https://github.com/tokio-rs/prost/issues/661
#[allow(clippy::derive_partial_eq_without_eq)]
pub mod protos {
tonic::include_proto!("common.teos.v2");
}
@ -13,7 +11,6 @@ pub mod constants;
pub mod cryptography;
pub mod dbm;
pub mod errors;
pub mod net;
pub mod receipts;
pub mod ser;
pub mod test_utils;
@ -22,7 +19,6 @@ use std::fmt;
use std::{convert::TryFrom, str::FromStr};
use serde::{Deserialize, Serialize};
use serde_json::json;
use bitcoin::secp256k1::{Error, PublicKey};
@ -75,113 +71,15 @@ impl TryFrom<serde_json::Value> for UserId {
UserId::try_from(a.pop().unwrap())
} else {
Err(format!(
"Unexpected json format. Expected a single parameter. Received: {param_count}"
"Unexpected json format. Expected a single parameter. Received: {}",
param_count
))
}
}
serde_json::Value::Object(mut m) => {
let param_count = m.len();
if param_count > 1 {
Err(format!(
"Unexpected json format. Expected a single parameter. Received: {param_count}"
))
} else {
UserId::try_from(json!(m
.remove("user_id")
.or_else(|| m.remove("tower_id"))
.ok_or("user_id or tower_id not found")?))
}
}
_ => Err(format!(
"Unexpected request format. Expected: user_id/tower_id. Received: '{value}'"
"Unexpected request format. Expected: user_id/tower_id. Received: '{}'",
value
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::collections::HashMap;
use crate::test_utils::get_random_user_id;
#[test]
fn try_from_json_string() {
let user_id = get_random_user_id();
assert_eq!(UserId::try_from(json!(user_id.to_string())), Ok(user_id));
}
#[test]
fn try_from_json_wrong_string() {
let user_id = "not_a_user_id";
assert!(matches!(
UserId::try_from(json!(user_id.to_string())),
Err(..)
));
}
#[test]
fn try_from_json_array() {
let user_id = get_random_user_id();
assert_eq!(UserId::try_from(json!([user_id.to_string()])), Ok(user_id));
}
#[test]
fn try_from_json_array_empty() {
assert!(matches!(UserId::try_from(json!([])), Err(..)));
}
#[test]
fn try_from_json_array_too_many_elements() {
let user_id = get_random_user_id();
assert!(matches!(
UserId::try_from(json!([user_id.to_string(), user_id.to_string()])),
Err(..)
));
}
#[test]
fn try_from_json_dict() {
let user_id = get_random_user_id();
assert_eq!(
UserId::try_from(json!(HashMap::from([("tower_id", user_id.to_string())]))),
Ok(user_id)
);
assert_eq!(
UserId::try_from(json!(HashMap::from([("user_id", user_id.to_string())]))),
Ok(user_id)
);
}
#[test]
fn try_from_json_empty_dict() {
assert!(matches!(
UserId::try_from(json!(HashMap::<String, serde_json::Value>::new())),
Err(..)
));
}
#[test]
fn try_from_json_wrong_dict() {
let user_id = get_random_user_id();
assert!(matches!(
UserId::try_from(json!(HashMap::from([("random_key", user_id.to_string())]))),
Err(..)
));
}
#[test]
fn try_from_json_dict_too_many_keys() {
let user_id = get_random_user_id();
assert!(matches!(
UserId::try_from(json!(HashMap::from([
("tower_id", user_id.to_string()),
("user_id", user_id.to_string())
]))),
Err(..)
));
}
}

View file

@ -1,29 +0,0 @@
pub enum Endpoint {
Register,
AddAppointment,
GetAppointment,
GetSubscriptionInfo,
Ping,
}
impl std::fmt::Display for Endpoint {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Endpoint::Register => "register",
Endpoint::AddAppointment => "add_appointment",
Endpoint::GetAppointment => "get_appointment",
Endpoint::GetSubscriptionInfo => "get_subscription_info",
Endpoint::Ping => "ping",
}
)
}
}
impl Endpoint {
pub fn path(&self) -> String {
format!("/{self}")
}
}

View file

@ -1,116 +0,0 @@
pub mod http;
use serde::Serialize;
use std::fmt;
/// Represents all types of teos network addresses
#[derive(Clone, Serialize, Debug, PartialEq, Eq)]
pub enum AddressType {
IpV4 = 0,
TorV3 = 1,
}
impl From<i32> for AddressType {
fn from(x: i32) -> Self {
match x {
0 => AddressType::IpV4,
1 => AddressType::TorV3,
x => panic!("Unknown address type {}", x),
}
}
}
impl std::str::FromStr for AddressType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"ipv4" => Ok(AddressType::IpV4),
"torv3" => Ok(AddressType::TorV3),
_ => Err(format!("Unknown type: {s}")),
}
}
}
impl fmt::Display for AddressType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
AddressType::IpV4 => "ipv4",
AddressType::TorV3 => "torv3",
};
write!(f, "{s}")
}
}
impl AddressType {
pub fn get_type(net_addr: &str) -> AddressType {
if net_addr.contains(".onion:") {
AddressType::TorV3
} else {
AddressType::IpV4
}
}
pub fn is_tor(&self) -> bool {
self == &AddressType::TorV3
}
pub fn is_clearnet(&self) -> bool {
self == &AddressType::IpV4
}
}
#[derive(Clone, Serialize, Debug, PartialEq, Eq)]
pub struct NetAddr {
net_addr: String,
#[serde(skip)]
addr_type: AddressType,
}
impl NetAddr {
pub fn new(net_addr: String) -> Self {
NetAddr {
addr_type: AddressType::get_type(&net_addr),
net_addr,
}
}
pub fn net_addr(&self) -> &str {
&self.net_addr
}
pub fn addr_type(&self) -> &AddressType {
&self.addr_type
}
pub fn is_onion(&self) -> bool {
self.addr_type().is_tor()
}
}
#[cfg(test)]
pub mod tests {
use super::*;
pub const TORV3_ADDR: &str =
"recnedb7xfhzjdrcgxongzli3a6qyrv5jwgowoho3v5g3rwk7kkglrid.onion:9814";
pub const IPV4_ADDR: &str = "teos.talaia.watch:9814";
#[test]
fn test_get_type() {
assert_eq!(AddressType::get_type(TORV3_ADDR), AddressType::TorV3);
assert_eq!(AddressType::get_type(IPV4_ADDR), AddressType::IpV4);
}
#[test]
fn test_is_tor() {
assert!(NetAddr::new(TORV3_ADDR.to_owned()).addr_type.is_tor());
assert!(!NetAddr::new(IPV4_ADDR.to_owned()).addr_type.is_tor());
}
#[test]
fn test_is_clearnet() {
assert!(!NetAddr::new(TORV3_ADDR.to_owned()).addr_type.is_clearnet());
assert!(NetAddr::new(IPV4_ADDR.to_owned()).addr_type.is_clearnet());
}
}

View file

@ -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
@ -25,7 +25,7 @@ pub struct RegistrationReceipt {
available_slots: u32,
subscription_start: u32,
subscription_expiry: u32,
#[serde(rename = "subscription_signature")]
#[serde(skip)]
signature: Option<String>,
}
@ -92,7 +92,8 @@ impl RegistrationReceipt {
}
pub fn sign(&mut self, sk: &SecretKey) {
self.signature = Some(cryptography::sign(&self.to_vec(), sk));
// 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());
}
pub fn verify(&self, id: &UserId) -> bool {
@ -152,7 +153,8 @@ impl AppointmentReceipt {
}
pub fn sign(&mut self, sk: &SecretKey) {
self.signature = Some(cryptography::sign(&self.to_vec(), sk));
// 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());
}
pub fn verify(&self, id: &UserId) -> bool {

View file

@ -15,98 +15,9 @@ where
seq.end()
}
pub mod serde_be {
use super::*;
use serde::de::{self, Deserializer};
pub fn serialize<S>(v: &[u8], s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut v = v.to_owned();
v.reverse();
hex::serialize(v, s)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
struct BEVisitor;
impl<'de> de::Visitor<'de> for BEVisitor {
type Value = Vec<u8>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a hex encoded string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
let mut v =
hex::decode(v).map_err(|_| E::custom("cannot deserialize the given value"))?;
v.reverse();
Ok(v)
}
}
deserializer.deserialize_any(BEVisitor)
}
}
pub mod serde_vec_bytes {
use super::*;
use serde::de::{self, Deserializer, SeqAccess};
pub fn serialize<S>(v: &[Vec<u8>], s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut seq = s.serialize_seq(Some(v.len()))?;
for element in v.iter() {
seq.serialize_element(&hex::encode(element))?;
}
seq.end()
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Vec<u8>>, D::Error>
where
D: Deserializer<'de>,
{
struct VecVisitor;
impl<'de> de::Visitor<'de> for VecVisitor {
type Value = Vec<Vec<u8>>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a hex encoded string")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut result = Vec::new();
while let Some(v) = seq.next_element::<String>()? {
result
.push(hex::decode(v).map_err(|_| {
de::Error::custom("cannot deserialize the given value")
})?);
}
Ok(result)
}
}
deserializer.deserialize_any(VecVisitor)
}
}
pub mod serde_status {
use super::*;
use serde::de::{self, Deserializer};
use serde::ser::Serializer;
use std::str::FromStr;
use crate::appointment::AppointmentStatus;

View file

@ -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::{consensus, Amount, ScriptBuf, Transaction, TxOut, Txid};
use bitcoin::Txid;
use crate::appointment::{Appointment, Locator};
use crate::cryptography;
@ -32,12 +32,6 @@ 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,
@ -48,18 +42,7 @@ pub fn generate_random_appointment(dispute_txid: Option<&Txid>) -> Appointment {
};
let tx_bytes = Vec::from_hex(TX_HEX).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 penalty_tx = consensus::deserialize(&tx_bytes).unwrap();
let mut raw_locator: [u8; 16] = cryptography::get_random_bytes(16).try_into().unwrap();
raw_locator.copy_from_slice(&dispute_txid[..16]);

View file

@ -1,9 +1,9 @@
[package]
name = "teos"
version = "0.2.0"
version = "0.0.1"
authors = ["Sergi Delgado Segura <sergi.delgado.s@gmail.com>"]
license = "MIT"
edition = "2021"
edition = "2018"
default-run="teosd"
[[bin]]
@ -19,36 +19,35 @@ path = "src/main.rs"
hex = { version = "0.4.3", features = [ "serde" ] }
home = "0.5.3"
log = "0.4"
prost = "0.12"
rcgen = { version = "0.13.1", features = ["pem", "x509-parser"] }
prost = "0.9"
rcgen = { version = "0.8", 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.11", features = [ "tls", "transport" ] }
tonic = { version = "0.6", features = [ "tls", "transport" ] }
tokio = { version = "1.5", features = [ "rt-multi-thread" ] }
triggered = "0.1.2"
warp = "0.3.5"
warp = "0.3.2"
torut = "0.2.1"
base64 = "0.22.1"
# Bitcoin and Lightning
bitcoin = { version = "0.32.0" }
bitcoincore-rpc = "0.19.0"
lightning = "0.1.0"
lightning-net-tokio = "0.1.0"
lightning-block-sync = { version = "0.1.0", features = [ "rpc-client" ] }
bitcoin = { version = "0.27", features = [ "base64" ] }
bitcoincore-rpc = "0.14.0"
lightning = "0.0.105"
lightning-net-tokio = "0.0.105"
lightning-block-sync = { version = "0.0.105", features = [ "rpc-client" ] }
# Local
teos-common = { path = "../teos-common" }
[build-dependencies]
tonic-build = "0.11"
tonic-build = "0.6"
[dev-dependencies]
jsonrpc-http-server = "17.1.0"
chunked_transfer = "1.4"
rand = "0.8.4"
tempdir = "0.3.7"
jsonrpc-http-server = "17.1.0"
tokio-stream = { version = "0.1.5", features = [ "net" ] }

View file

@ -6,15 +6,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.field_attribute("tower_id", "#[serde(with = \"hex::serde\")]")
.field_attribute(
"user_ids",
"#[serde(serialize_with = \"teos_common::ser::serde_vec_bytes::serialize\")]",
"#[serde(serialize_with = \"crate::api::http::serialize_vec_bytes\")]",
)
.field_attribute(
"GetUserResponse.appointments",
"#[serde(serialize_with = \"teos_common::ser::serde_vec_bytes::serialize\")]",
)
.field_attribute(
"NetworkAddress.address_type",
"#[serde(rename = \"type\", with = \"crate::api::serde::serde_address_type\")]",
"#[serde(serialize_with = \"crate::api::http::serialize_vec_bytes\")]",
)
.compile(
&[

View file

@ -7,26 +7,15 @@ import "common/teos/v2/appointment.proto";
import "common/teos/v2/user.proto";
import "google/protobuf/empty.proto";
message NetworkAddress {
// Tower public API endpoint.
enum AddressType {
IpV4 = 0;
TorV3 = 1;
}
AddressType address_type = 1;
string address = 2;
uint32 port = 3;
}
message GetTowerInfoResponse {
// Response with information about the tower.
bytes tower_id = 1;
uint32 n_registered_users = 2;
uint32 n_watcher_appointments = 3;
uint32 n_responder_trackers = 4;
bool bitcoind_reachable = 5;
repeated NetworkAddress addresses = 6;
}
service PublicTowerServices {

View file

@ -1,14 +1,13 @@
use serde::{Deserialize, Serialize};
use serde::{ser::SerializeSeq, Deserialize, Serialize, Serializer};
use std::convert::Infallible;
use std::error::Error;
use std::net::SocketAddr;
use tokio::time::Duration;
use tonic::transport::Channel;
use triggered::{Listener, Trigger};
use triggered::Listener;
use warp::{http::StatusCode, reject, reply, Filter, Rejection, Reply};
use teos_common::appointment::LOCATOR_LEN;
use teos_common::net::http::Endpoint;
use teos_common::protos as common_msgs;
use teos_common::{errors, USER_ID_LEN};
@ -36,14 +35,14 @@ impl ApiError {
fn missing_field(field_name: &str) -> Rejection {
reject::custom(Self::new(
format!("missing field `{field_name}`"),
format!("missing field `{}`", field_name),
errors::MISSING_FIELD,
))
}
fn empty_field(field_name: &str) -> Rejection {
reject::custom(Self::new(
format!("`{field_name}` field is empty"),
format!("`{}` field is empty", field_name),
errors::EMPTY_FIELD,
))
}
@ -51,13 +50,25 @@ impl ApiError {
fn wrong_field_length(field_name: &str, field_size: usize, expected_size: usize) -> Rejection {
reject::custom(Self::new(
format!(
"Wrong `{field_name}` field size. Expected {expected_size}, received {field_size}"
"Wrong `{}` field size. Expected {}, received {}",
field_name, expected_size, field_size
),
errors::WRONG_FIELD_SIZE,
))
}
}
pub fn serialize_vec_bytes<S>(v: &[Vec<u8>], s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut seq = s.serialize_seq(Some(v.len()))?;
for element in v.iter() {
seq.serialize_element(&hex::encode(element))?;
}
seq.end()
}
fn with_grpc(
grpc_endpoint: PublicTowerServicesClient<Channel>,
) -> impl Filter<Extract = (PublicTowerServicesClient<Channel>,), Error = Infallible> + Clone {
@ -97,13 +108,13 @@ fn parse_grpc_response<T: serde::Serialize>(
match result {
Ok(r) => {
let inner = r.into_inner();
log::debug!("Request succeeded");
log::info!("Request succeeded");
log::debug!("Response: {}", serde_json::json!(inner));
(reply::json(&inner), StatusCode::OK)
}
Err(s) => {
let (status_code, error_code) = match_status(&s);
log::debug!("Request failed, error_code={error_code}");
log::info!("Request failed, error_code={}", error_code);
log::debug!("Response: {}", serde_json::json!(s.message()));
(
reply::json(&ApiError::new(s.message().into(), error_code)),
@ -118,10 +129,10 @@ async fn register(
addr: Option<std::net::SocketAddr>,
mut grpc_conn: PublicTowerServicesClient<Channel>,
) -> std::result::Result<impl Reply, Rejection> {
log::debug!(
"Received a register request from {}",
addr.map_or("an unknown address".to_owned(), |a| a.to_string())
);
match addr {
Some(a) => log::info!("Received register request from {}", a),
None => log::info!("Received register request from unknown address"),
}
let user_id = req.user_id.clone();
if user_id.is_empty() {
@ -144,10 +155,10 @@ async fn add_appointment(
addr: Option<std::net::SocketAddr>,
mut grpc_conn: PublicTowerServicesClient<Channel>,
) -> std::result::Result<impl Reply, Rejection> {
log::debug!(
"Received an add_appointment request from {}",
addr.map_or("an unknown address".to_owned(), |a| a.to_string())
);
match addr {
Some(a) => log::info!("Received add_appointment request from {}", a),
None => log::info!("Received add_appointment request from unknown address"),
}
if let Some(a) = &req.appointment {
if a.locator.is_empty() {
@ -176,10 +187,10 @@ async fn get_appointment(
addr: Option<std::net::SocketAddr>,
mut grpc_conn: PublicTowerServicesClient<Channel>,
) -> std::result::Result<impl Reply, Rejection> {
log::debug!(
"Received an get_appointment request from {}",
addr.map_or("an unknown address".to_owned(), |a| a.to_string())
);
match addr {
Some(a) => log::info!("Received get_appointment request from {}", a),
None => log::info!("Received get_appointment request from unknown address"),
}
if req.locator.is_empty() {
return Err(ApiError::empty_field("locator"));
@ -204,10 +215,10 @@ async fn get_subscription_info(
addr: Option<std::net::SocketAddr>,
mut grpc_conn: PublicTowerServicesClient<Channel>,
) -> std::result::Result<impl Reply, Rejection> {
log::debug!(
"Received an get_subscription_info request from {}",
addr.map_or("an unknown address".to_owned(), |a| a.to_string())
);
match addr {
Some(a) => log::info!("Received get_subscription_info request from {}", a),
None => log::info!("Received get_subscription_info request from unknown address"),
}
if req.signature.is_empty() {
return Err(ApiError::empty_field("signature"));
@ -217,40 +228,32 @@ 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 {
) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
let register = warp::post()
.and(warp::path(Endpoint::Register.to_string()))
.and(warp::path("register"))
.and(warp::body::content_length_limit(REGISTER_BODY_LEN).and(warp::body::json()))
.and(warp::addr::remote())
.and(with_grpc(grpc_conn.clone()))
.and_then(register);
let add_appointment = warp::post()
.and(warp::path(Endpoint::AddAppointment.to_string()))
.and(warp::path("add_appointment"))
.and(warp::body::content_length_limit(ADD_APPOINTMENT_BODY_LEN).and(warp::body::json()))
.and(warp::addr::remote())
.and(with_grpc(grpc_conn.clone()))
.and_then(add_appointment);
let get_appointment = warp::post()
.and(warp::path(Endpoint::GetAppointment.to_string()))
.and(warp::path("get_appointment"))
.and(warp::body::content_length_limit(GET_APPOINTMENT_BODY_LEN).and(warp::body::json()))
.and(warp::addr::remote())
.and(with_grpc(grpc_conn.clone()))
.and_then(get_appointment);
let get_subscription_info = warp::post()
.and(warp::path(Endpoint::GetSubscriptionInfo.to_string()))
.and(warp::path("get_subscription_info"))
.and(
warp::body::content_length_limit(GET_SUBSCRIPTION_INFO_BODY_LEN)
.and(warp::body::json()),
@ -259,16 +262,10 @@ fn router(
.and(with_grpc(grpc_conn))
.and_then(get_subscription_info);
let ping = warp::get()
.and(warp::path(Endpoint::Ping.to_string()))
.and(warp::addr::remote())
.and_then(ping);
register
.or(add_appointment)
.or(get_appointment)
.or(get_subscription_info)
.or(ping)
.recover(handle_rejection)
}
@ -302,14 +299,9 @@ async fn handle_rejection(err: Rejection) -> Result<impl Reply, Rejection> {
}
}
pub async fn serve(
http_bind: SocketAddr,
grpc_bind: SocketAddr,
service_ready: Trigger,
shutdown_signal: Listener,
) {
pub async fn serve(http_bind: SocketAddr, grpc_bind: String, shutdown_signal: Listener) {
let grpc_conn = loop {
match PublicTowerServicesClient::connect(format!("http://{grpc_bind}")).await {
match PublicTowerServicesClient::connect(grpc_bind.clone()).await {
Ok(conn) => break conn,
Err(_) => {
log::error!("Cannot connect to the gRPC server. Retrying shortly");
@ -317,9 +309,8 @@ pub async fn serve(
}
}
};
let (_, server) =
warp::serve(router(grpc_conn)).bind_with_graceful_shutdown(http_bind, shutdown_signal);
service_ready.trigger();
let (_, server) = warp::serve(router(grpc_conn))
.bind_with_graceful_shutdown(http_bind, async { shutdown_signal.await });
server.await
}
@ -335,7 +326,7 @@ mod test_helpers {
use crate::api::internal::InternalAPI;
use crate::protos::public_tower_services_server::PublicTowerServicesServer;
use crate::test_utils::{create_api_with_config, ApiConfig, BitcoindStopper};
use crate::test_utils::{create_api_with_config, ApiConfig};
pub(crate) enum RequestBody<'a> {
Jsonify(&'a str),
@ -346,8 +337,8 @@ mod test_helpers {
pub(crate) async fn run_tower_in_background_with_config(
api_config: ApiConfig,
) -> (SocketAddr, Arc<InternalAPI>, BitcoindStopper) {
let (internal_rpc_api, bitcoind_stopper) = create_api_with_config(api_config).await;
) -> (SocketAddr, Arc<InternalAPI>) {
let internal_rpc_api = create_api_with_config(api_config).await;
let cloned = internal_rpc_api.clone();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
@ -361,19 +352,18 @@ mod test_helpers {
.unwrap();
});
(addr, cloned, bitcoind_stopper)
(addr, cloned)
}
pub(crate) async fn run_tower_in_background() -> (SocketAddr, BitcoindStopper) {
let (sock_addr, _, bitcoind_stopper) =
run_tower_in_background_with_config(ApiConfig::default()).await;
(sock_addr, bitcoind_stopper)
pub(crate) async fn run_tower_in_background() -> SocketAddr {
run_tower_in_background_with_config(ApiConfig::default())
.await
.0
}
pub(crate) async fn check_api_error(
endpoint: Endpoint,
body: RequestBody<'_>,
pub(crate) async fn check_api_error<'a>(
endpoint: &str,
body: RequestBody<'a>,
server_addr: SocketAddr,
) -> (ApiError, StatusCode) {
let grpc_conn = PublicTowerServicesClient::connect(format!(
@ -385,22 +375,15 @@ mod test_helpers {
.unwrap();
let req = match body {
RequestBody::Json(j) => warp::test::request()
.method("POST")
.path(&endpoint.path())
.json(&j),
RequestBody::DoNotJsonify(j) => warp::test::request()
.method("POST")
.path(&endpoint.path())
.json(&j),
RequestBody::Json(j) => warp::test::request().method("POST").path(endpoint).json(&j),
RequestBody::DoNotJsonify(j) => {
warp::test::request().method("POST").path(endpoint).json(&j)
}
RequestBody::Jsonify(j) => warp::test::request()
.method("POST")
.path(&endpoint.path())
.path(endpoint)
.json(&serde_json::from_str::<Value>(j).unwrap()),
RequestBody::Body(b) => warp::test::request()
.method("POST")
.path(&endpoint.path())
.body(b),
RequestBody::Body(b) => warp::test::request().method("POST").path(endpoint).body(b),
};
let res = req.reply(&router(grpc_conn)).await;
@ -411,7 +394,7 @@ mod test_helpers {
}
pub(crate) async fn request_to_api<B, T>(
endpoint: Endpoint,
endpoint: &str,
body: B,
server_addr: SocketAddr,
) -> Result<T, serde_json::Error>
@ -429,7 +412,7 @@ mod test_helpers {
let res = warp::test::request()
.method("POST")
.path(&endpoint.path())
.path(endpoint)
.json(&serde_json::json!(body))
.reply(&router(grpc_conn))
.await;
@ -447,9 +430,9 @@ mod tests_failures {
#[tokio::test]
async fn test_no_json_request_body() {
let (server_addr, _s) = run_tower_in_background().await;
let server_addr = run_tower_in_background().await;
let (api_error, status) =
check_api_error(Endpoint::Register, RequestBody::Body(""), server_addr).await;
check_api_error("/register", RequestBody::Body(""), server_addr).await;
assert!(api_error.error.contains("EOF while parsing"));
assert_eq!(api_error.error_code, errors::INVALID_REQUEST_FORMAT);
assert_eq!(status, StatusCode::BAD_REQUEST);
@ -457,13 +440,9 @@ mod tests_failures {
#[tokio::test]
async fn test_wrong_json_request_body() {
let (server_addr, _s) = run_tower_in_background().await;
let (api_error, status) = check_api_error(
Endpoint::Register,
RequestBody::DoNotJsonify(""),
server_addr,
)
.await;
let server_addr = run_tower_in_background().await;
let (api_error, status) =
check_api_error("/register", RequestBody::DoNotJsonify(""), server_addr).await;
assert!(api_error.error.contains("expected struct"));
assert_eq!(api_error.error_code, errors::WRONG_FIELD_TYPE);
assert_eq!(status, StatusCode::BAD_REQUEST);
@ -471,13 +450,9 @@ mod tests_failures {
#[tokio::test]
async fn test_empty_json_request_body() {
let (server_addr, _s) = run_tower_in_background().await;
let (api_error, status) = check_api_error(
Endpoint::Register,
RequestBody::Jsonify(r#"{}"#),
server_addr,
)
.await;
let server_addr = run_tower_in_background().await;
let (api_error, status) =
check_api_error("/register", RequestBody::Jsonify(r#"{}"#), server_addr).await;
assert!(api_error.error.contains("missing field"));
assert_eq!(api_error.error_code, errors::MISSING_FIELD);
assert_eq!(status, StatusCode::BAD_REQUEST);
@ -485,9 +460,9 @@ mod tests_failures {
#[tokio::test]
async fn test_empty_field() {
let (server_addr, _s) = run_tower_in_background().await;
let server_addr = run_tower_in_background().await;
let (api_error, status) = check_api_error(
Endpoint::Register,
"/register",
RequestBody::Jsonify(r#"{"user_id": ""}"#),
server_addr,
)
@ -499,9 +474,9 @@ mod tests_failures {
#[tokio::test]
async fn test_wrong_field_hex_encoding_odd() {
let (server_addr, _s) = run_tower_in_background().await;
let server_addr = run_tower_in_background().await;
let (api_error, status) = check_api_error(
Endpoint::Register,
"/register",
RequestBody::Jsonify(r#"{"user_id": "a"}"#),
server_addr,
)
@ -513,9 +488,9 @@ mod tests_failures {
#[tokio::test]
async fn test_wrong_hex_encoding_character() {
let (server_addr, _s) = run_tower_in_background().await;
let server_addr = run_tower_in_background().await;
let (api_error, status) =
check_api_error(Endpoint::Register,
check_api_error("/register",
RequestBody::Jsonify(r#"{"user_id": "022fa2900ed7fc07b4e8ca3ea081e846245b0497944644aa78ea0b994ac22074dZ"}"#),
server_addr
).await;
@ -527,9 +502,9 @@ mod tests_failures {
#[tokio::test]
async fn test_wrong_field_size() {
let (server_addr, _s) = run_tower_in_background().await;
let server_addr = run_tower_in_background().await;
let (api_error, status) = check_api_error(
Endpoint::Register,
"/register",
RequestBody::Jsonify(r#"{"user_id": "aa"}"#),
server_addr,
)
@ -542,9 +517,9 @@ mod tests_failures {
#[tokio::test]
async fn test_wrong_field_type() {
let (server_addr, _s) = run_tower_in_background().await;
let server_addr = run_tower_in_background().await;
let (api_error, status) = check_api_error(
Endpoint::Register,
"/register",
RequestBody::DoNotJsonify(r#"{"user_id": 1}"#),
server_addr,
)
@ -557,9 +532,9 @@ mod tests_failures {
#[tokio::test]
async fn test_request_missing_field() {
// We'll use a different endpoint here since we need a json object with more than one field
let (server_addr, _s) = run_tower_in_background().await;
let server_addr = run_tower_in_background().await;
let (api_error, status) = check_api_error(
Endpoint::AddAppointment,
"/add_appointment",
RequestBody::Jsonify(r#"{"signature": "aa"}"#),
server_addr,
)
@ -573,7 +548,7 @@ mod tests_failures {
#[tokio::test]
async fn test_empty_request_body() {
let (server_addr, _s) = run_tower_in_background().await;
let server_addr = run_tower_in_background().await;
let grpc_conn = PublicTowerServicesClient::connect(format!(
"http://{}:{}",
server_addr.ip(),
@ -584,16 +559,16 @@ mod tests_failures {
let res = warp::test::request()
.method("POST")
.path(&Endpoint::Register.path())
.path("/register")
.reply(&router(grpc_conn))
.await;
assert_eq!(res.status(), StatusCode::LENGTH_REQUIRED);
assert_eq!(res.status(), StatusCode::LENGTH_REQUIRED)
}
#[tokio::test]
async fn test_payload_too_large() {
let (server_addr, _s) = run_tower_in_background().await;
let server_addr = run_tower_in_background().await;
let grpc_conn = PublicTowerServicesClient::connect(format!(
"http://{}:{}",
server_addr.ip(),
@ -604,17 +579,17 @@ mod tests_failures {
let res = warp::test::request()
.method("POST")
.path(&Endpoint::Register.path())
.path("/register")
.json(&format!("{}{}", get_random_user_id(), get_random_user_id()))
.reply(&router(grpc_conn))
.await;
assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE)
}
#[tokio::test]
async fn test_wrong_endpoint() {
let (server_addr, _s) = run_tower_in_background().await;
let server_addr = run_tower_in_background().await;
let grpc_conn = PublicTowerServicesClient::connect(format!(
"http://{}:{}",
server_addr.ip(),
@ -625,16 +600,17 @@ mod tests_failures {
let res = warp::test::request()
.method("POST")
.path("/")
.json(&"")
.reply(&router(grpc_conn))
.await;
assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED);
assert_eq!(res.status(), StatusCode::NOT_FOUND)
}
#[tokio::test]
async fn test_wrong_method() {
let (server_addr, _s) = run_tower_in_background().await;
let server_addr = run_tower_in_background().await;
let grpc_conn = PublicTowerServicesClient::connect(format!(
"http://{}:{}",
server_addr.ip(),
@ -644,11 +620,12 @@ mod tests_failures {
.unwrap();
let res = warp::test::request()
.path("/")
.json(&"")
.reply(&router(grpc_conn))
.await;
assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED);
assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED)
}
}
@ -660,21 +637,18 @@ mod tests_methods {
};
use super::*;
use crate::responder::{ConfirmationStatus, TransactionTracker};
use crate::test_utils::{
generate_dummy_appointment, get_random_tx, ApiConfig, DURATION, SLOTS,
};
use crate::watcher::Breach;
use crate::extended_appointment::UUID;
use crate::test_utils::{generate_dummy_appointment, ApiConfig, DURATION, SLOTS};
use teos_common::test_utils::get_random_user_id;
use teos_common::{cryptography, UserId};
#[tokio::test]
async fn test_register() {
let (server_addr, _s) = run_tower_in_background().await;
let server_addr = run_tower_in_background().await;
let response =
request_to_api::<common_msgs::RegisterRequest, common_msgs::RegisterResponse>(
Endpoint::Register,
"/register",
common_msgs::RegisterRequest {
user_id: get_random_user_id().to_vec(),
},
@ -686,13 +660,13 @@ mod tests_methods {
#[tokio::test]
async fn test_register_max_slots() {
let (server_addr, _, _s) =
let (server_addr, _) =
run_tower_in_background_with_config(ApiConfig::new(u32::MAX, DURATION)).await;
let user_id = get_random_user_id();
// Register once, this should go trough and set slots to the limit
request_to_api::<common_msgs::RegisterRequest, common_msgs::RegisterResponse>(
Endpoint::Register,
"/register",
common_msgs::RegisterRequest {
user_id: user_id.to_vec(),
},
@ -704,7 +678,7 @@ mod tests_methods {
// Register again to get additional slots, this should fail
assert_eq!(
check_api_error(
Endpoint::Register,
"/register",
RequestBody::Json(serde_json::json!(common_msgs::RegisterRequest {
user_id: user_id.to_vec(),
})),
@ -723,7 +697,7 @@ mod tests_methods {
#[tokio::test]
async fn test_register_service_unavailable() {
let (server_addr, _, _s) = run_tower_in_background_with_config(
let (server_addr, _) = run_tower_in_background_with_config(
ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable(),
)
.await;
@ -732,7 +706,7 @@ mod tests_methods {
// Register with bitcoind down
assert_eq!(
check_api_error(
Endpoint::Register,
"/register",
RequestBody::Json(serde_json::json!(common_msgs::RegisterRequest {
user_id: user_id.to_vec(),
})),
@ -751,12 +725,12 @@ mod tests_methods {
#[tokio::test]
async fn test_add_appointment() {
let (server_addr, _s) = run_tower_in_background().await;
let server_addr = run_tower_in_background().await;
// Register first
let (user_sk, user_pk) = cryptography::get_random_keypair();
request_to_api::<common_msgs::RegisterRequest, common_msgs::RegisterResponse>(
Endpoint::Register,
"/register",
common_msgs::RegisterRequest {
user_id: user_pk.serialize().to_vec(),
},
@ -767,13 +741,13 @@ mod tests_methods {
// Then try to add an appointment
let appointment = generate_dummy_appointment(None).inner;
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
let response = request_to_api::<
common_msgs::AddAppointmentRequest,
common_msgs::AddAppointmentResponse,
>(
Endpoint::AddAppointment,
"/add_appointment",
common_msgs::AddAppointmentRequest {
appointment: Some(appointment.into()),
signature,
@ -790,14 +764,14 @@ mod tests_methods {
#[tokio::test]
async fn test_add_appointment_non_registered() {
let (server_addr, _s) = run_tower_in_background().await;
let (user_sk, _s) = cryptography::get_random_keypair();
let server_addr = run_tower_in_background().await;
let (user_sk, _) = cryptography::get_random_keypair();
let appointment = generate_dummy_appointment(None).inner;
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
assert_eq!(
check_api_error(
Endpoint::AddAppointment,
"/add_appointment",
RequestBody::Json(serde_json::json!(common_msgs::AddAppointmentRequest {
appointment: Some(appointment.into()),
signature,
@ -818,13 +792,13 @@ mod tests_methods {
#[tokio::test]
async fn test_add_appointment_already_triggered() {
// Get the InternalAPI so we can mess with the inner state
let (server_addr, internal_api, _s) =
let (server_addr, internal_api) =
run_tower_in_background_with_config(ApiConfig::new(u32::MAX, DURATION)).await;
// Register
let (user_sk, user_pk) = cryptography::get_random_keypair();
request_to_api::<common_msgs::RegisterRequest, common_msgs::RegisterResponse>(
Endpoint::Register,
"/register",
common_msgs::RegisterRequest {
user_id: user_pk.serialize().to_vec(),
},
@ -833,23 +807,17 @@ mod tests_methods {
.await
.unwrap();
// Add the appointment to the Responder as a tracker so it counts as triggered
let dispute_tx = get_random_tx();
let tracker = TransactionTracker::new(
Breach::new(dispute_tx.clone(), get_random_tx()),
UserId(user_pk),
ConfirmationStatus::ConfirmedIn(100),
);
// Add the appointment to the Responder so it counts as triggered
let appointment = generate_dummy_appointment(None).inner;
let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
internal_api
.get_watcher()
.add_dummy_tracker_to_responder(&tracker);
.add_random_tracker_to_responder(UUID::new(appointment.locator, UserId(user_pk)));
// Try to add it via the http API
let appointment = generate_dummy_appointment(Some(&dispute_tx.compute_txid())).inner;
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
assert_eq!(
check_api_error(
Endpoint::AddAppointment,
"/add_appointment",
RequestBody::Json(serde_json::json!(common_msgs::AddAppointmentRequest {
appointment: Some(appointment.into()),
signature,
@ -869,17 +837,17 @@ mod tests_methods {
#[tokio::test]
async fn test_add_appointment_service_unavailable() {
let (server_addr, _, _s) = run_tower_in_background_with_config(
let (server_addr, _) = run_tower_in_background_with_config(
ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable(),
)
.await;
let (user_sk, _) = cryptography::get_random_keypair();
let appointment = generate_dummy_appointment(None).inner;
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
assert_eq!(
check_api_error(
Endpoint::AddAppointment,
"/add_appointment",
RequestBody::Json(serde_json::json!(common_msgs::AddAppointmentRequest {
appointment: Some(appointment.into()),
signature,
@ -899,12 +867,12 @@ mod tests_methods {
#[tokio::test]
async fn test_get_appointment() {
let (server_addr, _s) = run_tower_in_background().await;
let server_addr = run_tower_in_background().await;
// Register first
let (user_sk, user_pk) = cryptography::get_random_keypair();
request_to_api::<common_msgs::RegisterRequest, common_msgs::RegisterResponse>(
Endpoint::Register,
"/register",
common_msgs::RegisterRequest {
user_id: user_pk.serialize().to_vec(),
},
@ -915,10 +883,10 @@ mod tests_methods {
// Add an appointment
let appointment = generate_dummy_appointment(None).inner;
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
request_to_api::<common_msgs::AddAppointmentRequest, common_msgs::AddAppointmentResponse>(
Endpoint::AddAppointment,
"/add_appointment",
common_msgs::AddAppointmentRequest {
appointment: Some(appointment.clone().into()),
signature,
@ -933,13 +901,14 @@ mod tests_methods {
common_msgs::GetAppointmentRequest,
common_msgs::GetAppointmentResponse,
>(
Endpoint::GetAppointment,
"/get_appointment",
common_msgs::GetAppointmentRequest {
locator: appointment.locator.to_vec(),
signature: cryptography::sign(
format!("get appointment {}", appointment.locator).as_bytes(),
&user_sk,
),
)
.unwrap(),
},
server_addr,
)
@ -953,7 +922,7 @@ mod tests_methods {
#[tokio::test]
async fn test_get_appointment_non_registered() {
let (server_addr, _s) = run_tower_in_background().await;
let server_addr = run_tower_in_background().await;
// User is not registered
let (user_sk, _) = cryptography::get_random_keypair();
@ -962,13 +931,14 @@ mod tests_methods {
assert_eq!(
check_api_error(
Endpoint::GetAppointment,
"/get_appointment",
RequestBody::Json(serde_json::json!(common_msgs::GetAppointmentRequest {
locator: appointment.locator.to_vec(),
signature: cryptography::sign(
format!("get appointment {}", appointment.locator).as_bytes(),
&user_sk,
)
.unwrap()
})),
server_addr,
)
@ -985,12 +955,12 @@ mod tests_methods {
#[tokio::test]
async fn test_get_appointment_not_found() {
let (server_addr, _s) = run_tower_in_background().await;
let server_addr = run_tower_in_background().await;
// Register first
let (user_sk, user_pk) = cryptography::get_random_keypair();
request_to_api::<common_msgs::RegisterRequest, common_msgs::RegisterResponse>(
Endpoint::Register,
"/register",
common_msgs::RegisterRequest {
user_id: user_pk.serialize().to_vec(),
},
@ -1004,13 +974,14 @@ mod tests_methods {
assert_eq!(
check_api_error(
Endpoint::GetAppointment,
"/get_appointment",
RequestBody::Json(serde_json::json!(common_msgs::GetAppointmentRequest {
locator: appointment.locator.to_vec(),
signature: cryptography::sign(
format!("get appointment {}", appointment.locator).as_bytes(),
&user_sk,
)
.unwrap()
})),
server_addr,
)
@ -1027,7 +998,7 @@ mod tests_methods {
#[tokio::test]
async fn test_get_appointment_service_unavailable() {
let (server_addr, _, _s) = run_tower_in_background_with_config(
let (server_addr, _) = run_tower_in_background_with_config(
ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable(),
)
.await;
@ -1038,13 +1009,14 @@ mod tests_methods {
assert_eq!(
check_api_error(
Endpoint::GetAppointment,
"/get_appointment",
RequestBody::Json(serde_json::json!(common_msgs::GetAppointmentRequest {
locator: appointment.locator.to_vec(),
signature: cryptography::sign(
format!("get appointment {}", appointment.locator).as_bytes(),
&user_sk,
)
.unwrap()
})),
server_addr,
)
@ -1061,12 +1033,12 @@ mod tests_methods {
#[tokio::test]
async fn test_get_subscription_info() {
let (server_addr, _s) = run_tower_in_background().await;
let server_addr = run_tower_in_background().await;
// Register first
let (user_sk, user_pk) = cryptography::get_random_keypair();
request_to_api::<common_msgs::RegisterRequest, common_msgs::RegisterResponse>(
Endpoint::Register,
"/register",
common_msgs::RegisterRequest {
user_id: user_pk.serialize().to_vec(),
},
@ -1080,9 +1052,10 @@ mod tests_methods {
common_msgs::GetSubscriptionInfoRequest,
common_msgs::GetSubscriptionInfoResponse,
>(
Endpoint::GetSubscriptionInfo,
"/get_subscription_info",
common_msgs::GetSubscriptionInfoRequest {
signature: cryptography::sign("get subscription info".as_bytes(), &user_sk),
signature: cryptography::sign("get subscription info".as_bytes(), &user_sk)
.unwrap(),
},
server_addr,
)
@ -1096,16 +1069,17 @@ mod tests_methods {
#[tokio::test]
async fn test_get_subscription_info_non_registered() {
let (server_addr, _s) = run_tower_in_background().await;
let server_addr = run_tower_in_background().await;
// User is not registered
let (user_sk, _) = cryptography::get_random_keypair();
assert_eq!(
check_api_error(
Endpoint::GetSubscriptionInfo,
"/get_subscription_info",
RequestBody::Json(serde_json::json!(common_msgs::GetSubscriptionInfoRequest {
signature: cryptography::sign("get subscription info".as_bytes(), &user_sk)
.unwrap(),
})),
server_addr,
)
@ -1123,16 +1097,17 @@ mod tests_methods {
#[tokio::test]
async fn test_get_subscription_info_service_unavailable() {
let (user_sk, _) = cryptography::get_random_keypair();
let (server_addr, _, _s) = run_tower_in_background_with_config(
let (server_addr, _) = run_tower_in_background_with_config(
ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable(),
)
.await;
assert_eq!(
check_api_error(
Endpoint::GetSubscriptionInfo,
"/get_subscription_info",
RequestBody::Json(serde_json::json!(common_msgs::GetSubscriptionInfoRequest {
signature: cryptography::sign("get subscription info".as_bytes(), &user_sk)
.unwrap(),
})),
server_addr,
)

View file

@ -2,7 +2,6 @@ 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;
@ -22,8 +21,6 @@ use teos_common::UserId;
pub struct InternalAPI {
/// A [Watcher] instance.
watcher: Arc<Watcher>,
/// A list of public API endpoints.
addresses: Vec<msgs::NetworkAddress>,
/// A flag that indicates wether bitcoind is reachable or not.
bitcoind_reachable: Arc<(Mutex<bool>, Condvar)>,
/// A signal indicating the tower is shuting down.
@ -34,22 +31,16 @@ impl InternalAPI {
/// Creates a new [InternalAPI] instance.
pub fn new(
watcher: Arc<Watcher>,
addresses: Vec<msgs::NetworkAddress>,
bitcoind_reachable: Arc<(Mutex<bool>, Condvar)>,
shutdown_trigger: Trigger,
) -> Self {
Self {
watcher,
addresses,
bitcoind_reachable,
shutdown_trigger,
}
}
pub fn get_addresses(&self) -> &Vec<msgs::NetworkAddress> {
&self.addresses
}
/// Checks whether bitcoind is reachable.
fn check_service_unavailable(&self) -> Result<(), Status> {
if *self.bitcoind_reachable.0.lock().unwrap() {
@ -134,7 +125,7 @@ impl PublicTowerServices for Arc<InternalAPI> {
)),
AddAppointmentFailure::SubscriptionExpired(x) => Err(Status::new(
Code::Unauthenticated,
format!("Your subscription expired at {x}"),
format!("Your subscription expired at {}", x),
)),
AddAppointmentFailure::AlreadyTriggered => Err(Status::new(
Code::AlreadyExists,
@ -192,7 +183,7 @@ impl PublicTowerServices for Arc<InternalAPI> {
)),
GetAppointmentFailure::SubscriptionExpired(x) => Err(Status::new(
Code::Unauthenticated,
format!("Your subscription expired at {x}"),
format!("Your subscription expired at {}", x),
)),
},
}
@ -214,7 +205,7 @@ impl PublicTowerServices for Arc<InternalAPI> {
),
GetSubscriptionInfoFailure::SubscriptionExpired(x) => Status::new(
Code::Unauthenticated,
format!("Your subscription expired at {x}"),
format!("Your subscription expired at {}", x),
),
})?;
@ -233,15 +224,8 @@ impl PrivateTowerServices for Arc<InternalAPI> {
/// Internally calls [Watcher::get_all_watcher_appointments] and [Watcher::get_all_responder_trackers].
async fn get_all_appointments(
&self,
request: Request<()>,
_: Request<()>,
) -> Result<Response<msgs::GetAllAppointmentsResponse>, Status> {
log::debug!(
"Received a get_all_appointments request from {}",
request
.remote_addr()
.map_or("an unknown address".to_owned(), |a| a.to_string())
);
let mut all_appointments = Vec::new();
for (_, appointment) in self.watcher.get_all_watcher_appointments().into_iter() {
@ -273,13 +257,6 @@ impl PrivateTowerServices for Arc<InternalAPI> {
&self,
request: tonic::Request<msgs::GetAppointmentsRequest>,
) -> Result<tonic::Response<msgs::GetAppointmentsResponse>, Status> {
log::debug!(
"Received a get_appointments requests from {}",
request
.remote_addr()
.map_or("an unknown address".to_owned(), |a| a.to_string())
);
let mut matching_appointments = vec![];
let locator = Locator::from_slice(&request.into_inner().locator).map_err(|_| {
Status::new(
@ -324,18 +301,10 @@ impl PrivateTowerServices for Arc<InternalAPI> {
/// and [Watcher::get_trackers_count].
async fn get_tower_info(
&self,
request: Request<()>,
_: Request<()>,
) -> Result<Response<msgs::GetTowerInfoResponse>, Status> {
log::debug!(
"Received a get_tower_info request from {}",
request
.remote_addr()
.map_or("an unknown address".to_owned(), |a| a.to_string())
);
Ok(Response::new(msgs::GetTowerInfoResponse {
tower_id: self.watcher.tower_id.to_vec(),
addresses: self.get_addresses().clone(),
n_registered_users: self.watcher.get_registered_users_count() as u32,
n_watcher_appointments: self.watcher.get_appointments_count() as u32,
n_responder_trackers: self.watcher.get_trackers_count() as u32,
@ -345,17 +314,7 @@ impl PrivateTowerServices for Arc<InternalAPI> {
/// Get user endpoint. Gets all users in the tower. Part of the private API.
/// Internally calls [Watcher::get_user_ids].
async fn get_users(
&self,
request: Request<()>,
) -> Result<Response<msgs::GetUsersResponse>, Status> {
log::debug!(
"Received a get_users requests from {}",
request
.remote_addr()
.map_or("an unknown address".to_owned(), |a| a.to_string())
);
async fn get_users(&self, _: Request<()>) -> Result<Response<msgs::GetUsersResponse>, Status> {
let user_ids = self
.watcher
.get_user_ids()
@ -372,13 +331,6 @@ impl PrivateTowerServices for Arc<InternalAPI> {
&self,
request: Request<msgs::GetUserRequest>,
) -> Result<Response<msgs::GetUserResponse>, Status> {
log::debug!(
"Received a get_user request from {}",
request
.remote_addr()
.map_or("an unknown address".to_owned(), |a| a.to_string())
);
let user_id = UserId::from_slice(&request.into_inner().user_id).map_err(|_| {
Status::new(
Code::InvalidArgument,
@ -387,13 +339,13 @@ impl PrivateTowerServices for Arc<InternalAPI> {
})?;
match self.watcher.get_user_info(user_id) {
Some((info, locators)) => Ok(Response::new(msgs::GetUserResponse {
Some(info) => Ok(Response::new(msgs::GetUserResponse {
available_slots: info.available_slots,
subscription_expiry: info.subscription_expiry,
// TODO: Should make it return locators and make `get_appointments` queryable using the (user_id, locator) pair for consistency.
appointments: locators
.into_iter()
.map(|locator| UUID::new(locator, user_id).to_vec())
appointments: info
.appointments
.iter()
.map(|(uuid, _)| uuid.to_vec())
.collect(),
})),
None => Err(Status::new(Code::NotFound, "User not found")),
@ -401,15 +353,10 @@ impl PrivateTowerServices for Arc<InternalAPI> {
}
/// Stop endpoint. Stops the tower daemon. Part of the private API.
async fn stop(&self, request: Request<()>) -> Result<Response<()>, Status> {
async fn stop(&self, _: Request<()>) -> Result<Response<()>, Status> {
self.shutdown_trigger.trigger();
log::debug!(
"Received a shutting down request from {}, notifying components",
request
.remote_addr()
.map_or("an unknown address".to_owned(), |a| a.to_string())
);
log::debug!("Received shutting down signal, notifying components");
Ok(Response::new(()))
}
}
@ -434,10 +381,11 @@ 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_dummy_appointment_with_user,
get_random_tx, DURATION, SLOTS, START_HEIGHT,
create_api, generate_dummy_appointment, generate_uuid, get_random_tx, DURATION, SLOTS,
START_HEIGHT,
};
use crate::watcher::Breach;
@ -446,7 +394,7 @@ mod tests_private_api {
#[tokio::test]
async fn test_get_all_appointments() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
let response = internal_api
.get_all_appointments(Request::new(()))
@ -454,19 +402,19 @@ mod tests_private_api {
.unwrap()
.into_inner();
assert!(matches!(response, msgs::GetAllAppointmentsResponse { .. }));
assert!(matches!(response, msgs::GetAllAppointmentsResponse { .. }))
}
#[tokio::test]
async fn test_get_all_appointments_watcher() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
// Add data to the Watcher so we can retrieve it later on
let (user_sk, user_pk) = get_random_keypair();
internal_api.watcher.register(UserId(user_pk)).unwrap();
let appointment = generate_dummy_appointment(None).inner;
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk);
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
internal_api
.watcher
.add_appointment(appointment.clone(), user_signature)
@ -487,10 +435,12 @@ mod tests_private_api {
#[tokio::test]
async fn test_get_all_appointments_responder() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
// Add data to the Responser so we can retrieve it later on
internal_api.watcher.add_random_tracker_to_responder();
internal_api
.watcher
.add_random_tracker_to_responder(generate_uuid());
let response = internal_api
.get_all_appointments(Request::new(()))
@ -507,9 +457,9 @@ mod tests_private_api {
#[tokio::test]
async fn test_get_appointments() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
let locator = Locator::new(get_random_tx().compute_txid()).to_vec();
let locator = Locator::new(get_random_tx().txid()).to_vec();
let response = internal_api
.get_appointments(Request::new(msgs::GetAppointmentsRequest { locator }))
.await
@ -521,11 +471,11 @@ mod tests_private_api {
#[tokio::test]
async fn test_get_appointments_watcher() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
for i in 0..3 {
// Create a dispute tx to be used for creating different dummy appointments with the same locator.
let dispute_txid = get_random_tx().compute_txid();
let dispute_txid = get_random_tx().txid();
// The number of different appointments to create for this dispute tx.
let appointments_to_create = 4 * i + 7;
@ -535,7 +485,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);
let signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
internal_api
.watcher
.add_appointment(appointment, signature)
@ -571,7 +521,7 @@ mod tests_private_api {
#[tokio::test]
async fn test_get_appointments_responder() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
for i in 0..3 {
// Create a dispute tx to be used for creating different trackers.
@ -590,10 +540,10 @@ mod tests_private_api {
);
internal_api
.watcher
.add_dummy_tracker_to_responder(&tracker);
.add_dummy_tracker_to_responder(generate_uuid(), &tracker);
}
let locator = Locator::new(dispute_tx.compute_txid());
let locator = Locator::new(dispute_tx.txid());
// Query for the current locator and assert it retrieves correct trackers.
let response = internal_api
@ -622,7 +572,7 @@ mod tests_private_api {
#[tokio::test]
async fn test_get_tower_info_empty() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
let response = internal_api
.get_tower_info(Request::new(()))
@ -638,7 +588,7 @@ mod tests_private_api {
#[tokio::test]
async fn test_get_tower_info() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
// Register a user
let (user_sk, user_pk) = get_random_keypair();
@ -648,7 +598,7 @@ mod tests_private_api {
// Add data to the Watcher
for _ in 0..2 {
let appointment = generate_dummy_appointment(None).inner;
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk);
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
internal_api
.watcher
.add_appointment(appointment.clone(), user_signature)
@ -657,7 +607,9 @@ mod tests_private_api {
// And the Responder
for _ in 0..3 {
internal_api.watcher.add_random_tracker_to_responder();
internal_api
.watcher
.add_random_tracker_to_responder(generate_uuid());
}
let response = internal_api
@ -675,7 +627,7 @@ mod tests_private_api {
#[tokio::test]
async fn test_get_users() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
let mut users = HashSet::new();
// Add a couple of users
@ -697,7 +649,7 @@ mod tests_private_api {
#[tokio::test]
async fn test_get_users_empty() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
let response = internal_api
.get_users(Request::new(()))
@ -710,7 +662,7 @@ mod tests_private_api {
#[tokio::test]
async fn test_get_user() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
// Register a user and get it back
let (user_sk, user_pk) = get_random_keypair();
@ -730,11 +682,12 @@ mod tests_private_api {
assert!(response.appointments.is_empty());
// Add an appointment and check back
let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None);
let user_signature = cryptography::sign(&appointment.inner.to_vec(), &user_sk);
let appointment = generate_dummy_appointment(None).inner;
let uuid = UUID::new(appointment.locator, user_id);
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
internal_api
.watcher
.add_appointment(appointment.inner, user_signature)
.add_appointment(appointment.clone(), user_signature)
.unwrap();
let response = internal_api
@ -752,7 +705,7 @@ mod tests_private_api {
#[tokio::test]
async fn test_get_user_not_found() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
// Non-registered user
let (_, user_pk) = get_random_keypair();
@ -773,7 +726,7 @@ mod tests_private_api {
#[tokio::test]
async fn test_stop() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
assert!(!internal_api.shutdown_trigger.is_triggered());
internal_api.stop(Request::new(())).await.unwrap();
@ -785,17 +738,15 @@ mod tests_private_api {
mod tests_public_api {
use super::*;
use crate::responder::{ConfirmationStatus, TransactionTracker};
use crate::extended_appointment::UUID;
use crate::test_utils::{
create_api, create_api_with_config, generate_dummy_appointment, get_random_tx, ApiConfig,
DURATION, SLOTS,
create_api, create_api_with_config, generate_dummy_appointment, ApiConfig, DURATION, SLOTS,
};
use crate::watcher::Breach;
use teos_common::cryptography::{self, get_random_keypair};
#[tokio::test]
async fn test_register() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
let (_, user_pk) = get_random_keypair();
@ -815,7 +766,7 @@ mod tests_public_api {
#[tokio::test]
async fn test_register_wrong_user_id() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
let mut user_ids = Vec::new();
@ -846,7 +797,7 @@ mod tests_public_api {
#[tokio::test]
async fn test_register_max_slots() {
let (internal_api, _s) = create_api_with_config(ApiConfig::new(u32::MAX, DURATION)).await;
let internal_api = create_api_with_config(ApiConfig::new(u32::MAX, DURATION)).await;
let (_, user_pk) = get_random_keypair();
let user_id = UserId(user_pk).to_vec();
@ -874,7 +825,7 @@ mod tests_public_api {
#[tokio::test]
async fn test_register_service_unavailable() {
let (internal_api, _s) =
let internal_api =
create_api_with_config(ApiConfig::new(u32::MAX, DURATION).bitcoind_unreachable()).await;
let (_, user_pk) = get_random_keypair();
@ -894,19 +845,19 @@ mod tests_public_api {
#[tokio::test]
async fn test_add_appointment() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
// User must be registered
let (user_sk, user_pk) = get_random_keypair();
internal_api.watcher.register(UserId(user_pk)).unwrap();
let appointment = generate_dummy_appointment(None).inner;
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
let response = internal_api
.add_appointment(Request::new(common_msgs::AddAppointmentRequest {
appointment: Some(appointment.clone().into()),
signature,
signature: user_signature.clone(),
}))
.await
.unwrap()
@ -920,18 +871,18 @@ mod tests_public_api {
#[tokio::test]
async fn test_add_appointment_non_registered() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
// User is not registered this time
let (user_sk, _) = get_random_keypair();
let appointment = generate_dummy_appointment(None).inner;
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
match internal_api
.add_appointment(Request::new(common_msgs::AddAppointmentRequest {
appointment: Some(appointment.clone().into()),
signature,
signature: user_signature.clone(),
}))
.await
{
@ -948,19 +899,19 @@ mod tests_public_api {
#[tokio::test]
async fn test_add_appointment_not_enough_slots() {
let (internal_api, _s) = create_api_with_config(ApiConfig::new(0, DURATION)).await;
let internal_api = create_api_with_config(ApiConfig::new(0, DURATION)).await;
// User is registered but has no slots
let (user_sk, user_pk) = get_random_keypair();
internal_api.watcher.register(UserId(user_pk)).unwrap();
let appointment = generate_dummy_appointment(None).inner;
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
match internal_api
.add_appointment(Request::new(common_msgs::AddAppointmentRequest {
appointment: Some(appointment.clone().into()),
signature,
signature: user_signature.clone(),
}))
.await
{
@ -977,19 +928,19 @@ mod tests_public_api {
#[tokio::test]
async fn test_add_appointment_subscription_expired() {
let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0)).await;
let internal_api = create_api_with_config(ApiConfig::new(SLOTS, 0)).await;
// User is registered but subscription is expired
let (user_sk, user_pk) = get_random_keypair();
internal_api.watcher.register(UserId(user_pk)).unwrap();
let appointment = generate_dummy_appointment(None).inner;
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
match internal_api
.add_appointment(Request::new(common_msgs::AddAppointmentRequest {
appointment: Some(appointment.clone().into()),
signature,
signature: user_signature.clone(),
}))
.await
{
@ -1003,30 +954,22 @@ mod tests_public_api {
#[tokio::test]
async fn test_add_appointment_already_triggered() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
let (user_sk, user_pk) = get_random_keypair();
let user_id = UserId(user_pk);
internal_api.watcher.register(user_id).unwrap();
// Add a tracker to the responder to simulate it being triggered.
let dispute_tx = get_random_tx();
let tracker = TransactionTracker::new(
Breach::new(dispute_tx.clone(), get_random_tx()),
user_id,
ConfirmationStatus::ConfirmedIn(100),
);
let appointment = generate_dummy_appointment(None).inner;
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
internal_api
.get_watcher()
.add_dummy_tracker_to_responder(&tracker);
.watcher
.add_random_tracker_to_responder(UUID::new(appointment.locator, user_id));
// Try to add it again using the API.
let appointment = generate_dummy_appointment(Some(&dispute_tx.compute_txid())).inner;
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
match internal_api
.add_appointment(Request::new(common_msgs::AddAppointmentRequest {
appointment: Some(appointment.into()),
signature,
appointment: Some(appointment.clone().into()),
signature: user_signature.clone(),
}))
.await
{
@ -1042,17 +985,17 @@ mod tests_public_api {
#[tokio::test]
async fn test_add_appointment_service_unavailable() {
let (internal_api, _s) =
let internal_api =
create_api_with_config(ApiConfig::new(u32::MAX, DURATION).bitcoind_unreachable()).await;
let (user_sk, _) = get_random_keypair();
let appointment = generate_dummy_appointment(None).inner;
let signature = cryptography::sign(&appointment.to_vec(), &user_sk);
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
match internal_api
.add_appointment(Request::new(common_msgs::AddAppointmentRequest {
appointment: Some(appointment.clone().into()),
signature,
signature: user_signature.clone(),
}))
.await
{
@ -1066,7 +1009,7 @@ mod tests_public_api {
#[tokio::test]
async fn test_get_appointment() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
// The user must be registered
let (user_sk, user_pk) = get_random_keypair();
@ -1074,7 +1017,7 @@ mod tests_public_api {
// Add the appointment
let appointment = generate_dummy_appointment(None).inner;
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk);
let user_signature = cryptography::sign(&appointment.to_vec(), &user_sk).unwrap();
internal_api
.watcher
.add_appointment(appointment.clone(), user_signature)
@ -1085,7 +1028,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),
signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(),
}))
.await
.unwrap()
@ -1094,12 +1037,12 @@ mod tests_public_api {
assert!(matches!(
response,
common_msgs::GetAppointmentResponse { .. }
));
))
}
#[tokio::test]
async fn test_get_appointment_non_registered() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
// Add a first user to link the appointment to him
let (user_sk, user_pk) = get_random_keypair();
@ -1113,7 +1056,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),
signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(),
}))
.await
{
@ -1127,7 +1070,7 @@ mod tests_public_api {
#[tokio::test]
async fn test_get_appointment_non_existent() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
// The user is registered but the appointment does not exist
let (user_sk, user_pk) = get_random_keypair();
@ -1140,7 +1083,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),
signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(),
}))
.await
{
@ -1154,7 +1097,7 @@ mod tests_public_api {
#[tokio::test]
async fn test_get_appointment_subscription_expired() {
let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0)).await;
let internal_api = create_api_with_config(ApiConfig::new(SLOTS, 0)).await;
// Register the user
let (user_sk, user_pk) = get_random_keypair();
@ -1168,7 +1111,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),
signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(),
}))
.await
{
@ -1182,7 +1125,7 @@ mod tests_public_api {
#[tokio::test]
async fn test_get_appointment_service_unavailable() {
let (internal_api, _s) =
let internal_api =
create_api_with_config(ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable()).await;
let (user_sk, _) = get_random_keypair();
@ -1191,7 +1134,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),
signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(),
}))
.await
{
@ -1205,7 +1148,7 @@ mod tests_public_api {
#[tokio::test]
async fn test_get_subscription_info() {
let (internal_api, _s) = create_api().await;
let internal_api = create_api().await;
// The user must be registered
let (user_sk, user_pk) = get_random_keypair();
@ -1215,7 +1158,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),
signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(),
}))
.await
.unwrap()
@ -1224,12 +1167,12 @@ mod tests_public_api {
assert!(matches!(
response,
common_msgs::GetSubscriptionInfoResponse { .. }
));
))
}
#[tokio::test]
async fn test_get_subscription_info_non_registered() {
let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0)).await;
let internal_api = create_api_with_config(ApiConfig::new(SLOTS, 0)).await;
// The user is not registered
let (user_sk, _) = get_random_keypair();
@ -1238,7 +1181,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),
signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(),
}))
.await
{
@ -1252,7 +1195,7 @@ mod tests_public_api {
#[tokio::test]
async fn test_get_subscription_info_expired() {
let (internal_api, _s) = create_api_with_config(ApiConfig::new(SLOTS, 0)).await;
let internal_api = create_api_with_config(ApiConfig::new(SLOTS, 0)).await;
// The user is registered but the subscription has expired
let (user_sk, user_pk) = get_random_keypair();
@ -1262,7 +1205,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),
signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(),
}))
.await
{
@ -1276,14 +1219,14 @@ mod tests_public_api {
#[tokio::test]
async fn test_get_subscription_info_service_unavailable() {
let (internal_api, _s) =
let internal_api =
create_api_with_config(ApiConfig::new(SLOTS, DURATION).bitcoind_unreachable()).await;
let (user_sk, _) = get_random_keypair();
let message = "get subscription info".to_string();
match internal_api
.get_subscription_info(Request::new(common_msgs::GetSubscriptionInfoRequest {
signature: cryptography::sign(message.as_bytes(), &user_sk),
signature: cryptography::sign(message.as_bytes(), &user_sk).unwrap(),
}))
.await
{

View file

@ -1,4 +1,3 @@
pub mod http;
pub mod internal;
pub mod serde;
pub mod tor;

View file

@ -1,62 +0,0 @@
use crate::protos as msgs;
use teos_common::net::AddressType;
impl msgs::NetworkAddress {
pub fn from_ipv4(address: String, port: u16) -> Self {
Self {
address_type: AddressType::IpV4 as i32,
address,
port: port as u32,
}
}
pub fn from_torv3(address: String, port: u16) -> Self {
Self {
address_type: AddressType::TorV3 as i32,
address,
port: port as u32,
}
}
}
pub mod serde_address_type {
use serde::de::{self, Deserializer};
use serde::Serializer;
use std::str::FromStr;
use super::AddressType;
pub fn serialize<S>(status: &i32, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&AddressType::from(*status).to_string())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<i32, D::Error>
where
D: Deserializer<'de>,
{
struct StatusVisitor;
impl<'de> de::Visitor<'de> for StatusVisitor {
type Value = i32;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string containing the address type")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
let status = AddressType::from_str(v)
.map_err(|_| E::custom("given address type is unknown"))?;
Ok(status as i32)
}
}
deserializer.deserialize_any(StatusVisitor)
}
}

View file

@ -1,209 +1,117 @@
use std::convert::TryInto;
use std::io::{Error, ErrorKind};
use std::net::SocketAddr;
use std::path::PathBuf;
use tokio::fs;
use tokio::net::TcpStream;
use tokio::time::{sleep, Duration};
use torut::control::UnauthenticatedConn;
use torut::onion::TorSecretKeyV3;
use triggered::{Listener, Trigger};
use triggered::Listener;
pub struct TorAPI {
sk: TorSecretKeyV3,
api_endpoint: SocketAddr,
onion_port: u16,
/// Expose an onion service that re-directs to the public api.
pub async fn expose_onion_service(
tor_control_port: u16,
}
api_port: u16,
onion_port: u16,
shutdown_signal_tor: Listener,
) -> Result<(), Error> {
let stream = connect_tor_cp(format!("127.0.0.1:{}", tor_control_port).parse().unwrap())
.await
.map_err(|e| Error::new(ErrorKind::ConnectionRefused, e))?;
impl TorAPI {
pub async fn new(
api_endpoint: SocketAddr,
onion_port: u16,
tor_control_port: u16,
path: PathBuf,
) -> Self {
let key = if let Some(key) = TorAPI::load_sk(path.clone()).await {
key
} else {
log::info!("Generating fresh Tor secret key");
let key = TorSecretKeyV3::generate();
TorAPI::store_sk(&key, path).await;
key
};
let mut unauth_conn = UnauthenticatedConn::new(stream);
Self {
sk: key,
api_endpoint,
onion_port,
tor_control_port,
}
}
let pre_auth = unauth_conn
.load_protocol_info()
.await
.map_err(|e| Error::new(ErrorKind::ConnectionRefused, e))?;
pub fn get_onion_address(&self) -> String {
self.sk.public().get_onion_address().to_string()
}
let auth_data = pre_auth
.make_auth_data()?
.expect("failed to make auth data");
/// Loads a Tor key from disk (if found).
async fn load_sk(path: PathBuf) -> Option<TorSecretKeyV3> {
log::info!("Loading Tor secret key from disk");
let key = fs::read(path.join("onion_v3_sk"))
.await
.map_err(|e| log::warn!("Tor secret key cannot be loaded. {e}"))
.ok()?;
let key: [u8; 64] = key
.try_into()
.map_err(|_| log::error!("Cannot convert loaded data into Tor secret key"))
.ok()?;
unauth_conn.authenticate(&auth_data).await.map_err(|_| {
Error::new(
ErrorKind::PermissionDenied,
"failed to authenticate with Tor",
)
})?;
Some(TorSecretKeyV3::from(key))
}
let mut auth_conn = unauth_conn.into_authenticated().await;
/// Stores a Tor key to disk.
async fn store_sk(key: &TorSecretKeyV3, path: PathBuf) {
if let Err(e) = fs::write(path.join("onion_v3_sk"), key.as_bytes()).await {
log::error!("Cannot store Tor secret key. {e}");
}
}
auth_conn.set_async_event_handler(Some(|_| async move { Ok(()) }));
/// Tries to connect to the Tor control port
async fn connect_tor_cp(&self) -> Result<TcpStream, Error> {
let sock = TcpStream::connect(format!("127.0.0.1:{}", self.tor_control_port))
.await
.map_err(|_| {
Error::new(
ErrorKind::ConnectionRefused,
"failed to connect to Tor control port",
)
})?;
Ok(sock)
}
let key = TorSecretKeyV3::generate();
/// Expose an onion service that re-directs to the public api.
pub async fn expose_onion_service(
&self,
service_ready: Trigger,
shutdown_signal_tor: Listener,
) -> Result<(), Error> {
let stream = self
.connect_tor_cp()
.await
.map_err(|e| Error::new(ErrorKind::ConnectionRefused, e))?;
let mut unauth_conn = UnauthenticatedConn::new(stream);
let pre_auth = unauth_conn
.load_protocol_info()
.await
.map_err(|e| Error::new(ErrorKind::ConnectionRefused, e))?;
let auth_data = pre_auth
.make_auth_data()?
.expect("failed to make auth data");
unauth_conn.authenticate(&auth_data).await.map_err(|_| {
auth_conn
.add_onion_v3(
&key,
false,
false,
false,
None,
&mut [(
onion_port,
format!("127.0.0.1:{}", api_port).parse().unwrap(),
)]
.iter(),
)
.await
.map_err(|e| {
Error::new(
ErrorKind::PermissionDenied,
"failed to authenticate with Tor",
ErrorKind::Other,
format!("failed to create onion hidden service: {}", e),
)
})?;
let mut auth_conn = unauth_conn.into_authenticated().await;
print_onion_service(key.clone(), onion_port);
auth_conn.set_async_event_handler(Some(|_| async move { Ok(()) }));
auth_conn
.add_onion_v3(
&self.sk,
false,
false,
false,
None,
&mut [(self.onion_port, self.api_endpoint)].iter(),
)
.await
.map_err(|e| {
Error::new(
ErrorKind::Other,
format!("failed to create onion hidden service: {e}"),
)
})?;
log::info!(
"Onion service: {}:{}",
self.get_onion_address(),
self.onion_port
);
service_ready.trigger();
shutdown_signal_tor.await;
auth_conn
.del_onion(
&self
.sk
.public()
.get_onion_address()
.get_address_without_dot_onion(),
)
.await
.unwrap();
Ok(())
// NOTE: Needed to keep connection with control port & hidden service running, as soon as we leave
// this function the control port stream is dropped and the hidden service is killed
loop {
sleep(Duration::from_secs(1)).await;
if shutdown_signal_tor.is_triggered() {
break;
}
}
auth_conn
.del_onion(
&key.public()
.get_onion_address()
.get_address_without_dot_onion(),
)
.await
.unwrap();
Ok(())
}
async fn connect_tor_cp(addr: SocketAddr) -> Result<TcpStream, Error> {
let sock = TcpStream::connect(addr).await.map_err(|_| {
Error::new(
ErrorKind::ConnectionRefused,
"failed to connect to tor control port",
)
})?;
Ok(sock)
}
fn print_onion_service(key: TorSecretKeyV3, onion_port: u16) {
let onion_addr = key.public().get_onion_address();
let onion = format!("{}:{}", onion_addr, onion_port);
log::info!("onion service: {}", onion);
}
#[cfg(test)]
mod tests {
use super::*;
use tempdir::TempDir;
use teos_common::test_utils::get_random_user_id;
#[tokio::test]
async fn test_store_load_sk() {
let key = TorSecretKeyV3::generate();
let tmp_path = TempDir::new(&format!("data_dir_{}", get_random_user_id())).unwrap();
TorAPI::store_sk(&key, tmp_path.path().into()).await;
let loaded_key = TorAPI::load_sk(tmp_path.path().into()).await;
assert_eq!(key, loaded_key.unwrap())
}
#[tokio::test]
async fn test_load_sk_inexistent() {
let tmp_path = TempDir::new(&format!("data_dir_{}", get_random_user_id())).unwrap();
let loaded_key = TorAPI::load_sk(tmp_path.path().into()).await;
assert_eq!(loaded_key, None);
}
#[tokio::test]
async fn test_load_sk_wrong_format() {
let tmp_path = TempDir::new(&format!("data_dir_{}", get_random_user_id())).unwrap();
fs::write(tmp_path.path().join("onion_v3_sk"), "random stuff")
.await
.unwrap();
let loaded_key = TorAPI::load_sk(tmp_path.path().into()).await;
assert_eq!(loaded_key, None);
}
#[tokio::test]
async fn test_connect_tor_cp_fail() {
let wrong_cp = 9000;
let tmp_path = TempDir::new(&format!("data_dir_{}", get_random_user_id())).unwrap();
let tor_api = TorAPI::new(
"127.0.1.1:9814".parse().unwrap(),
9814,
wrong_cp,
tmp_path.path().into(),
)
.await;
match tor_api.connect_tor_cp().await {
let tor_control_port = 9000;
let addr = format!("127.0.0.1:{}", tor_control_port).parse().unwrap();
match connect_tor_cp(addr).await {
Ok(_) => {}
Err(e) => {
assert_eq!("failed to connect to Tor control port", e.to_string())
assert_eq!("failed to connect to tor control port", e.to_string())
}
}
}

View file

@ -9,19 +9,17 @@
* at your option.
*/
use base64::{engine::general_purpose::URL_SAFE as BASE64, Engine};
use std::convert::TryInto;
use std::io::{Error, ErrorKind};
use std::sync::Arc;
use tokio::sync::Mutex;
use bitcoin::base64;
use bitcoin::hash_types::{BlockHash, Txid};
use bitcoin::Transaction;
use bitcoincore_rpc::{Auth, RawTx};
use bitcoin::hashes::hex::ToHex;
use bitcoin::{Block, Transaction};
use lightning::util::ser::Writeable;
use lightning_block_sync::http::{HttpEndpoint, JsonResponse};
use lightning_block_sync::http::HttpEndpoint;
use lightning_block_sync::rpc::RpcClient;
use lightning_block_sync::{AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource};
use lightning_block_sync::{AsyncBlockSourceResult, BlockHeaderData, BlockSource};
/// A simple implementation of a bitcoind client (`bitcoin-cli`) with the minimal functionality required by the tower.
pub struct BitcoindClient<'a> {
@ -32,39 +30,39 @@ pub struct BitcoindClient<'a> {
/// The port to connect to.
port: u16,
/// The RPC user `bitcoind` is configured with.
rpc_user: String,
rpc_user: &'a str,
/// The RPC password for the given user.
rpc_password: String,
rpc_password: &'a str,
}
impl BlockSource for &BitcoindClient<'_> {
/// Gets a block header given its hash.
fn get_header<'a>(
&'a self,
&'a mut self,
header_hash: &'a BlockHash,
height_hint: Option<u32>,
) -> AsyncBlockSourceResult<'a, BlockHeaderData> {
Box::pin(async move {
let rpc = self.bitcoind_rpc_client.lock().await;
let mut rpc = self.bitcoind_rpc_client.lock().await;
rpc.get_header(header_hash, height_hint).await
})
}
/// Gets a block given its hash.
fn get_block<'a>(
&'a self,
&'a mut self,
header_hash: &'a BlockHash,
) -> AsyncBlockSourceResult<'a, BlockData> {
) -> AsyncBlockSourceResult<'a, Block> {
Box::pin(async move {
let rpc = self.bitcoind_rpc_client.lock().await;
let mut rpc = self.bitcoind_rpc_client.lock().await;
rpc.get_block(header_hash).await
})
}
/// Get the best block known by our node.
fn get_best_block(&self) -> AsyncBlockSourceResult<(BlockHash, Option<u32>)> {
fn get_best_block(&mut self) -> AsyncBlockSourceResult<(BlockHash, Option<u32>)> {
Box::pin(async move {
let rpc = self.bitcoind_rpc_client.lock().await;
let mut rpc = self.bitcoind_rpc_client.lock().await;
rpc.get_best_block().await
})
}
@ -77,34 +75,12 @@ impl<'a> BitcoindClient<'a> {
pub async fn new(
host: &'a str,
port: u16,
auth: Auth,
teos_network: &'a str,
rpc_user: &'a str,
rpc_password: &'a str,
) -> std::io::Result<BitcoindClient<'a>> {
let http_endpoint = HttpEndpoint::for_host(host.to_owned()).with_port(port);
let (rpc_user, rpc_password) = {
let (user, pass) = auth.get_user_pass().map_err(|e| {
Error::new(
ErrorKind::InvalidInput,
format!("Cannot read cookie file. {}", e),
)
})?;
if user.is_none() {
Err(Error::new(
ErrorKind::InvalidInput,
"Empty btc_rpc_user parsed from rpc_cookie".to_string(),
))
} else if pass.is_none() {
Err(Error::new(
ErrorKind::InvalidInput,
"Empty btc_rpc_password parsed from rpc_cookie",
))
} else {
Ok((user.unwrap(), pass.unwrap()))
}
}?;
let rpc_credentials = BASE64.encode(format!("{}:{}", rpc_user, rpc_password));
let bitcoind_rpc_client = RpcClient::new(&rpc_credentials, http_endpoint);
let rpc_credentials = base64::encode(&format!("{}:{}", rpc_user, rpc_password));
let bitcoind_rpc_client = RpcClient::new(&rpc_credentials, http_endpoint)?;
let client = Self {
bitcoind_rpc_client: Arc::new(Mutex::new(bitcoind_rpc_client)),
@ -114,25 +90,17 @@ impl<'a> BitcoindClient<'a> {
rpc_password,
};
// Test that bitcoind is reachable.
let btc_network = client.get_chain().await?;
// Assert teos runs on the same chain/network as bitcoind.
if btc_network != teos_network {
Err(Error::new(
ErrorKind::InvalidInput,
format!("bitcoind is running on {btc_network} but teosd is set to run on {teos_network}"),
))
} else {
Ok(client)
// Test that bitcoind is reachable
match client.get_best_block_hash_and_height().await {
Ok(_) => Ok(client),
Err(e) => Err(e),
}
}
/// Gets a fresh RPC client.
pub fn get_new_rpc_client(&self) -> RpcClient {
pub fn get_new_rpc_client(&self) -> std::io::Result<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)
}
@ -140,46 +108,26 @@ impl<'a> BitcoindClient<'a> {
pub async fn get_best_block_hash_and_height(
&self,
) -> Result<(BlockHash, Option<u32>), std::io::Error> {
let rpc = self.bitcoind_rpc_client.lock().await;
let mut rpc = self.bitcoind_rpc_client.lock().await;
rpc.call_method::<(BlockHash, Option<u32>)>("getblockchaininfo", &[])
.await
}
/// Sends a transaction to the network.
pub async fn send_raw_transaction(&self, raw_tx: &Transaction) -> Result<Txid, std::io::Error> {
let rpc = self.bitcoind_rpc_client.lock().await;
let mut rpc = self.bitcoind_rpc_client.lock().await;
let raw_tx_json = serde_json::json!(raw_tx.encode().raw_hex());
let raw_tx_json = serde_json::json!(raw_tx.encode().to_hex());
rpc.call_method::<Txid>("sendrawtransaction", &[raw_tx_json])
.await
}
/// Gets a transaction given its id.
pub async fn get_raw_transaction(&self, txid: &Txid) -> Result<Transaction, std::io::Error> {
let rpc = self.bitcoind_rpc_client.lock().await;
let mut rpc = self.bitcoind_rpc_client.lock().await;
let txid_hex = serde_json::json!(txid.encode().raw_hex());
let txid_hex = serde_json::json!(txid.encode().to_hex());
rpc.call_method::<Transaction>("getrawtransaction", &[txid_hex])
.await
}
/// Gets bitcoind's network.
pub async fn get_chain(&self) -> std::io::Result<String> {
// A wrapper type to extract "chain" key from getblockchaininfo JsonResponse.
struct BtcNetwork(String);
impl TryInto<BtcNetwork> for JsonResponse {
type Error = std::io::Error;
fn try_into(self) -> std::io::Result<BtcNetwork> {
Ok(BtcNetwork(self.0["chain"].as_str().unwrap().to_string()))
}
}
// Ask the RPC client for the network bitcoind is running on.
let rpc = self.bitcoind_rpc_client.lock().await;
let btc_network = rpc
.call_method::<BtcNetwork>("getblockchaininfo", &[])
.await?;
Ok(btc_network.0)
}
}

View file

@ -6,7 +6,7 @@ use std::sync::{Arc, Condvar, Mutex};
use crate::responder::ConfirmationStatus;
use crate::{errors, rpc_errors};
use bitcoin::{Transaction, Txid};
use bitcoin::{BlockHash, Transaction, Txid};
use bitcoincore_rpc::{
jsonrpc::error::Error::Rpc as RpcError, jsonrpc::error::Error::Transport as TransportError,
Client as BitcoindClient, Error::JsonRpc as JsonRpcError, RpcApi,
@ -22,7 +22,7 @@ pub struct Carrier {
/// A map of receipts already issued by the [Carrier].
/// Used to prevent potentially re-sending the same transaction over and over.
issued_receipts: HashMap<Txid, ConfirmationStatus>,
/// The last known block height.
/// The last known block header.
block_height: u32,
}
@ -41,11 +41,6 @@ impl Carrier {
}
}
/// The last known block height.
pub(crate) fn block_height(&self) -> u32 {
self.block_height
}
/// Clears the receipts cached by the [Carrier]. Should be called periodically to prevent it from
/// growing unbounded.
pub(crate) fn clear_receipts(&mut self) {
@ -80,49 +75,49 @@ impl Carrier {
pub(crate) fn send_transaction(&mut self, tx: &Transaction) -> ConfirmationStatus {
self.hang_until_bitcoind_reachable();
if let Some(receipt) = self.issued_receipts.get(&tx.compute_txid()) {
log::info!("Transaction already sent: {}", tx.compute_txid());
if let Some(receipt) = self.issued_receipts.get(&tx.txid()) {
log::info!("Transaction already sent: {}", tx.txid());
return *receipt;
}
log::info!("Pushing transaction to the network: {}", tx.compute_txid());
log::info!("Pushing transaction to the network: {}", tx.txid());
let receipt = match self.bitcoin_cli.send_raw_transaction(tx) {
Ok(_) => {
// Here the transaction could, potentially, have been in mempool before the current height.
// This shouldn't really matter though.
log::info!("Transaction successfully delivered: {}", tx.compute_txid());
log::info!("Transaction successfully delivered: {}", tx.txid());
ConfirmationStatus::InMempoolSince(self.block_height)
}
Err(JsonRpcError(RpcError(rpcerr))) => match rpcerr.code {
// Since we're pushing a raw transaction to the network we can face several rejections
rpc_errors::RPC_VERIFY_REJECTED => {
log::error!("Transaction couldn't be broadcast. {rpcerr:?}");
log::error!("Transaction couldn't be broadcast. {:?}", rpcerr);
ConfirmationStatus::Rejected(rpc_errors::RPC_VERIFY_REJECTED)
}
rpc_errors::RPC_VERIFY_ERROR => {
log::error!("Transaction couldn't be broadcast. {rpcerr:?}");
log::error!("Transaction couldn't be broadcast. {:?}", rpcerr);
ConfirmationStatus::Rejected(rpc_errors::RPC_VERIFY_ERROR)
}
rpc_errors::RPC_VERIFY_ALREADY_IN_CHAIN => {
log::info!(
"Transaction was confirmed long ago, not keeping track of it: {}",
tx.compute_txid()
"Transaction is already in the blockchain: {}. Getting confirmation count",
tx.txid()
);
// Given we are not using txindex, if a transaction bounces we cannot get its confirmation count. However, [send_transaction] is guarded by
// checking whether the transaction id can be found in the [Responder]'s [TxIndex], meaning that if the transaction bounces it was confirmed long
// ago (> IRREVOCABLY_RESOLVED), so we don't need to worry about it.
ConfirmationStatus::IrrevocablyResolved
ConfirmationStatus::ConfirmedIn(self.get_tx_height(&tx.txid()).unwrap())
}
rpc_errors::RPC_DESERIALIZATION_ERROR => {
// Adding this here just for completeness. We should never end up here. The Carrier only sends txs handed by the Responder,
// who receives them from the Watcher, who checks that the tx can be properly deserialized.
log::info!("Transaction cannot be deserialized: {}", tx.compute_txid());
log::info!("Transaction cannot be deserialized: {}", tx.txid());
ConfirmationStatus::Rejected(rpc_errors::RPC_DESERIALIZATION_ERROR)
}
_ => {
// If something else happens (unlikely but possible) log it so we can treat it in future releases.
log::error!("Unexpected rpc error when calling sendrawtransaction: {rpcerr:?}");
log::error!(
"Unexpected rpc error when calling sendrawtransaction: {:?}",
rpcerr
);
ConfirmationStatus::Rejected(errors::UNKNOWN_JSON_RPC_EXCEPTION)
}
},
@ -134,48 +129,87 @@ impl Carrier {
}
Err(e) => {
// TODO: This may need finer catching.
log::error!("Unexpected error when calling sendrawtransaction: {e:?}");
log::error!("Unexpected error when calling sendrawtransaction: {:?}", e);
ConfirmationStatus::Rejected(errors::UNKNOWN_JSON_RPC_EXCEPTION)
}
};
self.issued_receipts.insert(tx.compute_txid(), receipt);
self.issued_receipts.insert(tx.txid(), receipt);
receipt
}
/// Checks whether a given transaction can be found in the mempool.
///
/// This uses `getrawtransaction` under the hood and, therefore, its behavior depends on whether `txindex` is enabled in bitcoind.
/// If `txindex` is disabled (default), it will only pull data from the mempool. Otherwise, it will also pull data from the transaction
/// index. Hence, we need to check whether the returned struct has any of the block related datum set (such as `blockhash`).
pub(crate) fn in_mempool(&self, txid: &Txid) -> bool {
/// Gets the block height at where a given [Transaction] was confirmed at (if any).
fn get_tx_height(&self, txid: &Txid) -> Option<u32> {
if let Some(block_hash) = self.get_block_hash_for_tx(txid) {
self.get_block_height(&block_hash)
} else {
None
}
}
/// Queries the height of a given [Block](bitcoin::Block). Returns it if the block can be found. Returns [None] otherwise.
fn get_block_height(&self, block_hash: &BlockHash) -> Option<u32> {
self.hang_until_bitcoind_reachable();
match self.bitcoin_cli.get_raw_transaction_info(txid, None) {
Ok(tx) => tx.blockhash.is_none(),
match self.bitcoin_cli.get_block_header_info(block_hash) {
Ok(header_data) => Some(header_data.height as u32),
Err(JsonRpcError(RpcError(rpcerr))) => match rpcerr.code {
rpc_errors::RPC_INVALID_ADDRESS_OR_KEY => {
log::info!("Transaction not found in mempool: {txid}");
false
log::info!("Block not found: {}", block_hash);
None
}
e => {
// DISCUSS: This could result in a silent error with unknown consequences
log::error!("Unexpected error code when calling getrawtransaction: {e}");
false
log::error!("Unexpected error code when calling getblockheader: {}", e);
None
}
},
Err(JsonRpcError(TransportError(_))) => {
// Connection refused, bitcoind is down.
log::error!("Connection lost with bitcoind, retrying request when possible");
self.flag_bitcoind_unreachable();
self.in_mempool(txid)
self.get_block_height(block_hash)
}
// TODO: This may need finer catching.
Err(e) => {
// DISCUSS: This could result in a silent error with unknown consequences
log::error!("Unexpected JSONRPCError when calling getrawtransaction: {e}");
false
log::error!("Unexpected JSONRPCError when calling getblockheader: {}", e);
None
}
}
}
/// Gets the block hash where a given [Transaction] was confirmed at (if any).
pub(crate) fn get_block_hash_for_tx(&self, txid: &Txid) -> Option<BlockHash> {
self.hang_until_bitcoind_reachable();
match self.bitcoin_cli.get_raw_transaction_info(txid, None) {
Ok(tx_data) => tx_data.blockhash,
Err(JsonRpcError(RpcError(rpcerr))) => match rpcerr.code {
rpc_errors::RPC_INVALID_ADDRESS_OR_KEY => {
log::info!("Transaction not found in mempool nor blockchain: {}", txid);
None
}
e => {
log::error!(
"Unexpected error code when calling getrawtransaction: {}",
e
);
None
}
},
Err(JsonRpcError(TransportError(_))) => {
// Connection refused, bitcoind is down.
log::error!("Connection lost with bitcoind, retrying request when possible");
self.flag_bitcoind_unreachable();
self.get_block_hash_for_tx(txid)
}
// TODO: This may need finer catching.
Err(e) => {
log::error!(
"Unexpected JSONRPCError when calling getrawtransaction: {}",
e
);
None
}
}
}
@ -184,11 +218,10 @@ impl Carrier {
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
use std::thread;
use crate::test_utils::{get_random_tx, start_server, BitcoindMock, MockOptions, START_HEIGHT};
use teos_common::test_utils::{TXID_HEX, TX_HEX};
use teos_common::test_utils::TX_HEX;
use bitcoin::consensus;
use bitcoin::hashes::hex::FromHex;
@ -208,17 +241,18 @@ mod tests {
#[test]
fn test_clear_receipts() {
let bitcoind_mock = BitcoindMock::new(MockOptions::default());
let bitcoind_mock = BitcoindMock::new(MockOptions::empty());
let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new()));
let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap());
let start_height = START_HEIGHT as u32;
start_server(bitcoind_mock);
let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
// Lets add some dummy data into the cache
for i in 0..10 {
carrier.issued_receipts.insert(
get_random_tx().compute_txid(),
get_random_tx().txid(),
ConfirmationStatus::ConfirmedIn(start_height - i),
);
}
@ -231,11 +265,11 @@ mod tests {
#[test]
fn test_send_transaction_ok() {
let bitcoind_mock = BitcoindMock::new(MockOptions::default());
let bitcoind_mock = BitcoindMock::new(MockOptions::empty());
let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new()));
let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap());
let start_height = START_HEIGHT as u32;
start_server(bitcoind_mock.server);
start_server(bitcoind_mock);
let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
let tx = consensus::deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap();
@ -244,25 +278,7 @@ mod tests {
assert_eq!(r, ConfirmationStatus::InMempoolSince(start_height));
// Check the receipt is on the cache
assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r);
}
#[test]
fn test_send_transaction_ok_already_in_mempool() {
let bitcoind_mock = BitcoindMock::new(MockOptions::in_mempool());
let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new()));
let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap());
let start_height = START_HEIGHT as u32;
start_server(bitcoind_mock.server);
let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
let tx = consensus::deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap();
let r = carrier.send_transaction(&tx);
assert_eq!(r, ConfirmationStatus::InMempoolSince(start_height));
// Check the receipt is on the cache
assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r);
assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r);
}
#[test]
@ -273,7 +289,7 @@ mod tests {
let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new()));
let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap());
let start_height = START_HEIGHT as u32;
start_server(bitcoind_mock.server);
start_server(bitcoind_mock);
let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
let tx = consensus::deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap();
@ -285,7 +301,7 @@ mod tests {
);
// Check the receipt is on the cache
assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r);
assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r);
}
#[test]
@ -295,7 +311,7 @@ mod tests {
let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new()));
let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap());
let start_height = START_HEIGHT as u32;
start_server(bitcoind_mock.server);
start_server(bitcoind_mock);
let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
let tx = consensus::deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap();
@ -307,27 +323,29 @@ mod tests {
);
// Check the receipt is on the cache
assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r);
assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r);
}
#[test]
fn test_send_transaction_verify_already_in_chain() {
let bitcoind_mock = BitcoindMock::new(MockOptions::with_error(
let bitcoind_mock = BitcoindMock::new(MockOptions::new(
rpc_errors::RPC_VERIFY_ALREADY_IN_CHAIN as i64,
BlockHash::default(),
START_HEIGHT,
));
let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new()));
let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap());
let start_height = START_HEIGHT as u32;
start_server(bitcoind_mock.server);
start_server(bitcoind_mock);
let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
let tx = consensus::deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap();
let r = carrier.send_transaction(&tx);
assert_eq!(r, ConfirmationStatus::IrrevocablyResolved);
assert_eq!(r, ConfirmationStatus::ConfirmedIn(start_height));
// Check the receipt is on the cache
assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r);
assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r);
}
#[test]
@ -337,7 +355,7 @@ mod tests {
let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new()));
let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap());
let start_height = START_HEIGHT as u32;
start_server(bitcoind_mock.server);
start_server(bitcoind_mock);
let mut carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
let tx = consensus::deserialize(&Vec::from_hex(TX_HEX).unwrap()).unwrap();
@ -349,13 +367,13 @@ mod tests {
);
// Check the receipt is on the cache
assert_eq!(carrier.issued_receipts.get(&tx.compute_txid()).unwrap(), &r);
assert_eq!(carrier.issued_receipts.get(&tx.txid()).unwrap(), &r);
}
#[test]
fn test_send_transaction_connection_error() {
// Try to connect to an offline bitcoind.
let bitcoind_mock = BitcoindMock::new(MockOptions::default());
let bitcoind_mock = BitcoindMock::new(MockOptions::empty());
let bitcoind_reachable = Arc::new((Mutex::new(false), Condvar::new()));
let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap());
let start_height = START_HEIGHT as u32;
@ -382,86 +400,91 @@ mod tests {
}
#[test]
fn test_in_mempool() {
let bitcoind_mock = BitcoindMock::new(MockOptions::in_mempool());
let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new()));
let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap());
let start_height = START_HEIGHT as u32;
start_server(bitcoind_mock.server);
let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
let txid = Txid::from_str(TXID_HEX).unwrap();
assert!(carrier.in_mempool(&txid));
}
#[test]
fn test_not_in_mempool() {
let bitcoind_mock = BitcoindMock::new(MockOptions::default());
let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new()));
let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap());
let start_height = START_HEIGHT as u32;
start_server(bitcoind_mock.server);
let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
let txid = Txid::from_str(TXID_HEX).unwrap();
assert!(!carrier.in_mempool(&txid));
}
#[test]
fn test_not_in_mempool_via_error() {
let bitcoind_mock = BitcoindMock::new(MockOptions::with_error(
rpc_errors::RPC_INVALID_ADDRESS_OR_KEY as i64,
));
let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new()));
let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap());
let start_height = START_HEIGHT as u32;
start_server(bitcoind_mock.server);
let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
let txid = Txid::from_str(TXID_HEX).unwrap();
assert!(!carrier.in_mempool(&txid));
}
#[test]
fn test_in_mempool_unexpected_error() {
fn test_get_tx_height_ok() {
let target_height = 21;
let bitcoind_mock =
BitcoindMock::new(MockOptions::with_error(rpc_errors::RPC_MISC_ERROR as i64));
BitcoindMock::new(MockOptions::with_block(BlockHash::default(), target_height));
let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new()));
let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap());
let start_height = START_HEIGHT as u32;
start_server(bitcoind_mock.server);
start_server(bitcoind_mock);
let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
let txid = Txid::from_str(TXID_HEX).unwrap();
assert!(!carrier.in_mempool(&txid));
}
#[test]
fn test_in_mempool_connection_error() {
// Try to connect to an offline bitcoind.
let bitcoind_mock = BitcoindMock::new(MockOptions::default());
let bitcoind_reachable = Arc::new((Mutex::new(false), Condvar::new()));
let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap());
let start_height = START_HEIGHT as u32;
let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable.clone(), start_height);
let txid = Txid::from_str(TXID_HEX).unwrap();
let delay = std::time::Duration::new(3, 0);
thread::spawn(move || {
thread::sleep(delay);
let (reachable, notifier) = &*bitcoind_reachable;
*reachable.lock().unwrap() = true;
notifier.notify_all();
});
let before = std::time::Instant::now();
carrier.in_mempool(&txid);
// Check the request has hanged for ~delay
let tx = consensus::deserialize::<Transaction>(&Vec::from_hex(TX_HEX).unwrap()).unwrap();
assert_eq!(
(std::time::Instant::now() - before).as_secs(),
delay.as_secs()
carrier.get_tx_height(&tx.txid()),
Some(target_height as u32)
);
}
#[test]
fn test_get_tx_height_not_found() {
// Hee we are not testing the case where the block hash is unknown (which will also return None). This is because we only
// learn block hashes from bitcoind, and once a block is known, it cannot disappear (ir can be disconnected, but not banish).
let bitcoind_mock = BitcoindMock::new(MockOptions::empty());
let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new()));
let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap());
let start_height = START_HEIGHT as u32;
start_server(bitcoind_mock);
let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
let tx = consensus::deserialize::<Transaction>(&Vec::from_hex(TX_HEX).unwrap()).unwrap();
assert_eq!(carrier.get_tx_height(&tx.txid()), None);
}
#[test]
fn test_get_block_height_ok() {
let target_height = 21;
let block_hash = BlockHash::default();
let bitcoind_mock = BitcoindMock::new(MockOptions::with_block(block_hash, target_height));
let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new()));
let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap());
let start_height = START_HEIGHT as u32;
start_server(bitcoind_mock);
let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
assert_eq!(
carrier.get_block_height(&block_hash),
Some(target_height as u32)
);
}
#[test]
fn test_get_block_height_not_found() {
let bitcoind_mock = BitcoindMock::new(MockOptions::empty());
let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new()));
let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap());
let start_height = START_HEIGHT as u32;
start_server(bitcoind_mock);
let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
assert_eq!(carrier.get_block_height(&BlockHash::default()), None);
}
#[test]
fn test_get_block_hash_for_tx_ok() {
let block_hash = BlockHash::default();
let bitcoind_mock = BitcoindMock::new(MockOptions::with_block(block_hash, 21));
let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new()));
let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap());
let start_height = START_HEIGHT as u32;
start_server(bitcoind_mock);
let tx = consensus::deserialize::<Transaction>(&Vec::from_hex(TX_HEX).unwrap()).unwrap();
let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
assert_eq!(carrier.get_block_hash_for_tx(&tx.txid()), Some(block_hash));
}
#[test]
fn test_get_block_hash_for_tx_not_found() {
let bitcoind_mock = BitcoindMock::new(MockOptions::empty());
let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new()));
let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap());
let start_height = START_HEIGHT as u32;
start_server(bitcoind_mock);
let tx = consensus::deserialize::<Transaction>(&Vec::from_hex(TX_HEX).unwrap()).unwrap();
let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, start_height);
assert_eq!(carrier.get_block_hash_for_tx(&tx.txid()), None);
}
}

View file

@ -26,7 +26,7 @@ where
{
/// A bitcoin client to poll best tips from.
spv_client: SpvClient<'a, P, C, L>,
/// The last known block header by the [ChainMonitor].
/// The lat known block header by the [ChainMonitor].
last_known_block_header: ValidatedBlockHeader,
/// A [DBM] (database manager) instance. Used to persist block data into disk.
dbm: Arc<Mutex<DBM>>,
@ -100,7 +100,7 @@ where
Err(e) => match e.kind() {
BlockSourceErrorKind::Persistent => {
// FIXME: This may need finer catching
log::error!("Unexpected persistent error: {e:?}");
log::error!("Unexpected persistent error: {:?}", e);
}
BlockSourceErrorKind::Transient => {
// Treating all transient as connection errors at least for now.
@ -135,8 +135,8 @@ mod tests {
use std::iter::FromIterator;
use std::thread;
use bitcoin::network::constants::Network;
use bitcoin::BlockHash;
use bitcoin::Network;
use lightning_block_sync::{poll::ChainPoller, SpvClient, UnboundedCache};
use crate::test_utils::{Blockchain, START_HEIGHT};
@ -156,18 +156,13 @@ mod tests {
}
impl chain::Listen for DummyListener {
fn filtered_block_connected(
&self,
header: &bitcoin::block::Header,
_: &chain::transaction::TransactionData,
_: u32,
) {
fn block_connected(&self, block: &bitcoin::Block, _: u32) {
self.connected_blocks
.borrow_mut()
.insert(header.block_hash());
.insert(block.block_hash());
}
fn block_disconnected(&self, header: &bitcoin::block::Header, _: u32) {
fn block_disconnected(&self, header: &bitcoin::BlockHeader, _: u32) {
self.disconnected_blocks
.borrow_mut()
.insert(header.block_hash());
@ -264,7 +259,10 @@ mod tests {
// If a new (worse, just one) block gets mined, nothing gets connected nor disconnected
cm.poll_best_tip().await;
assert_eq!(cm.last_known_block_header, best_tip);
assert!(cm.dbm.lock().unwrap().load_last_known_block().is_none());
assert!(matches!(
cm.dbm.lock().unwrap().load_last_known_block(),
Err { .. }
));
assert!(listener.connected_blocks.borrow().is_empty());
assert!(listener.disconnected_blocks.borrow().is_empty());
}

View file

@ -13,12 +13,6 @@ 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();
@ -26,14 +20,14 @@ async fn main() {
// Create data dir if it does not exist
fs::create_dir_all(&path).await.unwrap_or_else(|e| {
eprintln!("Cannot create data dir: {e:?}");
eprintln!("Cannot create data dir: {:?}", e);
std::process::exit(1);
});
let command = opt.command.clone();
// Load conf (from file or defaults) and patch it with the command line parameters received (if any)
let mut conf = config::from_file::<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"))
@ -53,17 +47,17 @@ async fn main() {
.ca_certificate(ca_cert)
.identity(Identity::from_pem(certificate, key));
let channel = Channel::from_shared(format!("https://{}:{}", conf.rpc_bind, conf.rpc_port))
let channel = Channel::from_shared(format!("http://{}:{}", conf.rpc_bind, conf.rpc_port))
.expect("Cannot create channel from endpoint")
.tls_config(tls)
.unwrap_or_else(|e| {
eprintln!("Could not configure tls: {e:?}");
eprintln!("Could not configure tls: {:?}", e);
std::process::exit(1);
})
.connect()
.await
.unwrap_or_else(|_| {
eprintln!("Could not connect to tower. Is teosd running?");
.unwrap_or_else(|e| {
eprintln!("Could not connect to tower: {:?}", e);
std::process::exit(1);
});
@ -86,10 +80,10 @@ async fn main() {
Ok(appointments) => {
println!("{}", pretty_json(&appointments.into_inner()).unwrap())
}
Err(status) => handle_error(status.message()),
Err(status) => println!("{}", status.message()),
}
}
Err(e) => handle_error(e),
Err(e) => println!("{}", e),
};
}
Command::GetTowerInfo => {
@ -112,10 +106,10 @@ async fn main() {
Ok(response) => {
println!("{}", pretty_json(&response.into_inner()).unwrap())
}
Err(status) => handle_error(status.message()),
Err(status) => println!("{}", status.message()),
}
}
Err(e) => handle_error(e),
Err(e) => println!("{}", e),
};
}
Command::Stop => {

View file

@ -4,7 +4,7 @@ use serde::Deserialize;
use structopt::StructOpt;
#[derive(Debug, StructOpt, Clone)]
#[structopt(rename_all = "lower_case")]
#[structopt(rename_all = "snake_case")]
pub enum Command {
/// Gets information about all appointments stored in the tower
GetAllAppointments,
@ -21,7 +21,7 @@ pub enum Command {
}
#[derive(Debug, StructOpt, Clone)]
#[structopt(rename_all = "snake_case")]
#[structopt(rename_all = "lowercase")]
pub struct GetUserData {
/// The user identifier (33-byte compressed public key).
pub user_id: String,
@ -37,7 +37,7 @@ pub struct GetAppointmentsData {
#[derive(StructOpt, Debug)]
#[structopt(rename_all = "lowercase")]
#[structopt(
version = env!("CARGO_PKG_VERSION"),
version = "0.0.1",
about = "The Eye of Satoshi - CLI",
name = "teos-cli"
)]
@ -54,6 +54,10 @@ pub struct Opt {
#[structopt(long, default_value = "~/.teos")]
pub data_dir: String,
/// Runs teos-cli in debug mode [default: false]
#[structopt(long)]
pub debug: bool,
/// Command
#[structopt(subcommand)]
pub command: Command,
@ -70,6 +74,7 @@ pub struct Opt {
pub struct Config {
pub rpc_bind: String,
pub rpc_port: u16,
pub debug: bool,
}
impl Config {
@ -81,6 +86,8 @@ impl Config {
if options.rpc_port.is_some() {
self.rpc_port = options.rpc_port.unwrap();
}
self.debug |= options.debug;
}
}
@ -95,6 +102,7 @@ impl Default for Config {
Self {
rpc_bind: "localhost".into(),
rpc_port: 8814,
debug: false,
}
}
}

View file

@ -2,7 +2,7 @@
api_bind = "127.0.0.1"
api_port = 9814
tor_control_port = 9051
onion_hidden_service_port = 9814
onion_hidden_service_port = 2121
tor_support = false
# RPC
@ -10,17 +10,14 @@ rpc_bind = "127.0.0.1"
rpc_port = 8814
# bitcoind
btc_network = "mainnet"
btc_network = "bitcoin"
btc_rpc_user = "CSW"
## Notice only user+password **OR** cookie is allowed as rpc auth, any other combination would be rejected
btc_rpc_password = "NotSatoshi"
btc_rpc_connect = "localhost"
btc_rpc_cookie = "~/.bitcoin/.cookie"
btc_rpc_port = 8332
# Flags
debug = false
deps_debug = false
overwrite_key = false
# General

View file

@ -1,7 +1,10 @@
//! Logic related to the tower configuration and command line parameter parsing.
use serde::{Deserialize, Serialize};
use bitcoin::network::constants::Network;
use serde::Deserialize;
use std;
use std::path::PathBuf;
use std::str::FromStr;
use structopt::StructOpt;
pub fn data_dir_absolute_path(data_dir: String) -> PathBuf {
@ -16,12 +19,15 @@ pub fn data_dir_absolute_path(data_dir: String) -> PathBuf {
}
}
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).unwrap_or_else(|e| {
eprintln!("Couldn't parse config file: {e}");
T::default()
}),
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,
),
Err(_) => T::default(),
}
}
@ -38,18 +44,10 @@ impl std::fmt::Display for ConfigError {
impl std::error::Error for ConfigError {}
#[derive(PartialEq)]
pub enum AuthMethod {
UserPass,
CookieFile,
Multiple,
Invalid,
}
/// Holds all the command line options.
#[derive(StructOpt, Debug, Clone)]
#[structopt(rename_all = "lowercase")]
#[structopt(version = env!("CARGO_PKG_VERSION"), about = "The Eye of Satoshi - Lightning watchtower")]
#[structopt(version = "0.0.1", about = "The Eye of Satoshi - Lightning watchtower")]
pub struct Opt {
/// Address teos HTTP(s) API will bind to [default: localhost]
#[structopt(long)]
@ -67,22 +65,18 @@ pub struct Opt {
#[structopt(long)]
pub rpc_port: Option<u16>,
/// Network bitcoind is connected to. Either mainnet, testnet, signet or regtest [default: mainnet]
/// Network bitcoind is connected to. Either bitcoin, testnet, signet or regtest [default: bitcoin]
#[structopt(long)]
pub btc_network: Option<String>,
/// bitcoind rpcuser
/// bitcoind rpcuser [default: user]
#[structopt(long)]
pub btc_rpc_user: Option<String>,
/// bitcoind rpcpassword
/// bitcoind rpcpassword [default: passwd]
#[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>,
@ -99,28 +93,19 @@ pub struct Opt {
#[structopt(long)]
pub debug: bool,
/// Runs third party libs in debug mode
#[structopt(long)]
pub deps_debug: bool,
/// Overwrites the tower secret key. THIS IS IRREVERSIBLE AND WILL CHANGE YOUR TOWER ID
#[structopt(long)]
pub overwrite_key: bool,
/// If set, creates a Tor endpoint to serve API data. This endpoint is additional to the clearnet HTTP API
/// If set, creates a tor endpoint to serve API data. This endpoint is additional to the clearnet HTTP API
#[structopt(long)]
pub tor_support: bool,
/// 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]
/// tor control port [default: 9051]
#[structopt(long)]
pub tor_control_port: Option<u16>,
/// Port for the onion hidden service to listen on [default: 9814]
/// Port for the onion hidden service to listen on [default: 2121]
#[structopt(long)]
pub onion_hidden_service_port: Option<u16>,
}
@ -131,7 +116,7 @@ pub struct Opt {
/// - Defaults
/// - Configuration file
/// - Command line options
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
#[serde(default)]
pub struct Config {
// API
@ -145,16 +130,13 @@ pub struct Config {
// Bitcoind
pub btc_network: String,
pub btc_rpc_user: String,
pub btc_rpc_cookie: String,
pub btc_rpc_password: String,
pub btc_rpc_connect: String,
pub btc_rpc_port: u16,
// Flags
pub debug: bool,
pub deps_debug: bool,
pub overwrite_key: bool,
pub force_update: bool,
// General
pub subscription_slots: u32,
@ -174,24 +156,6 @@ 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() {
@ -215,9 +179,6 @@ 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();
}
@ -233,9 +194,7 @@ impl Config {
self.tor_support |= options.tor_support;
self.debug |= options.debug;
self.deps_debug |= options.deps_debug;
self.overwrite_key = options.overwrite_key;
self.force_update = options.force_update;
}
/// Verifies that [Config] is properly built.
@ -247,63 +206,38 @@ impl Config {
/// This will also assign the default `btc_rpc_port` depending on the network if it has not
/// been overwritten at this point.
pub fn verify(&mut self) -> Result<(), ConfigError> {
let auth_method = self.get_auth_method();
if auth_method == AuthMethod::Invalid {
return Err(ConfigError("No valid bitcoind auth provided. Set either both btc_rpc_user/btc_rpc_password or btc_rpc_cookie".to_owned()));
} else if auth_method == AuthMethod::Multiple {
return Err(ConfigError(
"Multiple bitcoind auth provided. Pick a single one (either btc_rpc_user/btc_rpc_password or btc_rpc_cookie)"
.to_owned(),
));
if self.btc_rpc_user == String::new() {
return Err(ConfigError("btc_rpc_user must be set".to_owned()));
}
if self.btc_rpc_password == String::new() {
return Err(ConfigError("btc_rpc_password must be set".to_owned()));
}
// Normalize the network option to the ones used by bitcoind.
if ["mainnet", "testnet"].contains(&self.btc_network.as_str()) {
self.btc_network = self.btc_network.trim_end_matches("net").into();
match Network::from_str(&self.btc_network) {
Ok(network) => {
// Set the port to it's default (depending on the network) if it has not been
// overwritten at this point.
if self.btc_rpc_port == 0 {
self.btc_rpc_port = match network {
Network::Testnet => 18332,
Network::Signet => 38332,
Network::Regtest => 18443,
_ => 8332,
}
}
Ok(())
}
Err(_) => {
Err(ConfigError(format!("btc_network not recognized. Expected {{bitcoin, testnet, signet, regtest}}, received {}",
self.btc_network)))
}
}
let default_rpc_port = match self.btc_network.as_str() {
"main" => 8332,
"test" => 18332,
"regtest" => 18443,
"signet" => 38332,
_ => return Err(ConfigError(format!("btc_network not recognized. Expected {{mainnet, testnet, signet, regtest}}, received {}", self.btc_network)))
};
// Set the port to it's default (depending on the network) if it has not been
// overwritten at this point.
if self.btc_rpc_port == 0 {
self.btc_rpc_port = default_rpc_port;
}
Ok(())
}
/// Checks whether the config has been set with only with default values.
pub fn is_default(&self) -> bool {
self == &Config::default()
}
/// Logs non-default options.
pub fn log_non_default_options(&self) {
let json_default_config = serde_json::json!(&Config::default());
let json_config = serde_json::json!(&self);
let sensitive_args = ["btc_rpc_user", "btc_rpc_password"];
for (key, value) in json_config.as_object().unwrap().iter() {
if *value != json_default_config[key] {
log::info!(
"Custom config arg: {}: {}",
key,
if sensitive_args.contains(&key.as_str()) {
"****".to_owned()
} else {
value.to_string()
}
);
}
}
}
}
impl Default for Config {
@ -319,20 +253,17 @@ impl Default for Config {
api_port: 9814,
tor_support: false,
tor_control_port: 9051,
onion_hidden_service_port: 9814,
onion_hidden_service_port: 2121,
rpc_bind: "127.0.0.1".into(),
rpc_port: 8814,
btc_network: "mainnet".into(),
btc_network: "bitcoin".into(),
btc_rpc_user: String::new(),
btc_rpc_password: String::new(),
btc_rpc_cookie: String::new(),
btc_rpc_connect: "localhost".into(),
btc_rpc_port: 0,
debug: false,
deps_debug: false,
overwrite_key: false,
force_update: false,
subscription_slots: 10000,
subscription_duration: 4320,
expiry_delta: 6,
@ -361,15 +292,12 @@ 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"),
debug: false,
deps_debug: false,
overwrite_key: false,
force_update: false,
}
}
}
@ -389,7 +317,7 @@ mod tests {
assert_eq!(config.api_bind, expected_value);
// Check the rest of fields are equal. The easiest is to just the field back and compare with a clone
config.api_bind.clone_from(&config_clone.api_bind);
config.api_bind = config_clone.api_bind.clone();
assert_eq!(config, config_clone);
}
@ -398,9 +326,7 @@ mod tests {
// Tests that the default configuration does not pass verification checks. This is on purpose so some fields are
// required to be updated by the user.
let mut config = Config::default();
assert!(
matches!(config.verify(), Err(ConfigError(e)) if e.contains("No valid bitcoind auth provided"))
);
assert!(matches!(config.verify(), Err(ConfigError { .. })));
}
#[test]
@ -423,9 +349,7 @@ mod tests {
btc_network: "wrong_network".to_owned(),
..Default::default()
};
assert!(
matches!(config.verify(), Err(ConfigError(e)) if e.contains("btc_network not recognized"))
);
assert!(matches!(config.verify(), Err(ConfigError { .. })));
}
#[test]

File diff suppressed because it is too large Load diff

View file

@ -22,8 +22,8 @@ impl UUID {
/// Therefore, it provides a hard-to-forge id while reducing the tower lookups and the required data to be stored (no reverse maps).
pub fn new(locator: Locator, user_id: UserId) -> Self {
let mut uuid_data = locator.to_vec();
uuid_data.extend(user_id.0.serialize());
UUID(ripemd160::Hash::hash(&uuid_data).to_byte_array())
uuid_data.extend(&user_id.0.serialize());
UUID(ripemd160::Hash::hash(&uuid_data).into_inner())
}
/// Serializes the [UUID] returning its byte representation.
@ -46,6 +46,8 @@ impl std::fmt::Display for UUID {
/// An extended version of the appointment hold by the tower.
///
/// The [Appointment] is extended in terms of data, that is, it provides further information only relevant to the tower.
/// Notice [ExtendedAppointment]s are not kept in memory but persisted on disk. The [Watcher](crate::watcher::Watcher)
/// keeps [AppointmentSummary] instead.
#[derive(Debug, Eq, PartialEq, Clone)]
pub(crate) struct ExtendedAppointment {
/// The underlying appointment extended by [ExtendedAppointment].
@ -58,6 +60,18 @@ pub(crate) struct ExtendedAppointment {
pub start_block: u32,
}
/// A summary of an appointment.
///
/// Contains the minimal amount of data the [Watcher](crate::watcher::Watcher) needs to keep in memory in order to
/// watch for breaches.
#[derive(Debug, Eq, PartialEq, Clone)]
pub(crate) struct AppointmentSummary {
/// The [Appointment] locator.
pub locator: Locator,
/// The user this [Appointment] belongs to.
pub user_id: UserId,
}
impl ExtendedAppointment {
/// Create a new [ExtendedAppointment].
pub fn new(
@ -89,8 +103,12 @@ impl ExtendedAppointment {
self.inner.to_self_delay
}
pub fn uuid(&self) -> UUID {
UUID::new(self.inner.locator, self.user_id)
/// Computes the summary of the [ExtendedAppointment].
pub fn get_summary(&self) -> AppointmentSummary {
AppointmentSummary {
locator: self.locator(),
user_id: self.user_id,
}
}
}
@ -98,14 +116,22 @@ impl ExtendedAppointment {
mod tests {
use super::*;
use crate::test_utils::generate_uuid;
use teos_common::appointment::Appointment;
use teos_common::cryptography::get_random_bytes;
use teos_common::test_utils::get_random_user_id;
#[test]
fn test_uuid_ser_deser() {
let original_uuid = generate_uuid();
assert_eq!(
UUID::from_slice(&original_uuid.to_vec()).unwrap(),
original_uuid
);
fn test_get_summary() {
let locator = Locator::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);
}
}

View file

@ -1,11 +1,13 @@
//! Logic related to the Gatekeeper, the component in charge of managing access to the tower resources.
use lightning::chain;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::iter::FromIterator;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex};
use teos_common::appointment::{compute_appointment_slots, Locator};
use lightning::chain;
use teos_common::appointment::compute_appointment_slots;
use teos_common::constants::ENCRYPTED_BLOB_MAX_SIZE;
use teos_common::cryptography;
use teos_common::receipts::RegistrationReceipt;
@ -15,7 +17,7 @@ use crate::dbm::DBM;
use crate::extended_appointment::{ExtendedAppointment, UUID};
/// Data regarding a user subscription with the tower.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct UserInfo {
/// Number of appointment slots available for a given user.
pub(crate) available_slots: u32,
@ -23,6 +25,8 @@ 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 {
@ -32,6 +36,22 @@ 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,
}
}
}
@ -114,9 +134,8 @@ impl Gatekeeper {
}
/// Gets the data held by the tower about a given user.
pub(crate) fn get_user_info(&self, user_id: UserId) -> Option<(UserInfo, Vec<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)))
pub(crate) fn get_user_info(&self, user_id: UserId) -> Option<UserInfo> {
self.registered_users.lock().unwrap().get(&user_id).cloned()
}
/// Authenticates a user.
@ -156,10 +175,7 @@ impl Gatekeeper {
.available_slots
.checked_add(self.subscription_slots)
.ok_or(MaxSlotsReached)?;
user_info.subscription_expiry = user_info
.subscription_expiry
.checked_add(self.subscription_duration)
.unwrap_or(u32::MAX);
user_info.subscription_expiry = block_count + self.subscription_duration;
self.dbm.lock().unwrap().update_user(user_id, user_info);
user_info
@ -200,13 +216,7 @@ impl Gatekeeper {
// For updates, the difference between the existing appointment size and the update is computed.
let mut registered_users = self.registered_users.lock().unwrap();
let user_info = registered_users.get_mut(&user_id).unwrap();
let used_blob_size = self
.dbm
.lock()
.unwrap()
.get_appointment_length(uuid)
.unwrap_or(0);
let used_slots = compute_appointment_slots(used_blob_size, ENCRYPTED_BLOB_MAX_SIZE);
let used_slots = user_info.appointments.get(&uuid).map_or(0, |x| *x);
let required_slots =
compute_appointment_slots(appointment.encrypted_blob().len(), ENCRYPTED_BLOB_MAX_SIZE);
@ -215,6 +225,7 @@ impl Gatekeeper {
if diff <= user_info.available_slots as i64 {
// Filling / freeing slots depending on whether this is an update or not, and if it is bigger or smaller
// than the old appointment
user_info.appointments.insert(uuid, required_slots);
user_info.available_slots = (user_info.available_slots as i64 - diff) as u32;
self.dbm.lock().unwrap().update_user(user_id, user_info);
@ -244,50 +255,56 @@ impl Gatekeeper {
/// Gets a map of outdated users. Outdated users are those whose subscription has expired and the renewal grace period
/// has already passed ([expiry_delta](Self::expiry_delta)).
pub(crate) fn get_outdated_users(&self, block_height: u32) -> Vec<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)
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()))
.collect()
}
/// Deletes these appointments from the database and updates the user's information.
///
/// If `refund` is set, the appointments owners will get their slots refunded back.
///
/// DISCUSS: When `refund` is `false` we don't give back the slots to the user for the deleted appointments.
/// This is to discourage misbehavior (sending bad appointments, either non-decryptable or rejected by the network).
pub(crate) fn delete_appointments(&self, appointments: Vec<UUID>, refund: bool) {
let mut dbm = self.dbm.lock().unwrap();
/// Gets a set of outdated user ids.
pub(crate) fn get_outdated_user_ids(&self, block_height: u32) -> HashSet<UserId> {
self.get_outdated_users(block_height)
.keys()
.cloned()
.collect()
}
let updated_users = if refund {
let mut updated_users = HashMap::new();
let mut registered_users = self.registered_users.lock().unwrap();
// Give back the consumed slots to each user.
for uuid in appointments.iter() {
let (user_id, blob_size) = dbm.get_appointment_user_and_length(*uuid).unwrap();
registered_users.get_mut(&user_id).unwrap().available_slots +=
compute_appointment_slots(blob_size, ENCRYPTED_BLOB_MAX_SIZE);
updated_users.insert(user_id, registered_users[&user_id]);
}
updated_users
} else {
// No updated users.
HashMap::new()
};
/// Get a map of outdated appointments (from any user).
pub(crate) fn get_outdated_appointments(&self, block_height: u32) -> HashSet<UUID> {
HashSet::from_iter(
self.get_outdated_users(block_height)
.into_values()
.flatten(),
)
}
// An optimization for the case when only one appointment is being deleted without refunding.
// This avoids creating a DB transaction for a single query.
if appointments.len() == 1 && updated_users.is_empty() {
dbm.remove_appointment(appointments[0])
} else {
dbm.batch_remove_appointments(&appointments, &updated_users);
/// Deletes a collection of appointments from the users' subscriptions (from memory only)
/// and updates the available_slots count for the given user.
///
/// Notice appointments are only de-linked from users, but not actually removed. This is because the [Gatekeeper]
/// does not actually hold any [ExtendedAppointment](crate::extended_appointment::ExtendedAppointment) data,
/// just references to them.
pub(crate) fn delete_appointments_from_memory(
&self,
appointments: &HashMap<UUID, UserId>,
) -> HashMap<UserId, UserInfo> {
let mut updated_users = HashMap::new();
let mut registered_users = self.registered_users.lock().unwrap();
for (uuid, user_id) in appointments {
// Remove the appointment from the appointment list and update the available slots
if let Some(user_info) = registered_users.get_mut(user_id) {
if let Some(x) = user_info.appointments.remove(uuid) {
user_info.available_slots += x;
}
updated_users.insert(*user_id, user_info.clone());
};
}
updated_users
}
}
@ -295,28 +312,16 @@ impl chain::Listen for Gatekeeper {
/// Handles the monitoring process by the [Gatekeeper].
///
/// This is mainly used to keep track of time and expire / outdate subscriptions when needed.
fn filtered_block_connected(
&self,
header: &bitcoin::block::Header,
_: &chain::transaction::TransactionData,
height: u32,
) {
log::info!("New block received: {}", header.block_hash());
fn block_connected(&self, block: &bitcoin::Block, height: u32) {
log::info!("New block received: {}", block.block_hash());
// Expired user deletion is delayed. Users are deleted when their subscription is outdated, not expired.
let outdated_users = self.get_outdated_users(height);
if !outdated_users.is_empty() {
// Remove the outdated users from memory first.
{
let mut registered_users = self.registered_users.lock().unwrap();
// Removing each outdated user in a loop is more efficient than retaining non-outdated users
// because retaining would loop over all the available users which is always more than the outdated ones.
for outdated_user in outdated_users.iter() {
registered_users.remove(outdated_user);
}
}
self.dbm.lock().unwrap().batch_remove_users(&outdated_users);
}
let outdated_users = self.get_outdated_user_ids(height);
self.registered_users
.lock()
.unwrap()
.retain(|id, _| !outdated_users.contains(id));
self.dbm.lock().unwrap().batch_remove_users(&outdated_users);
// Update last known block height
self.last_known_block_height
@ -324,7 +329,7 @@ impl chain::Listen for Gatekeeper {
}
/// Handles reorgs in the [Gatekeeper]. Simply updates the last_known_block_height.
fn block_disconnected(&self, header: &bitcoin::block::Header, height: u32) {
fn block_disconnected(&self, header: &bitcoin::BlockHeader, height: u32) {
log::warn!("Block disconnected: {}", header.block_hash());
// There's nothing to be done here but updating the last known block
self.last_known_block_height
@ -336,13 +341,14 @@ impl chain::Listen for Gatekeeper {
mod tests {
use super::*;
use crate::test_utils::{generate_dummy_appointment_with_user, get_random_tracker, Blockchain};
use crate::test_utils::{
generate_dummy_appointment, generate_dummy_appointment_with_user, generate_uuid, 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;
@ -365,11 +371,21 @@ mod tests {
&self.registered_users
}
pub(crate) fn add_outdated_user(&self, user_id: UserId, outdates_at: u32) {
pub(crate) fn add_outdated_user(
&self,
user_id: UserId,
outdates_at: u32,
appointments: Option<Vec<UUID>>,
) {
self.add_update_user(user_id).unwrap();
let mut registered_users = self.registered_users.lock().unwrap();
let user = registered_users.get_mut(&user_id).unwrap();
let mut user = registered_users.get_mut(&user_id).unwrap();
user.subscription_expiry = outdates_at - self.expiry_delta;
if let Some(uuids) = appointments {
for uuid in uuids.iter() {
user.appointments.insert(*uuid, 1);
}
}
}
}
@ -435,7 +451,7 @@ mod tests {
// Let's now provide data generated by an actual user, still the user is unknown
let (user_sk, user_pk) = get_random_keypair();
let signature = cryptography::sign(message, &user_sk);
let signature = cryptography::sign(message, &user_sk).unwrap();
assert_eq!(
gatekeeper.authenticate_user(message, &signature),
Err(AuthenticationFailure("User not found."))
@ -478,10 +494,13 @@ mod tests {
.store(chain.get_block_count(), Ordering::Relaxed);
let updated_receipt = gatekeeper.add_update_user(user_id).unwrap();
assert_eq!(updated_receipt.available_slots(), SLOTS * 2);
assert_eq!(
updated_receipt.available_slots(),
receipt.available_slots() * 2
);
assert_eq!(
updated_receipt.subscription_expiry(),
START_HEIGHT as u32 + DURATION * 2
receipt.subscription_expiry() + 1
);
// Data in the database should have been updated too
@ -542,32 +561,25 @@ mod tests {
let available_slots = gatekeeper
.add_update_appointment(user_id, uuid, &appointment)
.unwrap();
// Simulate the watcher adding the appointment in the database.
gatekeeper
.dbm
.lock()
.unwrap()
.store_appointment(uuid, &appointment)
.unwrap();
let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap();
assert!(user_locators.contains(&appointment.locator()));
assert!(gatekeeper.registered_users.lock().unwrap()[&user_id]
.appointments
.contains_key(&uuid));
assert_eq!(slots_before, available_slots + 1);
// Slots should have been updated in the database too.
// Slots should have been updated in the database too. Notice the appointment won't be there yet
// given the Watcher is responsible for adding it, and it will do so after calling this method
let mut loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap();
assert_eq!(loaded_user.available_slots, available_slots);
// Adding the exact same appointment should leave the slots count unchanged.
// We don't really need to update the appointment in the DB since it's the very same appointment.
// Adding the exact same appointment should leave the slots count unchanged
let mut updated_slot_count = gatekeeper
.add_update_appointment(user_id, uuid, &appointment)
.unwrap();
let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap();
assert!(user_locators.contains(&appointment.locator()));
assert!(gatekeeper.registered_users.lock().unwrap()[&user_id]
.appointments
.contains_key(&uuid));
assert_eq!(updated_slot_count, available_slots);
loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap();
assert_eq!(loaded_user.available_slots, updated_slot_count);
@ -577,18 +589,10 @@ mod tests {
updated_slot_count = gatekeeper
.add_update_appointment(user_id, uuid, &bigger_appointment)
.unwrap();
// Simulate the watcher updating the appointment in the database.
gatekeeper
.dbm
.lock()
.unwrap()
.update_appointment(uuid, &bigger_appointment)
.unwrap();
let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap();
assert!(user_locators.contains(&appointment.locator()));
assert!(gatekeeper.registered_users.lock().unwrap()[&user_id]
.appointments
.contains_key(&uuid));
assert_eq!(updated_slot_count, available_slots - 1);
loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap();
assert_eq!(loaded_user.available_slots, updated_slot_count);
@ -596,43 +600,26 @@ mod tests {
updated_slot_count = gatekeeper
.add_update_appointment(user_id, uuid, &appointment)
.unwrap();
// Simulate the watcher updating the appointment in the database.
gatekeeper
.dbm
.lock()
.unwrap()
.update_appointment(uuid, &appointment)
.unwrap();
let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap();
assert!(user_locators.contains(&appointment.locator()));
assert!(gatekeeper.registered_users.lock().unwrap()[&user_id]
.appointments
.contains_key(&uuid));
assert_eq!(updated_slot_count, available_slots);
loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap();
assert_eq!(loaded_user.available_slots, updated_slot_count);
// Adding an appointment with a different uuid should not count as an update
let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None);
let new_uuid = generate_uuid();
updated_slot_count = gatekeeper
.add_update_appointment(user_id, uuid, &appointment)
.add_update_appointment(user_id, new_uuid, &appointment)
.unwrap();
// Simulate the watcher adding the appointment in the database.
gatekeeper
.dbm
.lock()
.unwrap()
.store_appointment(uuid, &appointment)
.unwrap();
let (_, user_locators) = gatekeeper.get_user_info(user_id).unwrap();
assert!(user_locators.contains(&appointment.locator()));
assert!(gatekeeper.registered_users.lock().unwrap()[&user_id]
.appointments
.contains_key(&new_uuid));
assert_eq!(updated_slot_count, available_slots - 1);
loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap();
assert_eq!(loaded_user.available_slots, updated_slot_count);
// Finally, trying to add an appointment when the user has no enough slots should fail
let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None);
gatekeeper
.registered_users
.lock()
@ -641,10 +628,9 @@ mod tests {
.unwrap()
.available_slots = 0;
assert!(matches!(
gatekeeper.add_update_appointment(user_id, uuid, &appointment),
gatekeeper.add_update_appointment(user_id, generate_uuid(), &appointment),
Err(NotEnoughSlots)
));
// The entry in the database should remain unchanged in this case
loaded_user = gatekeeper.dbm.lock().unwrap().load_user(user_id).unwrap();
assert_eq!(loaded_user.available_slots, updated_slot_count);
@ -690,179 +676,148 @@ mod tests {
// Initially, there are not outdated users, so querying any block height should return an empty map
for i in 0..start_height {
assert_eq!(gatekeeper.get_outdated_users(i), vec![]);
assert_eq!(gatekeeper.get_outdated_users(i).len(), 0);
}
// Adding a user whose subscription is outdated should return an entry
let user_id = get_random_user_id();
gatekeeper.add_update_user(user_id).unwrap();
// Add also an appointment so we can check the returned data
let appointment = generate_dummy_appointment(None);
let uuid = generate_uuid();
gatekeeper
.add_update_appointment(user_id, uuid, &appointment)
.unwrap();
// Check that data is not yet outdated
assert_eq!(gatekeeper.get_outdated_users(start_height), vec![]);
assert_eq!(gatekeeper.get_outdated_users(start_height).len(), 0);
// Add an outdated user and check again
gatekeeper.add_outdated_user(user_id, start_height);
assert_eq!(gatekeeper.get_outdated_users(start_height), vec![user_id]);
gatekeeper.add_outdated_user(user_id, start_height, None);
let outdated_users = gatekeeper.get_outdated_users(start_height);
assert_eq!(outdated_users.len(), 1);
assert_eq!(outdated_users[&user_id], HashSet::from_iter([uuid]));
}
#[test]
fn test_delete_appointments_without_refund() {
let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT));
let n_users = 100;
let n_apps = 10;
let mut uuids_to_delete = Vec::new();
let mut rest = Vec::new();
let mut trackers = Vec::new();
let mut users_info = HashMap::new();
fn test_get_outdated_appointments() {
let start_height = START_HEIGHT as u32 + EXPIRY_DELTA;
let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(start_height as usize));
for _ in 0..n_users {
// get_outdated_appointments returns a list of appointments that were outdated at a given block height, indistinguishably of their user.
// If there are no outdated users, there cannot be outdated appointments
for i in 0..start_height {
assert_eq!(gatekeeper.get_outdated_appointments(i).len(), 0);
}
// Adding data about different users and appointments should return a flattened list of appointments
let user1_id = get_random_user_id();
let user2_id = get_random_user_id();
let uuid1 = generate_uuid();
let uuid2 = generate_uuid();
// Manually set the user expiry for the test
for (user_id, uuid) in [(user1_id, uuid1), (user2_id, uuid2)] {
gatekeeper.add_outdated_user(user_id, start_height, Some(Vec::from_iter([uuid])));
}
let outdated_appointments = gatekeeper.get_outdated_appointments(start_height);
assert_eq!(outdated_appointments.len(), 2);
assert!(outdated_appointments.contains(&uuid1));
assert!(outdated_appointments.contains(&uuid2));
}
#[test]
fn test_delete_appointments_from_memory() {
let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT));
// delete_appointments will remove a list of appointments from the Gatekeeper (as long as they exist)
let mut all_appointments = HashMap::new();
let mut to_be_deleted = HashMap::new();
let mut rest = HashMap::new();
for i in 1..11 {
let user_id = get_random_user_id();
gatekeeper.add_update_user(user_id).unwrap();
for i in 0..n_apps {
let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None);
gatekeeper
.add_update_appointment(user_id, uuid, &appointment)
.unwrap();
// Add the appointment to the database. This is normally done by the Watcher.
let uuid = generate_uuid();
all_appointments.insert(uuid, user_id);
if i % 2 == 0 {
to_be_deleted.insert(uuid, user_id);
} else {
rest.insert(uuid, user_id);
}
}
// Calling the method with unknown data should work but do nothing
assert!(gatekeeper.registered_users.lock().unwrap().is_empty());
assert!(gatekeeper
.delete_appointments_from_memory(&all_appointments)
.is_empty());
// If there's matching data in the gatekeeper it should be deleted
for (uuid, user_id) in to_be_deleted.iter() {
gatekeeper.add_update_user(*user_id).unwrap();
gatekeeper
.add_update_appointment(*user_id, *uuid, &generate_dummy_appointment(None))
.unwrap();
}
// Check before deleting
assert_eq!(gatekeeper.registered_users.lock().unwrap().len(), 5);
for (uuid, user_id) in to_be_deleted.iter() {
assert!(gatekeeper.registered_users.lock().unwrap()[user_id]
.appointments
.contains_key(uuid));
// The slot count should be decreased now too (both in memory and in the database)
assert_ne!(
gatekeeper.registered_users.lock().unwrap()[user_id].available_slots,
gatekeeper.subscription_slots
);
assert_ne!(
gatekeeper
.dbm
.lock()
.unwrap()
.store_appointment(uuid, &appointment)
.unwrap();
if i % 2 == 0 {
uuids_to_delete.push(uuid);
} else {
rest.push(uuid);
}
// Also trigger some of these appointments as trackers.
if i % 5 == 0 {
gatekeeper
.dbm
.lock()
.unwrap()
.store_tracker(
uuid,
&get_random_tracker(user_id, ConfirmationStatus::ConfirmedIn(42)),
)
.unwrap();
trackers.push(uuid);
}
}
users_info.insert(user_id, gatekeeper.get_user_info(user_id).unwrap().0);
}
// Delete these appointments without refunding their owners.
gatekeeper.delete_appointments(uuids_to_delete.clone(), false);
for uuid in uuids_to_delete.clone() {
assert!(!gatekeeper.dbm.lock().unwrap().appointment_exists(uuid));
}
for uuid in rest {
assert!(gatekeeper.dbm.lock().unwrap().appointment_exists(uuid));
}
for uuid in trackers {
if uuids_to_delete.contains(&uuid) {
// The tracker should be deleted as well.
assert!(!gatekeeper.dbm.lock().unwrap().tracker_exists(uuid));
} else {
assert!(gatekeeper.dbm.lock().unwrap().tracker_exists(uuid));
}
}
for (user_id, user_info_before_deletion) in users_info {
// Since `refund` was false, the users' slots should not have changed after deleting appointments.
let (user_info_after_deletion, _) = gatekeeper.get_user_info(user_id).unwrap();
assert_eq!(user_info_after_deletion, user_info_before_deletion);
}
}
#[test]
fn test_delete_appointments_with_refund() {
let gatekeeper = init_gatekeeper(&Blockchain::default().with_height(START_HEIGHT));
let n_users = 100;
let n_apps = 10;
let mut uuids_to_delete = Vec::new();
let mut rest = Vec::new();
let mut trackers = Vec::new();
let mut users_remaining_slots = HashMap::new();
for _ in 0..n_users {
let user_id = get_random_user_id();
gatekeeper.add_update_user(user_id).unwrap();
let mut user_remaining_slots =
gatekeeper.get_user_info(user_id).unwrap().0.available_slots;
for i in 0..n_apps {
let (uuid, appointment) = generate_dummy_appointment_with_user(user_id, None);
gatekeeper
.add_update_appointment(user_id, uuid, &appointment)
.unwrap();
// Add the appointment to the database. This is normally done by the Watcher.
gatekeeper
.dbm
.lock()
.load_user(*user_id)
.unwrap()
.store_appointment(uuid, &appointment)
.unwrap();
if i % 2 == 0 {
// We don't reduce the remaining slots for the appointments which are
// going to delete since we will refund their owners.
uuids_to_delete.push(uuid);
} else {
rest.push(uuid);
user_remaining_slots -= compute_appointment_slots(
appointment.encrypted_blob().len(),
ENCRYPTED_BLOB_MAX_SIZE,
);
}
// Also trigger some of these appointments as trackers.
if i % 5 == 0 {
gatekeeper
.dbm
.lock()
.unwrap()
.store_tracker(
uuid,
&get_random_tracker(user_id, ConfirmationStatus::ConfirmedIn(42)),
)
.unwrap();
trackers.push(uuid);
}
}
users_remaining_slots.insert(user_id, user_remaining_slots);
}
// Delete these appointments and refund their owners their slots back.
gatekeeper.delete_appointments(uuids_to_delete.clone(), true);
for uuid in uuids_to_delete.clone() {
assert!(!gatekeeper.dbm.lock().unwrap().appointment_exists(uuid));
}
for uuid in rest {
assert!(gatekeeper.dbm.lock().unwrap().appointment_exists(uuid));
}
for uuid in trackers {
if uuids_to_delete.contains(&uuid) {
// The tracker should be deleted as well.
assert!(!gatekeeper.dbm.lock().unwrap().tracker_exists(uuid));
} else {
assert!(gatekeeper.dbm.lock().unwrap().tracker_exists(uuid));
}
}
for (user_id, correct_remaining_slots) in users_remaining_slots {
let remaining_slots_from_db =
gatekeeper.get_user_info(user_id).unwrap().0.available_slots;
assert_eq!(remaining_slots_from_db, correct_remaining_slots);
assert_eq!(
gatekeeper.registered_users.lock().unwrap()[&user_id].available_slots,
correct_remaining_slots
.available_slots,
gatekeeper.subscription_slots
);
}
for (_, user_id) in rest.iter() {
assert!(!gatekeeper
.registered_users
.lock()
.unwrap()
.contains_key(user_id));
}
// And after
gatekeeper.delete_appointments_from_memory(&all_appointments);
for (uuid, user_id) in to_be_deleted.iter() {
assert!(!gatekeeper.registered_users.lock().unwrap()[user_id]
.appointments
.contains_key(uuid));
// The slot count is back to default
assert_eq!(
gatekeeper.registered_users.lock().unwrap()[user_id].available_slots,
gatekeeper.subscription_slots
);
}
for (_, user_id) in rest.iter() {
assert!(!gatekeeper
.registered_users
.lock()
.unwrap()
.contains_key(user_id));
}
}
#[test]
fn test_filtered_block_connected() {
fn test_block_connected() {
// block_connected in the Gatekeeper is used to keep track of time in order to manage the users' subscription expiry.
// Remove users that get outdated at the new block's height from registered_users and the database.
let mut chain = Blockchain::default().with_height(START_HEIGHT);
@ -874,7 +829,7 @@ mod tests {
let user3_id = get_random_user_id();
for user_id in &[user1_id, user2_id, user3_id] {
gatekeeper.add_outdated_user(*user_id, chain.tip().height + 1)
gatekeeper.add_outdated_user(*user_id, chain.tip().height + 1, None)
}
// Connect a new block. Outdated users are deleted
@ -887,7 +842,10 @@ mod tests {
.lock()
.unwrap()
.contains_key(user_id));
assert!(gatekeeper.dbm.lock().unwrap().load_user(*user_id).is_none());
assert!(matches!(
gatekeeper.dbm.lock().unwrap().load_user(*user_id),
Err(DBError::NotFound)
));
}
// Check that the last_known_block_header has been properly updated

View file

@ -2,8 +2,6 @@
//!
//! A watchtower implementation written in Rust.
// FIXME: This is a temporary fix. See https://github.com/tokio-rs/prost/issues/661
#[allow(clippy::derive_partial_eq_without_eq)]
pub mod protos {
tonic::include_proto!("teos.v2");
}
@ -22,7 +20,6 @@ pub mod responder;
#[doc(hidden)]
mod rpc_errors;
pub mod tls;
mod tx_index;
pub mod watcher;
#[cfg(test)]

View file

@ -1,38 +1,36 @@
use log::LevelFilter;
use simple_logger::SimpleLogger;
use simple_logger::init_with_level;
use std::fs;
use std::io::ErrorKind;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
use std::sync::{Arc, Condvar, Mutex};
use structopt::StructOpt;
use tokio::task;
use tonic::transport::{Certificate, Server, ServerTlsConfig};
use bitcoin::network::Network;
use bitcoin::network::constants::Network;
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
use bitcoincore_rpc::{Auth, Client, RpcApi};
use bitcoincore_rpc::{Auth, Client};
use lightning_block_sync::init::validate_best_block_header;
use lightning_block_sync::poll::{
ChainPoller, Poll, Validate, ValidatedBlock, ValidatedBlockHeader,
};
use lightning_block_sync::{BlockSource, BlockSourceError, SpvClient, UnboundedCache};
use lightning_block_sync::{BlockSource, SpvClient, UnboundedCache};
use teos::api::internal::InternalAPI;
use teos::api::{http, tor::TorAPI};
use teos::api::{http, tor};
use teos::bitcoin_cli::BitcoindClient;
use teos::carrier::Carrier;
use teos::chain_monitor::ChainMonitor;
use teos::config::{self, AuthMethod, Config, Opt};
use teos::config::{self, Config, Opt};
use teos::dbm::DBM;
use teos::gatekeeper::Gatekeeper;
use teos::protos as msgs;
use teos::protos::private_tower_services_server::PrivateTowerServicesServer;
use teos::protos::public_tower_services_server::PublicTowerServicesServer;
use teos::responder::Responder;
use teos::tls::tls_init;
use teos::watcher::Watcher;
use teos_common::constants::IRREVOCABLY_RESOLVED;
use teos_common::cryptography::get_random_keypair;
use teos_common::TowerId;
@ -40,20 +38,22 @@ async fn get_last_n_blocks<B, T>(
poller: &mut ChainPoller<B, T>,
mut last_known_block: ValidatedBlockHeader,
n: usize,
) -> Result<Vec<ValidatedBlock>, BlockSourceError>
) -> Vec<ValidatedBlock>
where
B: DerefMut<Target = T> + Sized + Send + Sync,
T: BlockSource,
{
let mut last_n_blocks = Vec::with_capacity(n);
let mut last_n_blocks = Vec::new();
for _ in 0..n {
log::debug!("Fetching block #{}", last_known_block.height);
let block = poller.fetch_block(&last_known_block).await?;
last_known_block = poller.look_up_previous_header(&last_known_block).await?;
let block = poller.fetch_block(&last_known_block).await.unwrap();
last_known_block = poller
.look_up_previous_header(&last_known_block)
.await
.unwrap();
last_n_blocks.push(block);
}
Ok(last_n_blocks)
last_n_blocks
}
fn create_new_tower_keypair(db: &DBM) -> (SecretKey, PublicKey) {
@ -66,61 +66,41 @@ fn create_new_tower_keypair(db: &DBM) -> (SecretKey, PublicKey) {
async fn main() {
let opt = Opt::from_args();
let path = config::data_dir_absolute_path(opt.data_dir.clone());
let conf_file_path = path.join("teos.toml");
// Create data dir if it does not exist
fs::create_dir_all(&path).unwrap_or_else(|e| {
eprintln!("Cannot create data dir: {e:?}");
eprintln!("Cannot create data dir: {:?}", e);
std::process::exit(1);
});
// Load conf (from file or defaults) and patch it with the command line parameters received (if any)
let mut conf = config::from_file::<Config>(&conf_file_path);
let mut conf = config::from_file::<Config>(path.join("teos.toml"));
let is_default = conf.is_default();
conf.patch_with_options(opt);
conf.verify().unwrap_or_else(|e| {
eprintln!("{e}");
eprintln!("{}", e);
std::process::exit(1);
});
// Set log level
SimpleLogger::new()
.with_level(if conf.deps_debug {
LevelFilter::Debug
} else {
LevelFilter::Warn
})
.with_module_level(
"teos",
if conf.debug {
LevelFilter::Debug
} else {
LevelFilter::Info
},
)
.init()
.unwrap();
if conf.debug {
init_with_level(log::Level::Debug).unwrap()
} else {
init_with_level(log::Level::Info).unwrap()
}
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:?}");
eprintln!("Cannot create network dir: {:?}", e);
std::process::exit(1);
});
// Log default data dir
log::info!("Default data directory: {:?}", &path);
// Log datadir path
log::info!("Using data directory: {:?}", &path_network);
// Log config file path based on whether the config file is found or not
if is_default {
log::info!("Config file: {:?} (not found, skipping)", &conf_file_path);
} else {
log::info!("Config file: {:?}", &conf_file_path);
conf.log_non_default_options();
}
let dbm = Arc::new(Mutex::new(
DBM::new(path_network.join("teos_db.sql3")).unwrap(),
));
@ -132,30 +112,24 @@ async fn main() {
if conf.overwrite_key {
log::info!("Overwriting tower keys");
create_new_tower_keypair(&locked_db)
} else if let Some(sk) = locked_db.load_tower_key() {
(sk, PublicKey::from_secret_key(&Secp256k1::new(), &sk))
} else {
log::info!("Tower keys not found. Creating a fresh set");
create_new_tower_keypair(&locked_db)
match locked_db.load_tower_key() {
Ok(sk) => (sk, PublicKey::from_secret_key(&Secp256k1::new(), &sk)),
Err(_) => {
log::info!("Tower keys not found. Creating a fresh set");
create_new_tower_keypair(&locked_db)
}
}
}
};
log::info!("tower_id: {tower_pk}");
let btc_rpc_auth = match conf.get_auth_method() {
AuthMethod::CookieFile => {
Auth::CookieFile(config::data_dir_absolute_path(conf.btc_rpc_cookie))
}
AuthMethod::UserPass => Auth::UserPass(conf.btc_rpc_user, conf.btc_rpc_password),
// Notice an invalid conf would have failed on `Config::verify()`
_ => unreachable!("A verified conf will only have one of these two auth methods"),
};
log::info!("tower_id: {}", tower_pk);
// Initialize our bitcoind client
let (bitcoin_cli, bitcoind_reachable) = match BitcoindClient::new(
&conf.btc_rpc_connect,
conf.btc_rpc_port,
btc_rpc_auth.clone(),
&conf.btc_network,
&conf.btc_rpc_user,
&conf.btc_rpc_password,
)
.await
{
@ -168,8 +142,8 @@ async fn main() {
ErrorKind::InvalidData => "invalid btcrpcuser or btcrpcpassword".into(),
_ => e.to_string(),
};
log::error!("Failed to connect to bitcoind. Error: {e_msg}");
std::process::exit(1);
log::error!("Failed to connect to bitcoind. Error: {}", e_msg);
return;
}
};
@ -182,81 +156,28 @@ async fn main() {
};
let rpc = Arc::new(
Client::new(
&format!("{schema}{}:{}", conf.btc_rpc_connect, conf.btc_rpc_port),
btc_rpc_auth,
&format!("{}{}:{}", schema, conf.btc_rpc_connect, conf.btc_rpc_port),
Auth::UserPass(conf.btc_rpc_user.clone(), conf.btc_rpc_password.clone()),
)
.unwrap(),
);
let mut derefed = bitcoin_cli.deref();
// Load last known block from DB if found. Poll it from Bitcoind otherwise.
let last_known_block = dbm.lock().unwrap().load_last_known_block();
let tip = if let Some(block_hash) = last_known_block {
let mut last_known_header = derefed
let tip = if let Ok(block_hash) = last_known_block {
derefed
.get_header(&block_hash, None)
.await
.unwrap()
.validate(block_hash)
.unwrap();
log::info!(
"Last known block: {} (height: {})",
last_known_header.header.block_hash(),
last_known_header.height
);
// If we are running in pruned mode some data may be missing (if we happen to have been offline for a while)
if let Some(prune_height) = rpc.get_blockchain_info().unwrap().prune_height {
if last_known_header.height - IRREVOCABLY_RESOLVED + 1 < prune_height as u32 {
log::warn!(
"Cannot load blocks in the range {}-{}. Chain has gone too far out of sync",
last_known_header.height - IRREVOCABLY_RESOLVED + 1,
last_known_header.height
);
if conf.force_update {
log::info!("Forcing a backend update");
// We want to grab the first IRREVOCABLY_RESOLVED we know about for the initial cache
// So we can perform transitions from there onwards.
let target_height = prune_height + IRREVOCABLY_RESOLVED as u64;
let target_hash = rpc.get_block_hash(target_height).unwrap();
last_known_header = derefed
.get_header(
&rpc.get_block_hash(target_height).unwrap(),
Some(target_height as u32),
)
.await
.unwrap()
.validate(target_hash)
.unwrap();
} else {
log::error!(
"The underlying chain has gone too far out of sync. The tower block cache cannot be initialized. Run with --forceupdate to force update. THIS WILL, POTENTIALLY, MAKE THE TOWER MISS SOME OF ITS APPOINTMENTS"
);
std::process::exit(1);
}
}
}
last_known_header
.unwrap()
} else {
validate_best_block_header(&derefed).await.unwrap()
validate_best_block_header(&mut derefed).await.unwrap()
};
log::info!("Last known block: {}", tip.header.block_hash());
// DISCUSS: This is not really required (and only triggered in regtest). This is only in place so the caches can be
// populated with enough blocks mainly because the size of the cache is based on the amount of blocks passed when initializing.
// However, we could add an additional parameter to specify the size of the cache, and initialize with however may blocks we
// could pull from the backend. Adding this functionality just for regtest seemed unnecessary though, hence the check.
if tip.height < IRREVOCABLY_RESOLVED {
log::error!(
"Not enough blocks to start teosd (required: {IRREVOCABLY_RESOLVED}). Mine at least {} more",
IRREVOCABLY_RESOLVED - tip.height
);
std::process::exit(1);
}
log::info!(
"Current chain tip: {} (height: {})",
tip.header.block_hash(),
tip.height
);
let mut poller = ChainPoller::new(&mut derefed, Network::from_str(&conf.btc_network).unwrap());
let last_n_blocks = get_last_n_blocks(&mut poller, tip, 6).await;
// Build components
let gatekeeper = Arc::new(Gatekeeper::new(
@ -267,38 +188,17 @@ async fn main() {
dbm.clone(),
));
let mut poller = ChainPoller::new(
&mut derefed,
Network::from_core_arg(&conf.btc_network).unwrap(),
);
let (responder, watcher) = {
let last_n_blocks = get_last_n_blocks(&mut poller, tip, IRREVOCABLY_RESOLVED as usize)
.await.unwrap_or_else(|e| {
// I'm pretty sure this can only happen if we are pulling blocks from the target to the prune height, and by the time we get to
// the end at least one has been pruned.
log::error!("Couldn't load the latest {IRREVOCABLY_RESOLVED} blocks. Please try again (Error: {})", e.into_inner());
std::process::exit(1);
}
);
let responder = Arc::new(Responder::new(
&last_n_blocks,
tip.height,
Carrier::new(rpc, bitcoind_reachable.clone(), tip.height),
gatekeeper.clone(),
dbm.clone(),
));
let watcher = Arc::new(Watcher::new(
gatekeeper.clone(),
responder.clone(),
&last_n_blocks[0..6],
tip.height,
tower_sk,
TowerId(tower_pk),
dbm.clone(),
));
(responder, watcher)
};
let carrier = Carrier::new(rpc, bitcoind_reachable.clone(), tip.deref().height);
let responder = Arc::new(Responder::new(carrier, gatekeeper.clone(), dbm.clone()));
let watcher = Arc::new(Watcher::new(
gatekeeper.clone(),
responder.clone(),
last_n_blocks,
tip.height,
tower_sk,
TowerId(tower_pk),
dbm.clone(),
));
if watcher.is_fresh() & responder.is_fresh() & gatekeeper.is_fresh() {
log::info!("Fresh bootstrap");
@ -307,14 +207,14 @@ async fn main() {
}
let (shutdown_trigger, shutdown_signal_rpc_api) = triggered::trigger();
let shutdown_signal_internal_api = shutdown_signal_rpc_api.clone();
let shutdown_signal_internal_rpc_api = shutdown_signal_rpc_api.clone();
let shutdown_signal_http = shutdown_signal_rpc_api.clone();
let shutdown_signal_cm = shutdown_signal_rpc_api.clone();
let shutdown_signal_tor = shutdown_signal_rpc_api.clone();
// The ordering here actually matters. Listeners are called by order, and we want the gatekeeper to be called
// first so it updates the users' states and both the Watcher and the Responder operate only on registered users.
let listener = &(gatekeeper, &(watcher.clone(), responder));
// last, so both the Watcher and the Responder can query the necessary data from it during data deletion.
let listener = &(watcher.clone(), &(responder, gatekeeper));
let cache = &mut UnboundedCache::new();
let spv_client = SpvClient::new(tip, poller, cache, listener);
let mut chain_monitor = ChainMonitor::new(
@ -332,52 +232,31 @@ async fn main() {
log::info!("Bootstrap completed. Turning on interfaces");
// Build interfaces
let http_api_addr = format!("{}:{}", conf.api_bind, conf.api_port)
.parse()
.unwrap();
let mut addresses = vec![msgs::NetworkAddress::from_ipv4(
conf.api_bind.clone(),
conf.api_port,
)];
// Create Tor endpoint if required
let tor_api = if conf.tor_support {
let tor_api = TorAPI::new(
http_api_addr,
conf.onion_hidden_service_port,
conf.tor_control_port,
path_network,
)
.await;
addresses.push(msgs::NetworkAddress::from_torv3(
tor_api.get_onion_address(),
conf.onion_hidden_service_port,
));
Some(tor_api)
} else {
None
};
let internal_api = Arc::new(InternalAPI::new(
let rpc_api = Arc::new(InternalAPI::new(
watcher,
addresses,
bitcoind_reachable.clone(),
shutdown_trigger,
));
let internal_api_cloned = internal_api.clone();
let internal_rpc_api = rpc_api.clone();
let rpc_api_addr = format!("{}:{}", conf.rpc_bind, conf.rpc_port)
.parse()
.unwrap();
let internal_api_addr = format!("{}:{}", conf.internal_api_bind, conf.internal_api_port)
let internal_rpc_api_addr = format!("{}:{}", conf.internal_api_bind, conf.internal_api_port)
.parse()
.unwrap();
let internal_rpc_api_uri = format!(
"http://{}:{}",
conf.internal_api_bind, conf.internal_api_port
);
let http_api_addr = format!("{}:{}", conf.api_bind, conf.api_port)
.parse()
.unwrap();
// Generate mtls certificates to data directory so the admin can securely connect
// to the server to perform administrative tasks.
let (identity, ca_cert) = tls_init(&path).unwrap_or_else(|e| {
eprintln!("Couldn't generate tls certificates: {e:?}");
eprintln!("Couldn't generate tls certificates: {:?}", e);
std::process::exit(1);
});
@ -390,7 +269,7 @@ async fn main() {
Server::builder()
.tls_config(tls)
.expect("couldn't configure tls")
.add_service(PrivateTowerServicesServer::new(internal_api))
.add_service(PrivateTowerServicesServer::new(rpc_api))
.serve_with_shutdown(rpc_api_addr, shutdown_signal_rpc_api)
.await
.unwrap();
@ -398,38 +277,31 @@ async fn main() {
let public_api_task = task::spawn(async move {
Server::builder()
.add_service(PublicTowerServicesServer::new(internal_api_cloned))
.serve_with_shutdown(internal_api_addr, shutdown_signal_internal_api)
.add_service(PublicTowerServicesServer::new(internal_rpc_api))
.serve_with_shutdown(internal_rpc_api_addr, shutdown_signal_internal_rpc_api)
.await
.unwrap();
});
let (http_service_ready, ready_signal_http) = triggered::trigger();
let http_api_task = task::spawn(http::serve(
http_api_addr,
internal_api_addr,
http_service_ready,
internal_rpc_api_uri,
shutdown_signal_http,
));
ready_signal_http.await;
// Add Tor Onion Service for public API
let mut tor_task = Option::None;
let (tor_service_ready, ready_signal_tor) = triggered::trigger();
if let Some(tor_api) = tor_api {
log::info!("Starting up Tor hidden service");
if conf.tor_support {
log::info!("Starting up hidden tor service");
let tor_control_port = conf.tor_control_port;
let api_port = conf.api_port;
let onion_port = conf.onion_hidden_service_port;
tor_task = Some(task::spawn(async move {
if let Err(e) = tor_api
.expose_onion_service(tor_service_ready, shutdown_signal_tor)
tor::expose_onion_service(tor_control_port, api_port, onion_port, shutdown_signal_tor)
.await
{
eprintln!("Cannot connect to the Tor backend: {e}");
std::process::exit(1);
}
.unwrap();
}));
ready_signal_tor.await
}
log::info!("Tower ready");
@ -439,8 +311,8 @@ async fn main() {
http_api_task.await.unwrap();
private_api_task.await.unwrap();
public_api_task.await.unwrap();
if let Some(tor_task) = tor_task {
tor_task.await.unwrap();
if conf.tor_support {
tor_task.unwrap().await.unwrap();
}
log::info!("Shutting down tower");

File diff suppressed because it is too large Load diff

View file

@ -14,31 +14,27 @@ use std::thread;
use jsonrpc_http_server::jsonrpc_core::error::ErrorCode as JsonRpcErrorCode;
use jsonrpc_http_server::jsonrpc_core::{Error as JsonRpcError, IoHandler, Params, Value};
use jsonrpc_http_server::{CloseHandle, Server, ServerBuilder};
use jsonrpc_http_server::{Server, ServerBuilder};
use bitcoincore_rpc::{Auth, Client as BitcoindClient};
use bitcoin::block::Block;
use bitcoin::blockdata::block::{Block, BlockHeader};
use bitcoin::blockdata::constants::genesis_block;
use bitcoin::blockdata::script::{Builder, ScriptBuf};
use bitcoin::blockdata::script::{Builder, Script};
use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxIn, TxOut};
use bitcoin::hash_types::BlockHash;
use bitcoin::hash_types::Txid;
use bitcoin::hashes::Hash;
use bitcoin::merkle_tree::calculate_root;
use bitcoin::pow::Work;
use bitcoin::Amount;
use bitcoin::Network;
use bitcoin::Witness;
use bitcoin::network::constants::Network;
use bitcoin::util::hash::bitcoin_merkle_root;
use bitcoin::util::uint::Uint256;
use lightning_block_sync::poll::{
ChainPoller, Poll, Validate, ValidatedBlock, ValidatedBlockHeader,
};
use lightning_block_sync::{
AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource, BlockSourceError,
UnboundedCache,
AsyncBlockSourceResult, BlockHeaderData, BlockSource, BlockSourceError, UnboundedCache,
};
use teos_common::constants::IRREVOCABLY_RESOLVED;
use teos_common::cryptography::{get_random_bytes, get_random_keypair};
use teos_common::test_utils::{generate_random_appointment, get_random_user_id, TXID_HEX, TX_HEX};
use teos_common::UserId;
@ -48,9 +44,7 @@ use crate::carrier::Carrier;
use crate::dbm::DBM;
use crate::extended_appointment::{ExtendedAppointment, UUID};
use crate::gatekeeper::{Gatekeeper, UserInfo};
use crate::protos as msgs;
use crate::responder::{ConfirmationStatus, Responder, TransactionTracker};
use crate::rpc_errors;
use crate::watcher::{Breach, Watcher};
pub(crate) const SLOTS: u32 = 21;
@ -87,10 +81,23 @@ impl Blockchain {
pub fn with_height(mut self, height: usize) -> Self {
self.blocks.reserve_exact(height);
for _ in 1..=height {
self.generate(None);
let bits = BlockHeader::compact_target_from_u256(&Uint256::from_be_bytes([0xff; 32]));
for i in 1..=height {
let prev_block = &self.blocks[i - 1];
let prev_blockhash = prev_block.block_hash();
let time = prev_block.header.time + height as u32;
self.blocks.push(Block {
header: BlockHeader {
version: 0,
prev_blockhash,
merkle_root: Default::default(),
time,
bits,
nonce: 0,
},
txdata: vec![],
});
}
self
}
@ -156,11 +163,8 @@ impl Blockchain {
fn at_height_unvalidated(&self, height: usize) -> BlockHeaderData {
assert!(!self.blocks.is_empty());
assert!(height < self.blocks.len());
let height_bytes = height.to_be_bytes();
let mut padded_bytes = [0u8; 32];
padded_bytes[32 - height_bytes.len()..].copy_from_slice(&height_bytes);
BlockHeaderData {
chainwork: self.blocks[0].header.work() + Work::from_be_bytes(padded_bytes),
chainwork: self.blocks[0].header.work() + Uint256::from_u64(height as u64).unwrap(),
height: height as u32,
header: self.blocks[height].header,
}
@ -190,32 +194,26 @@ impl Blockchain {
}
pub fn generate(&mut self, txs: Option<Vec<Transaction>>) -> Block {
let bits = bitcoin::Target::from_be_bytes([0xff; 32]).to_compact_lossy();
let bits = BlockHeader::compact_target_from_u256(&Uint256::from_be_bytes([0xff; 32]));
let prev_block = self.blocks.last().unwrap();
let prev_blockhash = prev_block.block_hash();
let time = prev_block.header.time + (self.blocks.len() + 1) as u32;
let txdata = match txs {
Some(v) => {
if v.is_empty() {
vec![get_random_tx()]
} else {
v
}
}
None => vec![get_random_tx()],
Some(t) => t,
None => vec![],
};
let hashes = txdata.iter().map(|tx| tx.compute_txid().to_raw_hash());
let mut header = bitcoin::block::Header {
version: bitcoin::block::Version::from_consensus(0),
let hashes = txdata.iter().map(|obj| obj.txid().as_hash());
let mut header = BlockHeader {
version: 0,
prev_blockhash,
merkle_root: calculate_root(hashes).unwrap().into(),
merkle_root: bitcoin_merkle_root(hashes).into(),
time,
bits,
nonce: 0,
};
while header.validate_pow(header.target()).is_err() {
while header.validate_pow(&header.target()).is_err() {
header.nonce += 1;
}
@ -228,7 +226,7 @@ impl Blockchain {
impl BlockSource for Blockchain {
fn get_header<'a>(
&'a self,
&'a mut self,
header_hash: &'a BlockHash,
_height_hint: Option<u32>,
) -> AsyncBlockSourceResult<'a, BlockHeaderData> {
@ -251,7 +249,10 @@ impl BlockSource for Blockchain {
})
}
fn get_block<'a>(&'a self, header_hash: &'a BlockHash) -> AsyncBlockSourceResult<BlockData> {
fn get_block<'a>(
&'a mut self,
header_hash: &'a BlockHash,
) -> AsyncBlockSourceResult<'a, Block> {
Box::pin(async move {
for (height, block) in self.blocks.iter().enumerate() {
if block.header.block_hash() == *header_hash {
@ -260,14 +261,15 @@ impl BlockSource for Blockchain {
return Err(BlockSourceError::persistent("block not found"));
}
}
return Ok(BlockData::FullBlock(block.clone()));
return Ok(block.clone());
}
}
Err(BlockSourceError::transient("block not found"))
})
}
fn get_best_block(&self) -> AsyncBlockSourceResult<(BlockHash, Option<u32>)> {
fn get_best_block(&mut self) -> AsyncBlockSourceResult<(BlockHash, Option<u32>)> {
Box::pin(async move {
if *self.unreachable.lock().unwrap() {
return Err(BlockSourceError::transient("Connection refused"));
@ -294,20 +296,20 @@ pub(crate) fn get_random_tx() -> Transaction {
let prev_txid_bytes = get_random_bytes(32);
Transaction {
version: bitcoin::transaction::Version(2),
lock_time: bitcoin::locktime::absolute::LockTime::from_height(0).unwrap(),
version: 2,
lock_time: 0,
input: vec![TxIn {
previous_output: OutPoint::new(
Txid::from_slice(&prev_txid_bytes).unwrap(),
rng.gen_range(0..200),
),
script_sig: ScriptBuf::new(),
witness: Witness::new(),
sequence: bitcoin::Sequence(0),
script_sig: Script::new(),
witness: Vec::new(),
sequence: 0,
}],
output: vec![TxOut {
script_pubkey: Builder::new().push_int(1).into_script(),
value: Amount::from_sat(rng.gen_range(0..21_000_000_000)),
value: rng.gen_range(0..21000000000),
}],
}
}
@ -328,7 +330,7 @@ pub(crate) fn generate_dummy_appointment_with_user(
let mut app = generate_dummy_appointment(dispute_txid);
app.user_id = user_id;
(app.uuid(), app)
(UUID::new(app.locator(), user_id), app)
}
pub(crate) fn get_random_breach() -> Breach {
@ -346,82 +348,65 @@ pub(crate) fn get_random_tracker(
TransactionTracker::new(breach, user_id, status)
}
pub(crate) fn store_appointment_and_its_user(dbm: &DBM, appointment: &ExtendedAppointment) {
pub(crate) fn store_appointment_and_fks_to_db(
dbm: &DBM,
uuid: UUID,
appointment: &ExtendedAppointment,
) {
dbm.store_user(
appointment.user_id,
&UserInfo::new(AVAILABLE_SLOTS, SUBSCRIPTION_START, SUBSCRIPTION_EXPIRY),
)
// It's ok if the user is already stored.
.ok();
dbm.store_appointment(appointment.uuid(), appointment)
.unwrap();
.unwrap();
dbm.store_appointment(uuid, appointment).unwrap();
}
pub(crate) async fn get_last_n_blocks(chain: &mut Blockchain, n: usize) -> Vec<ValidatedBlock> {
let mut last_n_blocks = Vec::with_capacity(n);
let mut last_known_block = Ok(chain.tip());
let poller = ChainPoller::new(chain, Network::Regtest);
let tip = chain.tip();
let mut poller = ChainPoller::new(chain, Network::Bitcoin);
let mut last_n_blocks = Vec::new();
let mut last_known_block = tip;
for _ in 0..n {
let header = last_known_block.unwrap();
let block = poller.fetch_block(&header).await.unwrap();
let block = poller.fetch_block(&last_known_block).await.unwrap();
last_known_block = poller
.look_up_previous_header(&last_known_block)
.await
.unwrap();
last_n_blocks.push(block);
last_known_block = poller.look_up_previous_header(&header).await;
}
last_n_blocks
}
pub(crate) fn get_full_blocks(last_n_blocks: &[ValidatedBlock]) -> Vec<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,
Error(i64),
}
pub(crate) fn create_carrier(query: MockedServerQuery, height: u32) -> (Carrier, BitcoindStopper) {
pub(crate) fn create_carrier(query: MockedServerQuery, height: u32) -> Carrier {
let bitcoind_mock = match query {
MockedServerQuery::Regular => BitcoindMock::new(MockOptions::default()),
MockedServerQuery::InMempoool => BitcoindMock::new(MockOptions::in_mempool()),
MockedServerQuery::Regular => BitcoindMock::new(MockOptions::empty()),
MockedServerQuery::Error(x) => BitcoindMock::new(MockOptions::with_error(x)),
};
let bitcoin_cli = Arc::new(BitcoindClient::new(bitcoind_mock.url(), Auth::None).unwrap());
let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new()));
start_server(bitcoind_mock.server);
start_server(bitcoind_mock);
(
Carrier::new(bitcoin_cli, bitcoind_reachable, height),
bitcoind_mock.stopper,
)
Carrier::new(bitcoin_cli, bitcoind_reachable, height)
}
pub(crate) async fn create_responder(
chain: &mut Blockchain,
pub(crate) fn create_responder(
tip: ValidatedBlockHeader,
gatekeeper: Arc<Gatekeeper>,
dbm: Arc<Mutex<DBM>>,
server_url: &str,
) -> Responder {
let height = chain.tip().height;
// For the local TxIndex logic to be sound, our index needs to have, at least, IRREVOCABLY_RESOLVED blocks
debug_assert!(height >= IRREVOCABLY_RESOLVED);
let last_n_blocks = get_last_n_blocks(chain, IRREVOCABLY_RESOLVED as usize).await;
let bitcoin_cli = Arc::new(BitcoindClient::new(server_url, Auth::None).unwrap());
let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new()));
let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, height);
let carrier = Carrier::new(bitcoin_cli, bitcoind_reachable, tip.deref().height);
Responder::new(last_n_blocks.as_slice(), height, carrier, gatekeeper, dbm)
Responder::new(carrier, gatekeeper, dbm)
}
pub(crate) async fn create_watcher(
@ -430,23 +415,20 @@ pub(crate) async fn create_watcher(
gatekeeper: Arc<Gatekeeper>,
bitcoind_mock: BitcoindMock,
dbm: Arc<Mutex<DBM>>,
) -> (Watcher, BitcoindStopper) {
) -> Watcher {
let last_n_blocks = get_last_n_blocks(chain, 6).await;
start_server(bitcoind_mock.server);
start_server(bitcoind_mock);
let (tower_sk, tower_pk) = get_random_keypair();
let tower_id = UserId(tower_pk);
(
Watcher::new(
gatekeeper,
responder,
last_n_blocks.as_slice(),
chain.get_block_count(),
tower_sk,
tower_id,
dbm,
),
bitcoind_mock.stopper,
Watcher::new(
gatekeeper,
responder,
last_n_blocks,
chain.get_block_count(),
tower_sk,
tower_id,
dbm,
)
}
#[derive(Clone)]
@ -481,10 +463,8 @@ impl Default for ApiConfig {
}
}
pub(crate) async fn create_api_with_config(
api_config: ApiConfig,
) -> (Arc<InternalAPI>, BitcoindStopper) {
let bitcoind_mock = BitcoindMock::new(MockOptions::default());
pub(crate) async fn create_api_with_config(api_config: ApiConfig) -> Arc<InternalAPI> {
let bitcoind_mock = BitcoindMock::new(MockOptions::empty());
let mut chain = Blockchain::default().with_height(START_HEIGHT);
let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap()));
@ -495,9 +475,8 @@ pub(crate) async fn create_api_with_config(
EXPIRY_DELTA,
dbm.clone(),
));
let responder =
create_responder(&mut chain, gk.clone(), dbm.clone(), bitcoind_mock.url()).await;
let (watcher, stopper) = create_watcher(
let responder = create_responder(chain.tip(), gk.clone(), dbm.clone(), bitcoind_mock.url());
let watcher = create_watcher(
&mut chain,
Arc::new(responder),
gk.clone(),
@ -508,66 +487,58 @@ pub(crate) async fn create_api_with_config(
let bitcoind_reachable = Arc::new((Mutex::new(api_config.bitcoind_reachable), Condvar::new()));
let (shutdown_trigger, _) = triggered::trigger();
(
Arc::new(InternalAPI::new(
Arc::new(watcher),
vec![msgs::NetworkAddress::from_ipv4("address".to_string(), 21)],
bitcoind_reachable,
shutdown_trigger,
)),
stopper,
)
Arc::new(InternalAPI::new(
Arc::new(watcher),
bitcoind_reachable,
shutdown_trigger,
))
}
pub(crate) async fn create_api() -> (Arc<InternalAPI>, BitcoindStopper) {
pub(crate) async fn create_api() -> Arc<InternalAPI> {
create_api_with_config(ApiConfig::default()).await
}
#[derive(Clone)]
pub struct BitcoindStopper {
close_handle: CloseHandle,
}
impl BitcoindStopper {
pub fn new(close_handle: CloseHandle) -> Self {
Self { close_handle }
}
pub fn close_handle(&self) -> CloseHandle {
self.close_handle.clone()
}
}
impl Drop for BitcoindStopper {
fn drop(&mut self) {
self.close_handle().close()
}
}
pub(crate) struct BitcoindMock {
pub url: String,
pub server: Server,
stopper: BitcoindStopper,
}
#[derive(Default)]
pub(crate) struct MockOptions {
error_code: Option<i64>,
in_mempool: bool,
block_hash: Option<BlockHash>,
height: Option<usize>,
}
impl MockOptions {
pub fn with_error(error_code: i64) -> Self {
pub fn new(error_code: i64, block_hash: BlockHash, height: usize) -> Self {
Self {
error_code: Some(error_code),
in_mempool: false,
block_hash: Some(block_hash),
height: Some(height),
}
}
pub fn in_mempool() -> Self {
pub fn empty() -> Self {
Self {
error_code: None,
in_mempool: true,
block_hash: None,
height: None,
}
}
pub fn with_error(error_code: i64) -> Self {
Self {
error_code: Some(error_code),
block_hash: None,
height: None,
}
}
#[allow(dead_code)]
pub fn with_block(block_hash: BlockHash, height: usize) -> Self {
Self {
error_code: None,
block_hash: Some(block_hash),
height: Some(height),
}
}
}
@ -581,10 +552,15 @@ impl BitcoindMock {
Err(JsonRpcError::new(JsonRpcErrorCode::ServerError(error)))
});
io.add_alias("sendrawtransaction", "error");
io.add_alias("getrawtransaction", "error");
} else {
BitcoindMock::add_sendrawtransaction(&mut io);
BitcoindMock::add_getrawtransaction(&mut io, options.in_mempool);
}
if let Some(block_hash) = options.block_hash {
BitcoindMock::add_getrawtransaction(&mut io, block_hash.to_string());
if let Some(height) = options.height {
BitcoindMock::add_getblockheader(&mut io, block_hash.to_string(), height);
}
}
let server = ServerBuilder::new(io)
@ -594,7 +570,6 @@ impl BitcoindMock {
Self {
url: format!("http://{}", server.address()),
stopper: BitcoindStopper::new(server.close_handle()),
server,
}
}
@ -605,25 +580,41 @@ impl BitcoindMock {
});
}
fn add_getrawtransaction(io: &mut IoHandler, in_mempool: bool) {
fn add_getrawtransaction(io: &mut IoHandler, block_hash: String) {
io.add_sync_method("getrawtransaction", move |_params: Params| {
if !in_mempool {
Err(JsonRpcError::new(JsonRpcErrorCode::ServerError(rpc_errors::RPC_INVALID_ADDRESS_OR_KEY as i64)))
} else {
match _params {
Params::Array(x) => match x[1] {
Value::Bool(x) => {
if x {
Ok(serde_json::json!({"hex": TX_HEX, "txid": TXID_HEX, "hash": TXID_HEX, "size": 0,
"vsize": 0, "version": 1, "locktime": 0, "vin": [], "vout": [] }))
} else {
Ok(Value::String(TX_HEX.to_owned()))
}
match _params {
Params::Array(x) => match x[1] {
Value::Bool(x) => {
if x {
Ok(serde_json::json!({"hex": TX_HEX, "txid": TXID_HEX, "hash": TXID_HEX, "size": 0,
"vsize": 0, "version": 1, "locktime": 0, "vin": [], "vout": [], "blockhash": block_hash }))
} else {
Ok(Value::String(TX_HEX.to_owned()))
}
_ => panic!("Boolean param not found"),
},
_ => panic!("No params found"),
}
}
_ => panic!("Boolean param not found"),
},
_ => panic!("No params found"),
}
})
}
fn add_getblockheader(io: &mut IoHandler, block_hash: String, height: usize) {
io.add_sync_method("getblockheader", move |_params: Params| {
match _params {
Params::Array(x) => match x[1] {
Value::Bool(x) => {
if x {
Ok(serde_json::json!({"hash": block_hash, "confirmations": 1, "height": height, "version": 1,
"merkleroot": "4eca41cf0fa551346842eb317564a403e39553444790a65f949f95bc18d24643", "time": 1645719068, "nonce": 2, "bits": "207fffff",
"difficulty": 0.0, "chainwork": "0000000000000000000000000000000000000000000000000000000000001146", "nTx": 1}))
} else {
Ok(Value::String(TX_HEX.to_owned()))
}
}
_ => panic!("Boolean param not found"),
},
_ => panic!("No params found"),
}
})
}
@ -633,8 +624,8 @@ impl BitcoindMock {
}
}
pub(crate) fn start_server(server: Server) {
pub(crate) fn start_server(bitcoind: BitcoindMock) {
thread::spawn(move || {
server.wait();
bitcoind.server.wait();
});
}

View file

@ -5,7 +5,7 @@
* https://github.com/ElementsProject/lightning/blob/master/LICENSE
*/
use rcgen::{Certificate, Error as RcgenError, KeyPair};
use rcgen::{Certificate, KeyPair, RcgenError};
use std::convert::TryFrom;
use std::path::Path;
@ -30,20 +30,21 @@ impl From<std::io::Error> for GenCertificateFailure {
/// Just a wrapper around a certificate and an associated keypair.
#[derive(Clone, Debug)]
struct Identity {
pub struct Identity {
pub key: Vec<u8>,
pub certificate: Vec<u8>,
}
impl TryFrom<&Identity> for (Certificate, KeyPair) {
impl TryFrom<&Identity> for Certificate {
type Error = RcgenError;
fn try_from(id: &Identity) -> Result<(Certificate, KeyPair), RcgenError> {
let key = KeyPair::from_pem(&String::from_utf8_lossy(&id.key))?;
let params =
rcgen::CertificateParams::from_ca_cert_pem(&String::from_utf8_lossy(&id.certificate))?;
let cert = params.self_signed(&key)?;
Ok((cert, key))
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)
}
}
@ -66,35 +67,42 @@ fn generate_or_load_identity(
parent: Option<&Identity>,
) -> Result<Identity, GenCertificateFailure> {
// Just our naming convention here.
let cert_path = directory.join(format!("{filename}.pem"));
let key_path = directory.join(format!("{filename}-key.pem"));
let cert_path = directory.join(format!("{}.pem", filename));
let key_path = directory.join(format!("{}-key.pem", filename));
// 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()?;
log::debug!(
"Generating a new keypair in {:?}, it didn't exist",
&key_path
);
let keypair = KeyPair::generate(&rcgen::PKCS_ECDSA_P256_SHA256)?;
std::fs::write(&key_path, keypair.serialize_pem())?;
log::debug!("Generating a new certificate for key {key_path:?} at {cert_path:?}",);
log::debug!(
"Generating a new certificate for key {:?} at {:?}",
&key_path,
&cert_path
);
// Configure the certificate we want.
let subject_alt_names = vec!["teos".to_string(), "localhost".to_string()];
let mut params = rcgen::CertificateParams::new(subject_alt_names)?;
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;
if parent.is_none() {
params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
} else {
params.is_ca = rcgen::IsCa::NoCa;
params.is_ca = rcgen::IsCa::SelfSignedOnly;
}
params
.distinguished_name
.push(rcgen::DnType::CommonName, name);
let cert = Certificate::from_params(params)?;
std::fs::write(
&cert_path,
match parent {
None => params.self_signed(&keypair)?.pem(),
Some(ca) => {
let (ca_cert, ca_key) = <(Certificate, KeyPair)>::try_from(ca)?;
params.signed_by(&keypair, &ca_cert, &ca_key)?.pem()
}
None => cert.serialize_pem()?,
Some(ca) => cert.serialize_pem_with_signer(&Certificate::try_from(ca)?)?,
},
)?;
}

View file

@ -1,418 +0,0 @@
use std::collections::{HashMap, VecDeque};
use std::fmt;
use std::hash::Hash;
use std::ops::Deref;
use bitcoin::block::Header;
use bitcoin::hash_types::BlockHash;
use bitcoin::{Transaction, Txid};
use lightning_block_sync::poll::ValidatedBlock;
use teos_common::appointment::Locator;
/// A trait implemented by types that can be used as key in a [TxIndex].
pub trait Key: Hash + Eq {
fn from_txid(txid: Txid) -> Self;
}
impl Key for Txid {
fn from_txid(txid: Txid) -> Self {
txid
}
}
impl Key for Locator {
fn from_txid(txid: Txid) -> Self {
Locator::new(txid)
}
}
pub enum Type {
Transaction,
BlockHash,
}
pub enum Data {
Transaction(Transaction),
BlockHash(BlockHash),
}
impl fmt::Display for Data {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Data::Transaction(_) => write!(f, "Transaction"),
Data::BlockHash(_) => write!(f, "BlockHash"),
}
}
}
/// A trait implemented by types that can be used as value in a [TxIndex].
pub trait Value {
fn get_type() -> Type;
fn from_data(d: Data) -> Self;
}
impl Value for BlockHash {
fn get_type() -> Type {
Type::BlockHash
}
fn from_data(d: Data) -> Self {
match d {
Data::BlockHash(b) => b,
other => panic!("Cannot build a BlockHash from {}", other),
}
}
}
impl Value for Transaction {
fn get_type() -> Type {
Type::Transaction
}
fn from_data(d: Data) -> Self {
match d {
Data::Transaction(t) => t,
other => panic!("Cannot build a BlockHash from {}", other),
}
}
}
/// Data structure used to index locators computed from parsed blocks.
///
/// Holds up to `size` blocks with their corresponding computed [Locator]s.
#[derive(Debug, PartialEq, Eq)]
pub struct TxIndex<K: Key, V: Value> {
/// A [K]:[V] map.
index: HashMap<K, V>,
/// Vector of block hashes covered by the index.
blocks: VecDeque<BlockHash>,
/// Map of [BlockHash]:[Vec<K>]. Used to remove data from the index.
tx_in_block: HashMap<BlockHash, Vec<K>>,
/// The height of the last block included in the index.
tip: u32,
/// Maximum size of the index.
size: usize,
}
impl<K, V> TxIndex<K, V>
where
K: Key + Copy,
V: Value + Clone,
Self: Sized,
{
pub fn new(last_n_blocks: &[ValidatedBlock], height: u32) -> Self {
let size = last_n_blocks.len();
let mut tx_index = Self {
index: HashMap::new(),
blocks: VecDeque::with_capacity(size),
tx_in_block: HashMap::new(),
tip: height,
size,
};
for block in last_n_blocks.iter().rev() {
match block.deref() {
lightning_block_sync::BlockData::HeaderOnly(_) => {
panic!("Expected FullBlock")
}
lightning_block_sync::BlockData::FullBlock(block) => {
if let Some(prev_block_hash) = tx_index.blocks.back() {
if block.header.prev_blockhash != *prev_block_hash {
panic!("last_n_blocks contains unchained blocks");
}
};
let map = block
.txdata
.iter()
.map(|tx| {
(
K::from_txid(tx.compute_txid()),
match V::get_type() {
Type::Transaction => {
V::from_data(Data::Transaction(tx.clone()))
}
Type::BlockHash => {
V::from_data(Data::BlockHash(block.header.block_hash()))
}
},
)
})
.collect();
tx_index.update(block.header, &map);
}
}
}
tx_index
}
/// Gets an item from the index if present. [None] otherwise.
pub fn get<'a>(&'a self, k: &'a K) -> Option<&V> {
self.index.get(k)
}
/// Checks if the index if full.
pub fn is_full(&self) -> bool {
self.blocks.len() > self.size
}
/// Get's the height of a given block based on its position in the block queue.
pub fn get_height(&self, block_hash: &BlockHash) -> Option<usize> {
let pos = self.blocks.iter().position(|x| x == block_hash)?;
Some(self.tip as usize + pos + 1 - self.blocks.len())
}
/// Updates the index by adding data from a new block. Removes the oldest block if the index is full afterwards.
pub fn update(&mut self, block_header: Header, data: &HashMap<K, V>) {
self.blocks.push_back(block_header.block_hash());
let ks = data
.iter()
.map(|(k, v)| {
self.index.insert(*k, v.clone());
*k
})
.collect();
self.tx_in_block.insert(block_header.block_hash(), ks);
if self.is_full() {
// Avoid logging during bootstrap
log::debug!("New block added to index: {}", block_header.block_hash());
self.tip += 1;
self.remove_oldest_block();
}
}
/// Fixes the index by removing disconnected data.
pub fn remove_disconnected_block(&mut self, block_hash: &BlockHash) {
if let Some(ks) = self.tx_in_block.remove(block_hash) {
self.index.retain(|k, _| !ks.contains(k));
// Blocks should be disconnected from last backwards. Log if that's not the case so we can revisit this and fix it.
if let Some(ref h) = self.blocks.pop_back() {
if h != block_hash {
log::error!("Disconnected block does not match the oldest block stored in the TxIndex ({block_hash} != {h})");
}
}
} else {
log::warn!("The index is already empty");
}
}
/// Removes the oldest block from the index.
/// This removes data from `self.blocks`, `self.tx_in_block` and `self.index`.
pub fn remove_oldest_block(&mut self) {
let h = self.blocks.pop_front().unwrap();
let ks = self.tx_in_block.remove(&h).unwrap();
self.index.retain(|k, _| !ks.contains(k));
log::debug!("Oldest block removed from index: {h}");
}
}
impl<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,
"index: {:?}\n\nblocks: {:?}\n\ntx_in_block: {:?}\n\nsize: {}",
self.index, self.blocks, self.tx_in_block, self.size
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ops::Deref;
use crate::test_utils::{get_full_block, get_full_blocks, get_last_n_blocks, Blockchain};
use bitcoin::hashes::serde_macros::serde_details::SerdeHash;
use bitcoin::Block;
impl<K, V> TxIndex<K, V>
where
K: Key + std::cmp::Eq + Copy,
V: Value + Clone,
Self: Sized,
{
pub fn index_mut(&mut self) -> &mut HashMap<K, V> {
&mut self.index
}
pub fn blocks(&self) -> &VecDeque<BlockHash> {
&self.blocks
}
pub fn contains_key(&self, k: &K) -> bool {
self.index.contains_key(k)
}
}
#[tokio::test]
async fn test_new() {
let height = 10;
let mut chain = Blockchain::default().with_height(height as usize);
let last_six_blocks = get_last_n_blocks(&mut chain, 6).await;
let blocks: Vec<Block> = get_full_blocks(&last_six_blocks);
let cache: TxIndex<Locator, Transaction> = TxIndex::new(&last_six_blocks, height);
assert_eq!(blocks.len(), cache.size);
for block in blocks.iter() {
assert!(cache.blocks().contains(&block.block_hash()));
let mut locators = Vec::new();
for tx in block.txdata.iter() {
let locator = Locator::new(tx.compute_txid());
assert!(cache.contains_key(&locator));
locators.push(locator);
}
assert_eq!(cache.tx_in_block[&block.block_hash()], locators);
}
}
#[tokio::test]
async fn test_get_height() {
let cache_size = 10;
let height = 50;
let mut chain = Blockchain::default().with_height_and_txs(height, 42);
let last_n_blocks = get_last_n_blocks(&mut chain, cache_size).await;
// last_n_blocks is ordered from latest to earliest
let first_block = get_full_block(last_n_blocks.get(cache_size - 1).unwrap());
let last_block = get_full_block(last_n_blocks.first().unwrap());
let mid_block = get_full_block(last_n_blocks.get(cache_size / 2).unwrap());
let cache: TxIndex<Locator, Transaction> = TxIndex::new(&last_n_blocks, height as u32);
assert_eq!(
cache.get_height(&first_block.header.block_hash()).unwrap(),
height - cache_size + 1
);
assert_eq!(
cache.get_height(&last_block.header.block_hash()).unwrap(),
height
);
assert_eq!(
cache.get_height(&mid_block.header.block_hash()).unwrap(),
height - cache_size / 2
);
}
#[tokio::test]
async fn test_get_height_not_found() {
let cache_size = 10;
let height = 50;
let mut chain = Blockchain::default().with_height_and_txs(height, 42);
let cache: TxIndex<Locator, Transaction> = TxIndex::new(
&get_last_n_blocks(&mut chain, cache_size).await,
height as u32,
);
let fake_hash = &BlockHash::from_slice_delegated(&[0; 32]).unwrap();
assert!(cache.get_height(fake_hash).is_none());
}
#[tokio::test]
async fn test_update() {
let height = 10;
let mut chain = Blockchain::default().with_height(height as usize);
let mut last_n_blocks = get_last_n_blocks(&mut chain, 7).await;
// Store the last block to use it for an update and the first to check eviction
// Notice that the list of blocks is ordered from last to first.
let last_block = last_n_blocks.remove(0);
let first_block = last_n_blocks.last().unwrap();
// Init the cache with the 6 block before the last
let mut cache = TxIndex::new(&last_n_blocks, height);
// Update the cache with the last block
let full_block = get_full_block(&last_block);
let locator_tx_map = full_block
.txdata
.iter()
.map(|tx| (Locator::new(tx.compute_txid()), tx.clone()))
.collect();
let header = full_block.header;
cache.update(header, &locator_tx_map);
// Check that the new data is in the cache
assert!(cache.blocks().contains(&header.block_hash()));
for (locator, _) in locator_tx_map.iter() {
assert!(cache.contains_key(locator));
}
let block_hash = full_block.header.block_hash();
assert_eq!(
cache.tx_in_block[&block_hash],
locator_tx_map.keys().cloned().collect::<Vec<Locator>>()
);
// Check that the data from the first block has been evicted
let first_full_block = get_full_block(first_block);
let tx = first_full_block.txdata[0].clone();
assert!(!cache.contains_key(&Locator::new(tx.compute_txid())));
let block_hash = first_full_block.header.block_hash();
assert!(!cache.tx_in_block.contains_key(&block_hash));
}
#[tokio::test]
async fn test_remove_disconnected_block() {
let cache_size = 6;
let height = cache_size * 2;
let mut chain = Blockchain::default().with_height_and_txs(height, 42);
let mut cache: TxIndex<Locator, Transaction> = TxIndex::new(
&get_last_n_blocks(&mut chain, cache_size).await,
height as u32,
);
// TxIndex::fix removes the last connected block and removes all the associated data
for i in 0..cache_size {
let header = chain
.at_height(chain.get_block_count() as usize - i)
.deref()
.header;
let locators = cache.tx_in_block.get(&header.block_hash()).unwrap().clone();
// Make sure there's data regarding the target block in the cache before fixing it
assert_eq!(cache.blocks().len(), cache.size - i);
assert!(cache.blocks().contains(&header.block_hash()));
assert!(!locators.is_empty());
for locator in locators.iter() {
assert!(cache.contains_key(locator));
}
cache.remove_disconnected_block(&header.block_hash());
// Check that the block data is not in the cache anymore
assert_eq!(cache.blocks().len(), cache.size - i - 1);
assert!(!cache.blocks().contains(&header.block_hash()));
assert!(!cache.tx_in_block.contains_key(&header.block_hash()));
for locator in locators.iter() {
assert!(!cache.contains_key(locator));
}
}
// At this point the cache should be empty, fixing it further shouldn't do anything
for i in cache_size..cache_size * 2 {
assert!(cache.index.is_empty());
assert!(cache.blocks().is_empty());
assert!(cache.tx_in_block.is_empty());
let header = chain
.at_height(chain.get_block_count() as usize - i)
.deref()
.header;
cache.remove_disconnected_block(&header.block_hash());
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,9 @@
[package]
name = "watchtower-plugin"
version = "0.2.0"
version = "0.1.0"
authors = ["Sergi Delgado Segura <sergi.delgado.s@gmail.com>"]
license = "MIT"
edition = "2021"
edition = "2018"
[[bin]]
name = "watchtower-client"
@ -16,21 +16,21 @@ path = "src/main.rs"
backoff = { version = "0.4.0", features = ["tokio"] }
hex = { version = "0.4.3", features = [ "serde" ] }
home = "0.5.3"
reqwest = { version = "0.11", features = [ "blocking", "json", "socks" ] }
reqwest = { version = "0.11", features = [ "blocking", "json" ] }
log = "0.4.16"
rusqlite = { version = "0.26.0", features = [ "bundled", "limits" ] }
serde = "1.0.130"
serde_json = { version = "1.0", features = [ "preserve_order" ] }
tonic = { version = "0.11", features = [ "tls", "transport" ] }
tonic = { version = "^0.5", features = [ "tls", "transport" ] }
tokio = { version = "1.5", features = [ "rt-multi-thread", "fs" ] }
# Bitcoin and Lightning
bitcoin = "0.32.0"
cln-plugin = "0.3.0"
bitcoin = "0.27"
cln-plugin = "0.1.0"
# Local
teos-common = { path = "../teos-common" }
[dev-dependencies]
mockito = "0.32.4"
tempdir = "0.3.7"
httpmock = "0.6"
tempdir = "0.3.7"

View file

@ -8,16 +8,11 @@ commitment transaction is generated. It also keeps a summary of the messages sen
The plugin has the following methods:
- `registertower <tower_id>`: registers the user id (compressed public key) with a given tower.
- `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.
- `getappointmentreceipt <tower_id> <locator>`: pulls a given appointment receipt from the local database.
- `getregistrationreceipt <tower_id>`: pulls the latest registration receipt from the local database.
- `registertower tower_id` : registers the user id (compressed public key) with a given tower.
- `list_towers`: lists all registered towers.
- `gettowerinfo tower_id`: gets all the locally stored data about a given tower.
- `retrytower tower_id`: tries to send pending appointment to a (previously) unreachable tower.
- `getappointment tower_id locator`: queries a given tower about an appointment.
The plugin also has an implicit method to send appointments to the registered towers for every new commitment transaction.
@ -26,7 +21,7 @@ The plugin also has an implicit method to send appointments to the registered to
The first step to add the plugin to CLN is installing it. To do so you need to run (from the `rust-teos` folder):
```
cargo install --locked --path watchtower-plugin
cargo install --path watchtower-plugin
```
That will generate a binary called `watchtower-client`. That's the binary we need to link to CLN.
@ -109,12 +104,7 @@ All the appointments generated by the tower, as well as all the registered tower
Config options can be setup directly in the [CLN config file](https://github.com/ElementsProject/lightning#configuration-file). The currently available options are:
- `watchtower-port`: default tower API port.
- `watchtower-max-retry-time`: for how long (in seconds) a retry strategy will try to reach a temporary unreachable tower before giving up (default: 1 hour).
- `watchtower-auto-retry-delay`: how long (in seconds) the client will wait before auto-retrying a failed tower (default: 8 hours).
- `proxy`: Set a socks v5 proxy IP address and port. Notice this is necessary if you want to connect to a tower through Tor! (default: no proxy).
- `always-use-proxy`: Use the proxy always (default: false).
Notice `proxy` and `always-use-proxy` are general CLN options that are honored by the plugin, so if set the plugin will use Tor to communicate with the tower.
- `watchtower-max-retry-time`: the maximum time a retry strategy will try to reach a temporary unreachable tower before giving up.
# Getting started

View file

@ -1,52 +0,0 @@
// Collection of ENV variable names and values
pub const TOWERS_DATA_DIR: &str = "TOWERS_DATA_DIR";
pub const DEFAULT_TOWERS_DATA_DIR: &str = ".watchtower";
/// Collections of plugin option names, default values and descriptions
pub const WT_PORT: &str = "watchtower-port";
pub const DEFAULT_WT_PORT: i64 = 9814;
pub const WT_PORT_DESC: &str = "tower API port";
pub const WT_MAX_RETRY_TIME: &str = "watchtower-max-retry-time";
pub const DEFAULT_WT_MAX_RETRY_TIME: i64 = 3600;
pub const WT_MAX_RETRY_TIME_DESC: &str = "for how long (in seconds) a retry strategy will try to reach a temporary unreachable tower before giving up. Defaults to 1 hour";
pub const WT_AUTO_RETRY_DELAY: &str = "watchtower-auto-retry-delay";
pub const DEFAULT_WT_AUTO_RETRY_DELAY: i64 = 28800;
pub const WT_AUTO_RETRY_DELAY_DESC: &str = "how long (in seconds) a retrier will wait before auto-retrying a failed tower. Defaults to once every 8 hours";
pub const DEV_WT_MAX_RETRY_INTERVAL: &str = "dev-watchtower-max-retry-interval";
pub const DEFAULT_DEV_WT_MAX_RETRY_INTERVAL: i64 = 900;
pub const DEV_WT_MAX_RETRY_INTERVAL_DESC: &str =
"maximum length (in seconds) for a retry interval. Defaults to 15 min";
/// Collections of rpc method names and descriptions
pub const RPC_REGISTER_TOWER: &str = "registertower";
pub const RPC_REGISTER_TOWER_DESC: &str =
"Registers the client public key (user id) with the tower";
pub const RPC_GET_REGISTRATION_RECEIPT: &str = "getregistrationreceipt";
pub const RPC_GET_REGISTRATION_RECEIPT_DESC: &str =
"Gets the latest registration receipt given a tower id";
pub const RPC_GET_APPOINTMENT: &str = "getappointment";
pub const RPC_GET_APPOINTMENT_DESC: &str =
"Gets appointment data from the tower given a tower id and a locator";
pub const RPC_GET_APPOINTMENT_RECEIPT: &str = "getappointmentreceipt";
pub const RPC_GET_APPOINTMENT_RECEIPT_DESC: &str =
"Gets a (local) appointment receipt given a tower id and a locator";
pub const RPC_GET_SUBSCRIPTION_INFO: &str = "getsubscriptioninfo";
pub const RPC_GET_SUBSCRIPTION_INFO_DESC: &str =
"Gets the subscription information directly from the tower";
pub const RPC_LIST_TOWERS: &str = "listtowers";
pub const RPC_LIST_TOWERS_DESC: &str = "Lists all registered towers";
pub const RPC_GET_TOWER_INFO: &str = "gettowerinfo";
pub const RPC_GET_TOWER_INFO_DESC: &str = "Shows the info about a tower given a tower id";
pub const RPC_RETRY_TOWER: &str = "retrytower";
pub const RPC_RETRY_TOWER_DESC: &str =
"Retries to send pending appointment to an unreachable tower";
pub const RPC_ABANDON_TOWER: &str = "abandontower";
pub const RPC_ABANDON_TOWER_DESC: &str = "Forgets about a tower and wipes all local data";
pub const RPC_PING: &str = "pingtower";
pub const RPC_PING_DESC: &str = "Polls the tower to check if it is online";
/// Collections of hook names
pub const HOOK_COMMITMENT_REVOCATION: &str = "commitment_revocation";

View file

@ -3,7 +3,6 @@ use std::{convert::TryFrom, str::FromStr};
use hex::FromHex;
use serde::{Deserialize, Serialize};
use serde_json::json;
use bitcoin::{Transaction, Txid};
@ -22,10 +21,10 @@ pub enum RegisterError {
impl std::fmt::Display for RegisterError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
RegisterError::InvalidId(x) => write!(f, "{x}"),
RegisterError::InvalidHost(x) => write!(f, "{x}"),
RegisterError::InvalidPort(x) => write!(f, "{x}"),
RegisterError::InvalidFormat(x) => write!(f, "{x}"),
RegisterError::InvalidId(x) => write!(f, "{}", x),
RegisterError::InvalidHost(x) => write!(f, "{}", x),
RegisterError::InvalidPort(x) => write!(f, "{}", x),
RegisterError::InvalidFormat(x) => write!(f, "{}", x),
}
}
}
@ -56,7 +55,7 @@ impl RegisterParams {
fn from_id(tower_id: &str) -> Result<Self, RegisterError> {
Ok(Self {
tower_id: TowerId::from_str(tower_id)
.map_err(|_| RegisterError::InvalidId("Invalid tower id".to_owned()))?,
.map_err(|_| RegisterError::InvalidId("Invalid tower id".into()))?,
host: None,
port: None,
})
@ -64,10 +63,10 @@ impl RegisterParams {
fn with_host(self, host: &str) -> Result<Self, RegisterError> {
if host.is_empty() {
Err(RegisterError::InvalidHost("hostname is empty".to_owned()))
Err(RegisterError::InvalidHost("hostname is empty".into()))
} else if host.contains(' ') {
Err(RegisterError::InvalidHost(
"hostname contains white spaces".to_owned(),
"hostname contains white spaces".into(),
))
} else {
Ok(Self {
@ -80,7 +79,8 @@ impl RegisterParams {
fn with_port(self, port: u64) -> Result<Self, RegisterError> {
if port > u16::MAX as u64 {
Err(RegisterError::InvalidPort(format!(
"port must be a 16-byte integer. Received: {port}"
"port must be a 16-byte integer. Received: {}",
port
)))
} else {
Ok(Self {
@ -94,8 +94,6 @@ 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) => {
@ -110,7 +108,7 @@ impl TryFrom<serde_json::Value> for RegisterParams {
let port = if let Some(p) = v.next() {
p.parse()
.map(Some)
.map_err(|_| RegisterError::InvalidPort(format!("Port is not a number: {p}")))?
.map_err(|_| RegisterError::InvalidPort(format!("Port is not a number: {}", p)))?
} else {
None
};
@ -122,46 +120,35 @@ impl TryFrom<serde_json::Value> for RegisterParams {
},
serde_json::Value::Array(mut a) => {
let param_count = a.len();
match param_count {
1 => RegisterParams::try_from(a.pop().unwrap()),
2 | 3 => {
let tower_id = a.get(0).unwrap().as_str().ok_or_else(|| RegisterError::InvalidId("tower_id must be a string".to_string()))?;
let host = Some(a.get(1).unwrap().as_str().ok_or_else(|| RegisterError::InvalidHost("host must be a string".to_string()))?);
let port = if let Some(p) = a.get(2) {
Some(p.as_u64().ok_or_else(|| RegisterError::InvalidPort(format!("port must be a number. Received: {p}")))?)
} else {
let tower_id = a.get(0).unwrap();
let host = a.get(1).unwrap();
if !tower_id.is_string() {
return Err(RegisterError::InvalidId(format!("tower_id must be a string. Received: {}", tower_id)));
}
if !host.is_string() {
return Err(RegisterError::InvalidHost(format!("host must be a string. Received: {}", host)));
}
let port = if param_count == 3 {
let p = a.get(2).unwrap();
if !p.is_u64() {
return Err(RegisterError::InvalidPort(format!("port must be a number. Received: {}", p)));
}
p.as_u64()
} else{
None
};
RegisterParams::new(tower_id, host, port)
RegisterParams::new(tower_id.as_str().unwrap(), host.as_str(), port)
}
_ => Err(RegisterError::InvalidFormat(format!("Unexpected request format. The request needs 1-3 parameters. Received: {param_count}"))),
}
},
serde_json::Value::Object(mut m) => {
let allowed_keys = ["tower_id", "host", "port"];
let param_count = m.len();
if m.is_empty() || param_count > allowed_keys.len() {
Err(RegisterError::InvalidFormat(format!("Unexpected request format. The request needs 1-3 parameters. Received: {param_count}")))
} else if !m.contains_key(allowed_keys[0]){
Err(RegisterError::InvalidId(format!("{} is mandatory", allowed_keys[0])))
} else if !m.iter().all(|(k, _)| allowed_keys.contains(&k.as_str())) {
Err(RegisterError::InvalidFormat("Invalid named parameter found in request".to_owned()))
} else {
let mut params = Vec::with_capacity(allowed_keys.len());
for k in allowed_keys {
if let Some(v) = m.remove(k) {
params.push(v);
}
}
RegisterParams::try_from(json!(params))
_ => Err(RegisterError::InvalidFormat(format!("Unexpected request format. The request needs 1-3 parameters. Received: {}", param_count))),
}
},
_ => Err(RegisterError::InvalidFormat(
format!("Unexpected request format. Expected: 'tower_id[@host][:port]' or 'tower_id [host] [port]'. Received: '{value}'"),
format!("Unexpected request format. Expected: 'tower_id[@host][:port]' or 'tower_id [host] [port]'. Received: '{}'", value),
)),
}
}
@ -178,9 +165,9 @@ pub enum GetAppointmentError {
impl std::fmt::Display for GetAppointmentError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
GetAppointmentError::InvalidId(x) => write!(f, "{x}"),
GetAppointmentError::InvalidLocator(x) => write!(f, "{x}"),
GetAppointmentError::InvalidFormat(x) => write!(f, "{x}"),
GetAppointmentError::InvalidId(x) => write!(f, "{}", x),
GetAppointmentError::InvalidLocator(x) => write!(f, "{}", x),
GetAppointmentError::InvalidFormat(x) => write!(f, "{}", x),
}
}
}
@ -195,68 +182,41 @@ 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) => {
let param_count = a.len();
if param_count != 2 {
Err(GetAppointmentError::InvalidFormat(format!(
"Unexpected request format. The request needs 2 parameter. Received: {param_count}"
"Unexpected request format. The request needs 2 parameter. Received: {}",
param_count
)))
} else {
let tower_id = if let Some(s) = a.get(0).unwrap().as_str() {
TowerId::from_str(s).map_err(|_| {
GetAppointmentError::InvalidId("Invalid tower id".to_owned())
})
TowerId::from_str(s)
.map_err(|_| GetAppointmentError::InvalidId("Invalid tower id".into()))
} else {
Err(GetAppointmentError::InvalidId(
"tower_id must be a hex encoded string".to_owned(),
"tower_id must be a hex encoded string".into(),
))
}?;
let locator = if let Some(s) = a.get(1).unwrap().as_str() {
Locator::from_hex(s).map_err(|_| {
GetAppointmentError::InvalidLocator("Invalid locator".to_owned())
GetAppointmentError::InvalidLocator("Invalid locator".into())
})
} else {
Err(GetAppointmentError::InvalidLocator(
"locator must be a hex encoded string".to_owned(),
"locator must be a hex encoded string".into(),
))
}?;
Ok(Self { tower_id, locator })
}
}
serde_json::Value::Object(mut m) => {
let allowed_keys = ["tower_id", "locator"];
if m.len() > allowed_keys.len() {
return Err(GetAppointmentError::InvalidFormat(
"Invalid named argument found in request".to_owned(),
));
}
// DISCUSS: There may be a more idiomatic way of doing this
for k in allowed_keys.iter() {
if !m.contains_key(*k) {
return Err(GetAppointmentError::InvalidFormat(format!(
"{k} is mandatory"
)));
}
}
let mut params = Vec::with_capacity(allowed_keys.len());
for k in allowed_keys {
if let Some(v) = m.remove(k) {
params.push(v);
}
}
GetAppointmentParams::try_from(json!(params))
}
_ => Err(GetAppointmentError::InvalidFormat(format!(
"Unexpected request format. Expected: tower_id locator. Received: '{value}'"
"Unexpected request format. Expected: tower_id locator. Received: '{}'",
value
))),
}
}
@ -277,7 +237,6 @@ pub struct CommitmentRevocation {
mod tests {
use super::*;
use serde_json::json;
use std::collections::HashMap;
const VALID_ID: &str = "020dea894c967319407265764aba31bdef75d463f96800f34dd6df61380d82dfc0";
@ -303,7 +262,7 @@ mod tests {
// Any properly formatted host should work
let params = RegisterParams::from_id(VALID_ID).unwrap();
let host = "myhost";
assert_eq!(params.with_host(host).unwrap().host, Some(host.to_owned()));
assert_eq!(params.with_host(host).unwrap().host, Some(host.into()));
// Host must not be empty not have spaces
assert!(matches!(
@ -338,18 +297,21 @@ mod tests {
#[test]
fn test_try_from_json_string() {
let ok = [
format!("{VALID_ID}@host:80"),
format!("{VALID_ID}@host"),
format!("{}@host:80", VALID_ID),
format!("{}@host", VALID_ID),
VALID_ID.to_string(),
];
let wrong_id = ["", "id@host:80", "@host:80", "@:80"];
let wrong_host = [
format!("{VALID_ID}@"),
format!("{VALID_ID}@ "),
format!("{VALID_ID}@ host"),
format!("{VALID_ID}@:80"),
format!("{}@", VALID_ID),
format!("{}@ ", VALID_ID),
format!("{}@ host", VALID_ID),
format!("{}@:80", VALID_ID),
];
let wrong_port = [
format!("{}@host:", VALID_ID),
format!("{}@host:port", VALID_ID),
];
let wrong_port = [format!("{VALID_ID}@host:"), format!("{VALID_ID}@host:port")];
for s in ok {
let v = serde_json::Value::Array(vec![serde_json::Value::String(s.to_string())]);
@ -409,61 +371,6 @@ mod tests {
assert!(matches!(p, Err(RegisterError::InvalidFormat(..))));
}
#[test]
fn test_try_from_json_dict() {
let id = json!(VALID_ID);
let host = json!("host");
let port = json!(80);
for v in [
HashMap::from([("tower_id", &id), ("host", &host), ("port", &port)]),
HashMap::from([("tower_id", &id), ("host", &host)]),
HashMap::from([("tower_id", &id)]),
] {
let p = RegisterParams::try_from(json!(v));
assert!(matches!(p, Ok(..)));
}
// Id key missing
let p =
RegisterParams::try_from(json!(HashMap::from([("host", &host), ("port", &port)])));
assert!(matches!(p, Err(RegisterError::InvalidId(..))));
// Wrong id key
let p = RegisterParams::try_from(json!(HashMap::from([
("wrong_tower_id", &id),
("tower_id", &id),
("host", &host),
("port", &port)
])));
assert!(matches!(p, Err(RegisterError::InvalidFormat(..))));
// Wrong host key
let p = RegisterParams::try_from(json!(HashMap::from([
("tower_id", &id),
("wrong_host", &host),
("port", &port)
])));
assert!(matches!(p, Err(RegisterError::InvalidFormat(..))));
// Wrong port key
let p = RegisterParams::try_from(json!(HashMap::from([
("tower_id", &id),
("host", &host),
("wrong_port", &port)
])));
assert!(matches!(p, Err(RegisterError::InvalidFormat(..))));
// Wrong param count (params should be 1-3)
let p = RegisterParams::try_from(json!(HashMap::from([
("tower_id", &id),
("host", &host),
("port", &port),
("another_param", &json!(0))
])));
assert!(matches!(p, Err(RegisterError::InvalidFormat(..))));
}
#[test]
fn test_try_from_other_json() {
// Unexpected json object (it must be either String or Array)
@ -507,40 +414,6 @@ mod tests {
assert!(matches!(p, Err(GetAppointmentError::InvalidLocator(..))));
}
#[test]
fn test_try_from_dict() {
let id = json!(VALID_ID);
let locator = json!("c69517f00d9482e6b1c41639f9bdfd5c");
// Valid params
let p = GetAppointmentParams::try_from(json!(HashMap::from([
("tower_id", &id),
("locator", &locator)
])));
assert!(matches!(p, Ok(..)));
// Wrong keys
let p = GetAppointmentParams::try_from(json!(HashMap::from([
("wrong_tower_id", &id),
("locator", &locator)
])));
assert!(matches!(p, Err(GetAppointmentError::InvalidFormat(..))));
let p = GetAppointmentParams::try_from(json!(HashMap::from([
("tower_id", &id),
("wrong_locator", &locator)
])));
assert!(matches!(p, Err(GetAppointmentError::InvalidFormat(..))));
// Too many parameters
let p = GetAppointmentParams::try_from(json!(HashMap::from([
("tower_id", &id),
("locator", &locator),
("another_param", &json!(0))
])));
assert!(matches!(p, Err(GetAppointmentError::InvalidFormat(..))));
}
#[test]
fn test_try_from_other_json() {
// Unexpected json object (it must be either String or Array)

View file

@ -118,14 +118,14 @@ impl DBM {
/// When a new key is generated, old keys are not overwritten but are not retrievable from the API either.
pub fn store_client_key(&self, sk: &SecretKey) -> Result<(), Error> {
let query = "INSERT INTO keys (key) VALUES (?)";
self.store_data(query, params![sk.display_secret().to_string()])
self.store_data(query, params![sk.to_string()])
}
/// Loads the last known client secret key from the database.
///
/// Loads the key with higher id from the database. Old keys are not overwritten just in case a recovery is needed,
/// but they are not accessible from the API either.
pub fn load_client_key(&self) -> Option<SecretKey> {
pub fn load_client_key(&self) -> Result<SecretKey, Error> {
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())
})
.ok()
.map_err(|_| Error::NotFound)
}
/// 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) -> Option<TowerInfo> {
pub fn load_tower_record(&self, tower_id: TowerId) -> Result<TowerInfo, Error> {
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),
))
})
.ok()?;
.map_err(|_| Error::NotFound)?;
if let Some(proof) = self.load_misbehaving_proof(tower_id) {
if let Ok(proof) = self.load_misbehaving_proof(tower_id) {
tower.status = TowerStatus::Misbehaving;
tower.set_misbehaving_proof(proof);
} else if !tower.pending_appointments.is_empty() {
tower.status = TowerStatus::TemporaryUnreachable;
}
Some(tower)
Ok(tower)
}
/// Loads the latest registration receipt for a given tower.
@ -216,11 +216,11 @@ impl DBM {
&self,
tower_id: TowerId,
user_id: UserId,
) -> Option<RegistrationReceipt> {
) -> Result<RegistrationReceipt, Error> {
let mut stmt = self
.connection
.prepare(
"SELECT available_slots, subscription_start, subscription_expiry, signature
"SELECT *
FROM registration_receipts
WHERE tower_id = ?1 AND subscription_expiry = (SELECT MAX(subscription_expiry)
FROM registration_receipts
@ -228,26 +228,20 @@ impl DBM {
)
.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();
let receipt = stmt
.query_row([tower_id.to_vec()], |row| {
let slots: u32 = row.get(1).unwrap();
let start: u32 = row.get(2).unwrap();
let expiry: u32 = row.get(3).unwrap();
let signature: String = row.get(4).unwrap();
Ok(RegistrationReceipt::with_signature(
user_id, slots, start, expiry, signature,
))
})
.ok()
}
Ok(RegistrationReceipt::with_signature(
user_id, slots, start, expiry, signature,
))
})
.map_err(|_| Error::NotFound)?;
/// Removes a tower record from the database.
///
/// This triggers a cascade deletion of all related data, such as appointments, appointment receipts, etc. As long as there is a single
/// reference to them.
pub fn remove_tower_record(&self, tower_id: TowerId) -> Result<(), Error> {
let query = "DELETE FROM towers WHERE tower_id=?";
self.remove_data(query, params![tower_id.to_vec()])
Ok(receipt)
}
/// Loads all tower records from the database.
@ -286,9 +280,6 @@ impl DBM {
if self.exists_misbehaving_proof(tower_id) {
tower.status = TowerStatus::Misbehaving;
} else if !tower.pending_appointments.is_empty() {
// TODO: We could set the status to SubscriptionError here if we checked the state of the subscription
// (using available_slots and expiry). This will be possible once we implement cln rpc queries (which are
// already viable since cln-plugin = "0.1.1").
tower.status = TowerStatus::TemporaryUnreachable;
}
@ -325,32 +316,7 @@ impl DBM {
tx.commit()
}
/// Loads a given appointment receipt of a given tower from the database.
pub fn load_appointment_receipt(
&self,
tower_id: TowerId,
locator: Locator,
) -> Option<AppointmentReceipt> {
let mut stmt = self
.connection
.prepare("SELECT start_block, user_signature, tower_signature FROM appointment_receipts WHERE tower_id = ?1 and locator = ?2")
.unwrap();
stmt.query_row(params![tower_id.to_vec(), locator.to_vec()], |row| {
let start_block = row.get::<_, u32>(0).unwrap();
let user_sig = row.get::<_, String>(1).unwrap();
let tower_sig = row.get::<_, String>(2).unwrap();
Ok(AppointmentReceipt::with_signature(
user_sig,
start_block,
tower_sig,
))
})
.ok()
}
/// Loads the appointment receipts associated to a given tower.
/// Loads the appointment receipts associated to a given tower
///
/// TODO: Currently this is only loading a summary of the receipt, if we need to really load all the information
/// for any reason this method may need to be renamed.
@ -390,7 +356,10 @@ impl DBM {
// TODO: Can this be prepared instead of formatted (using ?1 seems to fail)?
let mut stmt = self
.connection
.prepare(&format!("SELECT locator FROM {status} WHERE tower_id = ?"))
.prepare(&format!(
"SELECT locator FROM {} WHERE tower_id = ?",
status
))
.unwrap();
let mut rows = stmt.query(params![tower_id.to_vec()]).unwrap();
@ -402,22 +371,6 @@ impl DBM {
appointments
}
/// Loads an appointment from the database.
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 = ?")
.unwrap();
stmt.query_row(params![locator.to_vec()], |row| {
let encrypted_blob = row.get::<_, Vec<u8>>(0).unwrap();
let to_self_delay = row.get::<_, u32>(1).unwrap();
Ok(Appointment::new(locator, encrypted_blob, to_self_delay))
})
.ok()
}
/// Stores an appointment into the database.
///
/// Appointments are only stored as a whole when they are pending or invalid.
@ -546,7 +499,7 @@ impl DBM {
let mut appointments = Vec::new();
let mut stmt = self
.connection
.prepare(&format!("SELECT a.locator, a.encrypted_blob, a.to_self_delay FROM appointments as a, {table} as t WHERE a.locator = t.locator AND t.tower_id = ?"))
.prepare(&format!("SELECT * FROM appointments as a, {} as t WHERE a.locator = t.locator AND t.tower_id = ?", table))
.unwrap();
let mut rows = stmt.query([tower_id.to_vec()]).unwrap();
@ -595,7 +548,7 @@ impl DBM {
}
/// Loads the misbehaving proof for a given tower from the database (if found).
fn load_misbehaving_proof(&self, tower_id: TowerId) -> Option<MisbehaviorProof> {
fn load_misbehaving_proof(&self, tower_id: TowerId) -> Result<MisbehaviorProof, Error> {
let mut misbehaving_stmt = self
.connection
.prepare("SELECT locator, recovered_id FROM misbehaving_proofs WHERE tower_id = ?")
@ -630,7 +583,7 @@ impl DBM {
.unwrap();
MisbehaviorProof::new(locator, receipt, recovered_id)
})
.ok()
.map_err(|_| Error::NotFound)
}
/// Checks whether a misbehaving proof exists for a given tower.
@ -647,7 +600,6 @@ impl DBM {
mod tests {
use super::*;
use teos_common::cryptography::get_random_keypair;
use teos_common::test_utils::{
generate_random_appointment, get_random_registration_receipt, get_random_user_id,
get_registration_receipt_from_previous,
@ -670,19 +622,6 @@ mod tests {
.unwrap();
stmt.exists(params![locator.to_vec()]).unwrap()
}
pub(crate) fn appointment_receipt_exists(
&self,
locator: Locator,
tower_id: TowerId,
) -> bool {
let mut stmt = self
.connection
.prepare("SELECT * FROM appointment_receipts WHERE locator=?1 AND tower_id=?2 ")
.unwrap();
stmt.exists(params![locator.to_vec(), tower_id.to_vec()])
.unwrap()
}
}
#[test]
@ -702,7 +641,7 @@ mod tests {
let receipt = get_random_registration_receipt();
let tower_info = TowerInfo::new(
net_addr.to_owned(),
net_addr.into(),
receipt.available_slots(),
receipt.subscription_start(),
receipt.subscription_expiry(),
@ -789,7 +728,10 @@ mod tests {
// If the tower does not exists, `load_tower` will fail.
let tower_id = get_random_user_id();
assert!(dbm.load_tower_record(tower_id).is_none());
assert!(matches!(
dbm.load_tower_record(tower_id),
Err(Error::NotFound)
));
}
#[test]
@ -815,7 +757,7 @@ mod tests {
towers.insert(
tower_id,
TowerSummary::new(
net_addr.to_owned(),
net_addr.into(),
receipt.available_slots(),
receipt.subscription_start(),
receipt.subscription_expiry(),
@ -833,29 +775,6 @@ mod tests {
assert_eq!(dbm.load_towers(), HashMap::new());
}
#[test]
fn test_remove_tower_record() {
let mut dbm = DBM::in_memory().unwrap();
let tower_id = get_random_user_id();
let net_addr = "talaia.watch";
let receipt = get_random_registration_receipt();
dbm.store_tower_record(tower_id, net_addr, &receipt)
.unwrap();
assert!(matches!(dbm.remove_tower_record(tower_id), Ok(())));
}
#[test]
fn test_remove_tower_record_inexistent() {
let dbm = DBM::in_memory().unwrap();
assert!(matches!(
dbm.remove_tower_record(get_random_user_id()),
Err(Error::NotFound)
));
}
#[test]
fn test_store_load_appointment_receipts() {
let mut dbm = DBM::in_memory().unwrap();
@ -866,7 +785,7 @@ mod tests {
let receipt = get_random_registration_receipt();
let mut tower_summary = TowerSummary::new(
net_addr.to_owned(),
net_addr.into(),
receipt.available_slots(),
receipt.subscription_start(),
receipt.subscription_expiry(),
@ -880,9 +799,9 @@ mod tests {
let appointment = generate_random_appointment(None);
let user_signature = "user_signature";
let appointment_receipt = AppointmentReceipt::with_signature(
user_signature.to_owned(),
user_signature.into(),
42,
"tower_signature".to_owned(),
"tower_signature".into(),
);
tower_summary.available_slots -= 1;
@ -903,55 +822,6 @@ mod tests {
assert_eq!(dbm.load_appointment_receipts(tower_id), receipts);
}
#[test]
fn test_load_appointment_receipt() {
let mut dbm = DBM::in_memory().unwrap();
let tower_id = get_random_user_id();
let appointment = generate_random_appointment(None);
// If there is no appointment receipt for the given (locator, tower_id) pair, Error::NotFound is returned
// Try first with both being unknown
assert!(dbm
.load_appointment_receipt(tower_id, appointment.locator)
.is_none());
// Add the tower but not the appointment and try again
let net_addr = "talaia.watch";
let receipt = get_random_registration_receipt();
dbm.store_tower_record(tower_id, net_addr, &receipt)
.unwrap();
assert!(dbm
.load_appointment_receipt(tower_id, appointment.locator)
.is_none());
// Add both
let tower_summary = TowerSummary::new(
net_addr.to_owned(),
receipt.available_slots(),
receipt.subscription_start(),
receipt.subscription_expiry(),
);
let appointment_receipt = AppointmentReceipt::with_signature(
"user_signature".to_owned(),
42,
"tower_signature".to_owned(),
);
dbm.store_appointment_receipt(
tower_id,
appointment.locator,
tower_summary.available_slots,
&appointment_receipt,
)
.unwrap();
assert_eq!(
dbm.load_appointment_receipt(tower_id, appointment.locator)
.unwrap(),
appointment_receipt
);
}
#[test]
fn test_load_appointment_locators() {
// `load_appointment_locators` is used to load locators from either `appointment_receipts`, `pending_appointments` or `invalid_appointments`
@ -963,7 +833,7 @@ mod tests {
let receipt = get_random_registration_receipt();
let tower_summary = TowerSummary::new(
net_addr.to_owned(),
net_addr.into(),
receipt.available_slots(),
receipt.subscription_start(),
receipt.subscription_expiry(),
@ -979,9 +849,9 @@ mod tests {
for _ in 0..5 {
let appointment = generate_random_appointment(None);
let appointment_receipt = AppointmentReceipt::with_signature(
user_signature.to_owned(),
user_signature.into(),
42,
"tower_signature".to_owned(),
"tower_signature".into(),
);
let pending_appointment = generate_random_appointment(None);
let invalid_appointment = generate_random_appointment(None);
@ -1018,27 +888,7 @@ mod tests {
);
}
#[test]
fn test_store_load_appointment() {
let mut dbm = DBM::in_memory().unwrap();
let appointment = generate_random_appointment(None);
let tx = dbm.get_mut_connection().transaction().unwrap();
DBM::store_appointment(&tx, &appointment).unwrap();
tx.commit().unwrap();
let loaded_appointment = dbm.load_appointment(appointment.locator);
assert_eq!(appointment, loaded_appointment.unwrap());
}
#[test]
fn test_store_load_appointment_inexistent() {
let dbm = DBM::in_memory().unwrap();
let locator = generate_random_appointment(None).locator;
let loaded_appointment = dbm.load_appointment(locator);
assert!(loaded_appointment.is_none());
}
// `store_appointments` is implicitly tested by `store_pending_appointment` and `store_invalid_appointment`
#[test]
fn test_store_pending_appointment() {
@ -1050,7 +900,7 @@ mod tests {
let receipt = get_random_registration_receipt();
let mut tower_summary = TowerSummary::new(
net_addr.to_owned(),
net_addr.into(),
receipt.available_slots(),
receipt.subscription_start(),
receipt.subscription_expiry(),
@ -1180,7 +1030,7 @@ mod tests {
let receipt = get_random_registration_receipt();
let mut tower_summary = TowerSummary::new(
net_addr.to_owned(),
net_addr.into(),
receipt.available_slots(),
receipt.subscription_start(),
receipt.subscription_expiry(),
@ -1243,7 +1093,7 @@ mod tests {
let receipt = get_random_registration_receipt();
let tower_summary = TowerSummary::new(
net_addr.to_owned(),
net_addr.into(),
receipt.available_slots(),
receipt.subscription_start(),
receipt.subscription_expiry(),
@ -1258,9 +1108,9 @@ mod tests {
// Store a misbehaving proof and load it back
let appointment = generate_random_appointment(None);
let appointment_receipt = AppointmentReceipt::with_signature(
"user_signature".to_owned(),
"user_signature".into(),
42,
"tower_signature".to_owned(),
"tower_signature".into(),
);
let proof = MisbehaviorProof::new(
@ -1276,7 +1126,10 @@ mod tests {
#[test]
fn test_store_load_non_existing_misbehaving_proof() {
let dbm = DBM::in_memory().unwrap();
assert!(dbm.load_misbehaving_proof(get_random_user_id()).is_none());
assert!(matches!(
dbm.load_misbehaving_proof(get_random_user_id()),
Err(Error::NotFound)
));
}
#[test]
@ -1289,7 +1142,7 @@ mod tests {
let receipt = get_random_registration_receipt();
let tower_summary = TowerSummary::new(
net_addr.to_owned(),
net_addr.into(),
receipt.available_slots(),
receipt.subscription_start(),
receipt.subscription_expiry(),
@ -1304,9 +1157,9 @@ mod tests {
// // Store a misbehaving proof check
let appointment = generate_random_appointment(None);
let appointment_receipt = AppointmentReceipt::with_signature(
"user_signature".to_owned(),
"user_signature".into(),
42,
"tower_signature".to_owned(),
"tower_signature".into(),
);
let proof = MisbehaviorProof::new(
@ -1324,16 +1177,4 @@ mod tests {
let dbm = DBM::in_memory().unwrap();
assert!(!dbm.exists_misbehaving_proof(get_random_user_id()));
}
#[test]
fn test_store_load_client_key() {
let dbm = DBM::in_memory().unwrap();
assert!(dbm.load_client_key().is_none());
for _ in 0..7 {
let sk = get_random_keypair().0;
dbm.store_client_key(&sk).unwrap();
assert_eq!(dbm.load_client_key().unwrap(), sk);
}
}
}

View file

@ -4,11 +4,9 @@ use std::fmt;
use serde::Serialize;
use teos_common::appointment::{Appointment, Locator};
use teos_common::net::NetAddr;
use teos_common::receipts::AppointmentReceipt;
use teos_common::TowerId;
pub mod constants;
pub mod convert;
pub mod dbm;
pub mod net;
@ -73,14 +71,9 @@ impl TowerStatus {
*self == TowerStatus::Reachable
}
/// Whether the tower is unreachable or not.
pub fn is_temporary_unreachable(&self) -> bool {
*self == TowerStatus::TemporaryUnreachable
}
/// Whether the tower is unreachable or not.
pub fn is_unreachable(&self) -> bool {
*self == TowerStatus::Unreachable
*self == TowerStatus::TemporaryUnreachable || *self == TowerStatus::Unreachable
}
/// Whether the tower is misbehaving or not.
@ -92,18 +85,12 @@ impl TowerStatus {
pub fn is_subscription_error(&self) -> bool {
*self == TowerStatus::SubscriptionError
}
/// Whether the tower can be manually retried
pub fn is_retryable(&self) -> bool {
self.is_unreachable() || self.is_subscription_error()
}
}
/// Summarized data associated with a given tower.
#[derive(Clone, Serialize, Debug, PartialEq, Eq)]
pub struct TowerSummary {
#[serde(flatten)]
pub net_addr: NetAddr,
pub net_addr: String,
pub available_slots: u32,
subscription_start: u32,
pub subscription_expiry: u32,
@ -123,7 +110,7 @@ impl TowerSummary {
subscription_expiry: u32,
) -> Self {
Self {
net_addr: NetAddr::new(net_addr),
net_addr,
available_slots,
subscription_start,
subscription_expiry,
@ -143,7 +130,7 @@ impl TowerSummary {
invalid_appointments: HashSet<Locator>,
) -> Self {
Self {
net_addr: NetAddr::new(net_addr),
net_addr,
available_slots,
subscription_start,
subscription_expiry,
@ -158,20 +145,6 @@ impl TowerSummary {
self.status = status;
self
}
/// Updates the main information about the summary while preserving the appointment maps.
pub fn udpate(
&mut self,
net_addr: String,
available_slots: u32,
subscription_start: u32,
subscription_expiry: u32,
) {
self.net_addr = NetAddr::new(net_addr);
self.available_slots = available_slots;
self.subscription_start = subscription_start;
self.subscription_expiry = subscription_expiry;
}
}
impl From<TowerInfo> for TowerSummary {
@ -290,12 +263,11 @@ mod tests {
mod tower_status {
use super::*;
use TowerStatus::*;
#[test]
fn test_is_reachable() {
for status in STATUSES {
if status == Reachable {
if status == TowerStatus::Reachable {
assert!(status.is_reachable())
} else {
assert!(!status.is_reachable());
@ -303,32 +275,10 @@ mod tests {
}
}
#[test]
fn test_is_temporary_reachable() {
for status in STATUSES {
if status == TemporaryUnreachable {
assert!(status.is_temporary_unreachable())
} else {
assert!(!status.is_temporary_unreachable());
}
}
}
#[test]
fn test_is_unreachable() {
for status in STATUSES {
if status == Unreachable {
assert!(status.is_unreachable())
} else {
assert!(!status.is_unreachable());
}
}
}
#[test]
fn test_is_misbehaving() {
for status in STATUSES {
if status == Misbehaving {
if status == TowerStatus::Misbehaving {
assert!(status.is_misbehaving())
} else {
assert!(!status.is_misbehaving());
@ -339,24 +289,13 @@ mod tests {
#[test]
fn test_is_subscription_error() {
for status in STATUSES {
if status == SubscriptionError {
if status == TowerStatus::SubscriptionError {
assert!(status.is_subscription_error())
} else {
assert!(!status.is_subscription_error());
}
}
}
#[test]
fn test_is_retryable() {
for status in STATUSES {
if status == Unreachable || status == SubscriptionError {
assert!(status.is_retryable())
} else {
assert!(!status.is_retryable());
}
}
}
}
mod tower_summary {
@ -366,15 +305,9 @@ mod tests {
use teos_common::test_utils::generate_random_appointment;
impl TowerSummary {
pub fn set_net_addr(&mut self, net_addr: String) {
self.net_addr = NetAddr::new(net_addr);
}
}
#[test]
fn test_new() {
let net_addr: String = "addr".to_owned();
let net_addr: String = "addr".into();
let tower_summary = TowerSummary::new(
net_addr.clone(),
@ -385,7 +318,7 @@ mod tests {
assert_eq!(
tower_summary,
TowerSummary {
net_addr: NetAddr::new(net_addr),
net_addr,
available_slots: AVAILABLE_SLOTS,
subscription_start: SUBSCRIPTION_START,
subscription_expiry: SUBSCRIPTION_EXPIRY,
@ -398,7 +331,7 @@ mod tests {
#[test]
fn test_with_appointments() {
let net_addr: String = "addr".to_owned();
let net_addr: String = "addr".into();
let pending_appointments =
HashSet::from_iter([generate_random_appointment(None).locator]);
@ -416,7 +349,7 @@ mod tests {
assert_eq!(
tower_summary,
TowerSummary {
net_addr: NetAddr::new(net_addr),
net_addr,
available_slots: AVAILABLE_SLOTS,
subscription_start: SUBSCRIPTION_START,
subscription_expiry: SUBSCRIPTION_EXPIRY,
@ -430,7 +363,7 @@ mod tests {
#[test]
fn test_with_status() {
let mut tower_summary = TowerSummary::new(
"addr".to_owned(),
"addr".into(),
AVAILABLE_SLOTS,
SUBSCRIPTION_START,
SUBSCRIPTION_EXPIRY,
@ -469,7 +402,7 @@ mod tests {
#[test]
fn test_new() {
let tower_info = TowerInfo::new(
"addr".to_owned(),
"addr".into(),
AVAILABLE_SLOTS,
SUBSCRIPTION_START,
SUBSCRIPTION_EXPIRY,
@ -478,14 +411,14 @@ mod tests {
Vec::new(),
);
assert!(tower_info.status.is_reachable());
assert_eq!(tower_info.status, TowerStatus::Reachable);
assert!(tower_info.misbehaving_proof.is_none());
}
#[test]
fn test_with_status() {
let mut tower_info = TowerInfo::empty(
"addr".to_owned(),
"addr".into(),
AVAILABLE_SLOTS,
SUBSCRIPTION_START,
SUBSCRIPTION_EXPIRY,
@ -499,7 +432,7 @@ mod tests {
#[test]
fn test_set_misbehaving_proof() {
let mut tower_info = TowerInfo::empty(
"addr".to_owned(),
"addr".into(),
AVAILABLE_SLOTS,
SUBSCRIPTION_START,
SUBSCRIPTION_EXPIRY,
@ -507,9 +440,9 @@ mod tests {
assert_eq!(tower_info.misbehaving_proof, None);
let appointment_receipt = AppointmentReceipt::with_signature(
"user_signature".to_owned(),
"user_signature".into(),
SUBSCRIPTION_START + 1,
"tower_signature".to_owned(),
"tower_signature".into(),
);
let proof = MisbehaviorProof::new(
generate_random_appointment(None).locator,

View file

@ -1,83 +1,36 @@
use std::convert::TryFrom;
use std::env;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, MutexGuard};
use std::sync::{Arc, Mutex};
use home::home_dir;
use serde_json::json;
use tokio::io::{stdin, stdout};
use tokio::sync::mpsc::unbounded_channel;
use cln_plugin::options::config_type::DefaultInteger;
use cln_plugin::options::ConfigOption;
use cln_plugin::options::{ConfigOption, Value};
use cln_plugin::{anyhow, Builder, Error, Plugin};
use teos_common::appointment::{Appointment, Locator};
use teos_common::net::http::Endpoint;
use teos_common::net::NetAddr;
use teos_common::protos as common_msgs;
use teos_common::receipts::RegistrationReceipt;
use teos_common::TowerId;
use teos_common::{cryptography, errors};
use watchtower_plugin::convert::{CommitmentRevocation, GetAppointmentParams, RegisterParams};
use watchtower_plugin::net::http::{
self, get_request, post_request, process_post_response, AddAppointmentError, ApiResponse,
add_appointment, 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,
);
use watchtower_plugin::retrier::Retrier;
use watchtower_plugin::wt_client::WTClient;
use watchtower_plugin::TowerStatus;
fn to_cln_error(e: RequestError) -> Error {
let e = match e {
match e {
RequestError::ConnectionError(e) => anyhow!(e),
RequestError::DeserializeError(e) => anyhow!(e),
RequestError::Unexpected(e) => anyhow!(e),
};
log::info!("{e}");
e
}
/// Sends fresh data to a retrier as long as is does not exist, or it does and its running.
fn send_to_retrier(state: &MutexGuard<WTClient>, tower_id: TowerId, locator: Locator) {
if if let Some(status) = state.get_retrier_status(&tower_id) {
// A retrier in the retriers map can only be running or idle
status.is_running()
} else {
true
} {
state
.unreachable_towers
.send((tower_id, RevocationData::Fresh(locator)))
.unwrap();
} else {
log::debug!("Not sending data to idle retrier ({tower_id}, {locator})")
}
}
@ -93,7 +46,7 @@ async fn register(
v: serde_json::Value,
) -> Result<serde_json::Value, Error> {
let params = RegisterParams::try_from(v).map_err(|x| anyhow!(x))?;
let mut host = params.host.unwrap_or_else(|| "localhost".to_owned());
let host = params.host.unwrap_or_else(|| "localhost".into());
let tower_id = params.tower_id;
let user_id = plugin.state().lock().unwrap().user_id;
@ -101,29 +54,54 @@ async fn register(
// Otherwise the tower could just generate a subscription starting far in the future. For this we need to access lightning RPC
// which is not available in the current version of `cln-plugin` (but already on master). Add it for the next release.
// FIXME: This is a workaround. Ideally, `cln_plugin::options::Value` will implement `as_u64` so we can simply call and unwrap
// given that we are certain the option exists.
let port = params.port.unwrap_or(
u16::try_from(plugin.option(&WT_PORT_CONFG).unwrap())
.map_err(|_| anyhow!("{} out of range", constants::WT_PORT))?,
if let Value::Integer(x) = plugin.option("watchtower-port").unwrap() {
x as u16
} else {
// We will never end up here, but we need to define an else. Should be fixed alongside the previous fixme.
9814
},
);
let tower_net_addr = {
if !host.starts_with("http://") && !host.starts_with("https://") {
host = format!("http://{host}")
let mut tower_net_addr = format!("{}:{}", host, port);
if !tower_net_addr.starts_with("http") {
tower_net_addr = format!("http://{}", tower_net_addr)
}
let register_endpoint = format!("{}/register", tower_net_addr);
log::info!("Registering in the Eye of Satoshi (tower_id={})", tower_id);
let receipt = process_post_response(
post_request(
&register_endpoint,
&common_msgs::RegisterRequest {
user_id: user_id.to_vec(),
},
)
.await,
)
.await
.map(|r: common_msgs::RegisterResponse| {
RegistrationReceipt::with_signature(
user_id,
r.available_slots,
r.subscription_start,
r.subscription_expiry,
r.subscription_signature,
)
})
.map_err(|e| {
if e.is_connection() {
plugin
.state()
.lock()
.unwrap()
.set_tower_status(tower_id, TowerStatus::TemporaryUnreachable);
}
NetAddr::new(format!("{host}:{port}"))
};
let proxy = plugin.state().lock().unwrap().proxy.clone();
let receipt = http::register(tower_id, user_id, &tower_net_addr, &proxy)
.await
.map_err(|e| {
let mut state = plugin.state().lock().unwrap();
if e.is_connection() && state.towers.contains_key(&tower_id) {
state.set_tower_status(tower_id, TowerStatus::TemporaryUnreachable);
}
to_cln_error(e)
})?;
to_cln_error(e)
})?;
if !receipt.verify(&tower_id) {
return Err(anyhow!(
@ -135,7 +113,7 @@ async fn register(
.state()
.lock()
.unwrap()
.add_update_tower(tower_id, tower_net_addr.net_addr(), &receipt).map_err(|e| {
.add_update_tower(tower_id, tower_net_addr, &receipt).map_err(|e| {
if e.is_expiry() {
anyhow!("Registration receipt contains a subscription expiry that is not higher than the one we are currently registered for")
} else {
@ -153,67 +131,6 @@ async fn register(
Ok(json!(receipt))
}
/// Gets the latest registration receipt from the client to a given tower (if it exists).
///
/// This is pulled from the database
async fn get_registration_receipt(
plugin: Plugin<Arc<Mutex<WTClient>>>,
v: serde_json::Value,
) -> Result<serde_json::Value, Error> {
let tower_id = TowerId::try_from(v).map_err(|x| anyhow!(x))?;
let state = plugin.state().lock().unwrap();
if let Some(response) = state.get_registration_receipt(tower_id) {
Ok(json!(response))
} else {
Err(anyhow!(
"Cannot find {tower_id} within the known towers. Have you registered?"
))
}
}
/// Gets the subscription information directly form the tower.
async fn get_subscription_info(
plugin: Plugin<Arc<Mutex<WTClient>>>,
v: serde_json::Value,
) -> Result<serde_json::Value, Error> {
let tower_id = TowerId::try_from(v).map_err(|x| anyhow!(x))?;
let (user_sk, tower_net_addr, proxy) = {
let state = plugin.state().lock().unwrap();
if let Some(info) = state.towers.get(&tower_id) {
Ok((state.user_sk, info.net_addr.clone(), state.proxy.clone()))
} else {
Err(anyhow!("Unknown tower id: {tower_id}"))
}
}?;
let signature = cryptography::sign("get subscription info".as_bytes(), &user_sk);
let response: common_msgs::GetSubscriptionInfoResponse = process_post_response(
post_request(
&tower_net_addr,
Endpoint::GetSubscriptionInfo,
&common_msgs::GetSubscriptionInfoRequest { signature },
&proxy,
)
.await,
)
.await
.map_err(|e| {
if e.is_connection() {
plugin
.state()
.lock()
.unwrap()
.set_tower_status(tower_id, TowerStatus::TemporaryUnreachable);
}
to_cln_error(e)
})?;
Ok(json!(response))
}
/// Gets information about an appointment from the tower.
async fn get_appointment(
plugin: Plugin<Arc<Mutex<WTClient>>>,
@ -221,29 +138,30 @@ async fn get_appointment(
) -> Result<serde_json::Value, Error> {
let params = GetAppointmentParams::try_from(v).map_err(|x| anyhow!(x))?;
let (user_sk, tower_net_addr, proxy) = {
let user_sk = plugin.state().lock().unwrap().user_sk;
let tower_net_addr = {
let state = plugin.state().lock().unwrap();
if let Some(info) = state.towers.get(&params.tower_id) {
Ok((state.user_sk, info.net_addr.clone(), state.proxy.clone()))
Ok(info.net_addr.clone())
} else {
Err(anyhow!("Unknown tower id: {}", params.tower_id))
}
}?;
let get_appointment_endpoint = format!("{}/get_appointment", tower_net_addr);
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(
&tower_net_addr,
Endpoint::GetAppointment,
&get_appointment_endpoint,
&common_msgs::GetAppointmentRequest {
locator: params.locator.to_vec(),
signature,
},
&proxy,
)
.await,
)
@ -262,32 +180,6 @@ async fn get_appointment(
Ok(json!(response))
}
/// Gets an appointment receipt from the client given a tower_id and a locator (if it exists).
///
/// This is pulled from the database
async fn get_appointment_receipt(
plugin: Plugin<Arc<Mutex<WTClient>>>,
v: serde_json::Value,
) -> Result<serde_json::Value, Error> {
let params = GetAppointmentParams::try_from(v).map_err(|x| anyhow!(x))?;
let state = plugin.state().lock().unwrap();
if let Some(r) = state.get_appointment_receipt(params.tower_id, params.locator) {
Ok(json!(r))
} else if state.towers.contains_key(&params.tower_id) {
Err(anyhow!(
"Cannot find {} within {}. Did you send that appointment?",
params.locator,
params.tower_id
))
} else {
Err(anyhow!(
"Cannot find {} within the known towers. Have you registered?",
params.tower_id
))
}
}
/// Lists all the registered towers.
///
/// The given information comes from memory, so it is summarized.
@ -307,114 +199,45 @@ 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))?;
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 tower_info = state.load_tower_info(tower_id).map_err(|_| {
anyhow!(
"Cannot find {} within the known towers. Have you registered?",
tower_id
)
};
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()
)))
}
// 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.towers.get(&tower_id).unwrap().status)
))
}
/// Triggers a manual retry of a tower, tries to send all pending appointments to it.
///
/// Only works if the tower is unreachable or there's been a subscription error (and the tower is not already being retried).
/// Only works if the tower is unreachable or there's been a subscription error.
async fn retry_tower(
plugin: Plugin<Arc<Mutex<WTClient>>>,
v: serde_json::Value,
) -> Result<serde_json::Value, Error> {
let tower_id = TowerId::try_from(v).map_err(|e| anyhow!(e))?;
let state = plugin.state().lock().unwrap();
if let Some(tower_status) = state.get_tower_status(&tower_id) {
if let Some(retrier_status) = state.retriers.get(&tower_id) {
if retrier_status.is_idle() {
// We don't send any associated data in this case given the idle retrier already has it all.
state
.unreachable_towers
.send((tower_id, RevocationData::None))
.map_err(|e| anyhow!(e))?;
} else {
// Status can only be running or idle for data in the retriers map.
return Err(anyhow!("{tower_id} is already being retried"));
}
} else if tower_status.is_retryable() {
// We do send associated data here given there is no retrier associated to this tower.
state
.unreachable_towers
.send((
tower_id,
RevocationData::Stale(
state
.towers
.get(&tower_id)
.unwrap()
.pending_appointments
.iter()
.cloned()
.collect(),
),
))
.map_err(|e| anyhow!(e))?;
} else {
if let Some(tower) = state.towers.get(&tower_id) {
if tower.status == TowerStatus::TemporaryUnreachable {
return Err(anyhow!("{} is already being retried", tower_id));
} else if tower.status != TowerStatus::Unreachable {
return Err(anyhow!(
"Tower status must be unreachable or have a subscription issue to manually retry",
"Tower status must be unreachable to manually retry",
));
}
} else {
return Err(anyhow!("Unknown tower {tower_id}"));
}
Ok(json!(format!("Retrying {tower_id}")))
}
/// Forgets about a tower wiping out all local data associated to it.
async fn abandon_tower(
plugin: Plugin<Arc<Mutex<WTClient>>>,
v: serde_json::Value,
) -> 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.contains_key(&tower_id) {
state.remove_tower(tower_id).unwrap();
Ok(json!(format!("{tower_id} successfully abandoned")))
state
.unreachable_towers
.send(tower_id)
.map_err(|e| anyhow!(e))?;
Ok(json!(format!("Retrying {}", tower_id)))
} else {
Err(anyhow!("Unknown tower {tower_id}"))
Err(anyhow!("Unknown tower {}", tower_id))
}
}
@ -426,7 +249,7 @@ async fn on_commitment_revocation(
v: serde_json::Value,
) -> Result<serde_json::Value, Error> {
let commitment_revocation = serde_json::from_value::<CommitmentRevocation>(v)
.map_err(|e| anyhow!("Cannot decode commitment_revocation data. Error: {e}"))?;
.map_err(|e| anyhow!("Cannot decode commitment_revocation data. Error: {}", e))?;
log::debug!(
"New commitment revocation received for channel {}. Commit number {}",
commitment_revocation.channel_id,
@ -447,7 +270,8 @@ 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.
@ -460,12 +284,9 @@ async fn on_commitment_revocation(
.map(|(id, info)| (*id, info.net_addr.clone(), info.status))
.collect::<Vec<_>>();
let proxy = plugin.state().lock().unwrap().proxy.clone();
for (tower_id, net_addr, status) in towers {
if status.is_reachable() {
match http::add_appointment(tower_id, &net_addr, &proxy, &appointment, &signature).await
{
match add_appointment(tower_id, &net_addr, &appointment, &signature).await {
Ok((slots, receipt)) => {
plugin
.state()
@ -478,30 +299,31 @@ async fn on_commitment_revocation(
AddAppointmentError::RequestError(e) => {
if e.is_connection() {
log::warn!(
"{tower_id} cannot be reached. Adding {} to pending appointments",
"{} cannot be reached. Adding {} to pending appointments",
tower_id,
appointment.locator
);
let mut state = plugin.state().lock().unwrap();
state.set_tower_status(tower_id, TowerStatus::TemporaryUnreachable);
state.add_pending_appointment(tower_id, &appointment);
send_to_retrier(&state, tower_id, appointment.locator);
state.unreachable_towers.send(tower_id).unwrap();
}
}
AddAppointmentError::ApiError(e) => match e.error_code {
errors::INVALID_SIGNATURE_OR_SUBSCRIPTION_ERROR => {
log::warn!(
"There is a subscription issue with {tower_id}. Adding {} to pending",
appointment.locator
);
log::warn!("There is a subscription issue with {}", tower_id);
let mut state = plugin.state().lock().unwrap();
state.set_tower_status(tower_id, TowerStatus::SubscriptionError);
state.add_pending_appointment(tower_id, &appointment);
send_to_retrier(&state, tower_id, appointment.locator);
state.unreachable_towers.send(tower_id).unwrap();
}
_ => {
log::warn!(
"{tower_id} rejected the appointment. Error: {}, error_code: {}",
"{} rejected the appointment. Error: {}, error_code: {}",
tower_id,
e.error,
e.error_code
);
@ -523,26 +345,25 @@ async fn on_commitment_revocation(
},
};
} else if status.is_misbehaving() {
log::warn!("{tower_id} is misbehaving. Not sending any further appointments",);
log::warn!(
"{} is misbehaving. Not sending any further appointments",
tower_id
);
} else {
if status.is_subscription_error() {
log::warn!(
"There is a subscription issue with {tower_id}. Adding {} to pending",
appointment.locator
"There is a subscription issue with {}. Adding appointment to pending",
tower_id,
);
} else {
log::warn!(
"{tower_id} is {status}. Adding {} to pending",
appointment.locator,
);
log::warn!("{} is {}. Adding appointment to pending", tower_id, status);
}
let mut state = plugin.state().lock().unwrap();
state.add_pending_appointment(tower_id, &appointment);
if !status.is_unreachable() {
send_to_retrier(&state, tower_id, appointment.locator);
}
plugin
.state()
.lock()
.unwrap()
.add_pending_appointment(tower_id, &appointment);
}
}
@ -552,119 +373,79 @@ async fn on_commitment_revocation(
#[tokio::main]
async fn main() -> Result<(), Error> {
let data_dir = match env::var(constants::TOWERS_DATA_DIR) {
let data_dir = match env::var("TOWERS_DATA_DIR") {
Ok(v) => PathBuf::from(v),
Err(_) => home_dir().unwrap().join(constants::DEFAULT_TOWERS_DATA_DIR),
};
let builder = Builder::new(stdin(), stdout())
.option(WT_PORT_CONFG)
.option(WT_MAX_RETRY_TIME_CONFIG)
.option(WT_AUTO_RETRY_DELAY_CONFIG)
.option(DEV_WT_MAX_RETRY_INTERVAL_CONFIG)
.rpcmethod(
constants::RPC_REGISTER_TOWER,
constants::RPC_REGISTER_TOWER_DESC,
register,
)
.rpcmethod(
constants::RPC_GET_REGISTRATION_RECEIPT,
constants::RPC_GET_REGISTRATION_RECEIPT_DESC,
get_registration_receipt,
)
.rpcmethod(
constants::RPC_GET_APPOINTMENT,
constants::RPC_GET_APPOINTMENT_DESC,
get_appointment,
)
.rpcmethod(
constants::RPC_GET_APPOINTMENT_RECEIPT,
constants::RPC_GET_APPOINTMENT_RECEIPT_DESC,
get_appointment_receipt,
)
.rpcmethod(
constants::RPC_GET_SUBSCRIPTION_INFO,
constants::RPC_GET_SUBSCRIPTION_INFO_DESC,
get_subscription_info,
)
.rpcmethod(
constants::RPC_LIST_TOWERS,
constants::RPC_LIST_TOWERS_DESC,
list_towers,
)
.rpcmethod(
constants::RPC_GET_TOWER_INFO,
constants::RPC_GET_TOWER_INFO_DESC,
get_tower_info,
)
.rpcmethod(constants::RPC_PING, constants::RPC_PING_DESC, ping)
.rpcmethod(
constants::RPC_RETRY_TOWER,
constants::RPC_RETRY_TOWER_DESC,
retry_tower,
)
.rpcmethod(
constants::RPC_ABANDON_TOWER,
constants::RPC_ABANDON_TOWER_DESC,
abandon_tower,
)
.hook(
constants::HOOK_COMMITMENT_REVOCATION,
on_commitment_revocation,
);
// We're unwrapping here given it does not seem we actually have anything to check at the moment.
// Change this so the plugin can be disabled soon if this happens not to be the case.
let midstate = if let Some(midstate) = builder.configure().await? {
midstate
} else {
return Ok(());
Err(_) => home_dir().unwrap().join(".watchtower"),
};
let (tx, rx) = unbounded_channel();
let wt_client = Arc::new(Mutex::new(
WTClient::with_proxy(
data_dir,
tx,
midstate.configuration().proxy.map(|proxy| {
// We don't need to inform `always-use-proxy` needing `proxy` to work. This is done by CLN already when needed.
ProxyInfo::new(
proxy,
midstate.configuration().always_use_proxy.unwrap_or(false),
)
}),
let state = Arc::new(Mutex::new(WTClient::new(data_dir, tx).await));
let state_clone = state.clone();
let builder = Builder::new(state, stdin(), stdout())
.option(ConfigOption::new(
"watchtower-port",
Value::Integer(9814),
"tower API port",
))
.option(ConfigOption::new(
"watchtower-max-retry-time",
Value::Integer(900),
"the time (in seconds) after where the retrier will give up trying to send data to a temporary unreachable tower",
))
.option(ConfigOption::new(
"dev-watchtower-max-retry-interval",
Value::Integer(60),
"the maximum time (in seconds) for a retrier wait interval",
))
.rpcmethod(
"registertower",
"Registers the client public key (user id) with the tower.",
register,
)
.await,
));
let max_elapsed_time = u16::try_from(midstate.option(&WT_MAX_RETRY_TIME_CONFIG).unwrap())
.inspect_err(|_| {
log::error!("{} out of range", constants::WT_MAX_RETRY_TIME);
})?;
let auto_retry_delay = u32::try_from(midstate.option(&WT_AUTO_RETRY_DELAY_CONFIG).unwrap())
.inspect_err(|_| {
log::error!("{} out of range", constants::WT_AUTO_RETRY_DELAY);
})?;
let max_interval_time = u16::try_from(
midstate.option(&DEV_WT_MAX_RETRY_INTERVAL_CONFIG).unwrap(),
)
.inspect_err(|_| {
log::error!("{} out of range", constants::DEV_WT_MAX_RETRY_INTERVAL);
})?;
let plugin = midstate.start(wt_client.clone()).await?;
tokio::spawn(async move {
RetryManager::new(
wt_client,
rx,
max_elapsed_time,
auto_retry_delay,
max_interval_time,
.rpcmethod(
"getappointment",
"Gets appointment data from the tower given the tower id and the locator.",
get_appointment,
)
.manage_retry()
.await
});
plugin.join().await
.rpcmethod("listtowers", "Lists all registered towers.", list_towers)
.rpcmethod(
"gettowerinfo",
"Shows the info about a given tower.",
get_tower_info,
)
.rpcmethod(
"retrytower",
"Retries to send pending appointment to an unreachable tower.",
retry_tower,
)
.hook("commitment_revocation", on_commitment_revocation);
if let Some(plugin) = builder.start().await? {
// FIXME: This is a workaround. Ideally, `cln_plugin::options::Value` will implement `as_u64` so we can simply call and unwrap
// given that we are certain the option exists.
let max_elapsed_time =
if let Value::Integer(x) = plugin.option("watchtower-max-retry-time").unwrap() {
x as u16
} else {
// We will never end up here, but we need to define an else. Should be fixed alongside the previous fixme.
900
};
let max_interval_time = if let Value::Integer(x) =
plugin.option("dev-watchtower-max-retry-interval").unwrap()
{
x as u16
} else {
// We will never end up here, but we need to define an else. Should be fixed alongside the previous fixme.
60
};
tokio::spawn(async move {
Retrier::new(state_clone, max_elapsed_time, max_interval_time)
.manage_retry(rx)
.await
});
plugin.join().await
} else {
Ok(())
}
}

View file

@ -1,15 +1,12 @@
use reqwest::{Method, Response};
use reqwest::Response;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use teos_common::appointment::Appointment;
use teos_common::cryptography;
use teos_common::net::http::Endpoint;
use teos_common::net::NetAddr;
use teos_common::protos as common_msgs;
use teos_common::receipts::{AppointmentReceipt, RegistrationReceipt};
use teos_common::{TowerId, UserId};
use teos_common::receipts::AppointmentReceipt;
use teos_common::TowerId;
use crate::net::ProxyInfo;
use crate::MisbehaviorProof;
/// Represents a generic api response.
@ -55,52 +52,21 @@ impl From<RequestError> for AddAppointmentError {
}
}
/// Handles the logic of interacting with the `register` endpoint of the tower.
pub async fn register(
tower_id: TowerId,
user_id: UserId,
tower_net_addr: &NetAddr,
proxy: &Option<ProxyInfo>,
) -> Result<RegistrationReceipt, RequestError> {
log::info!("Registering in the Eye of Satoshi (tower_id={tower_id})");
process_post_response(
post_request(
tower_net_addr,
Endpoint::Register,
&common_msgs::RegisterRequest {
user_id: user_id.to_vec(),
},
proxy,
)
.await,
)
.await
.map(|r: common_msgs::RegisterResponse| {
RegistrationReceipt::with_signature(
user_id,
r.available_slots,
r.subscription_start,
r.subscription_expiry,
r.subscription_signature,
)
})
}
/// Encapsulates the logging and response parsing of sending and appointment to the tower.
pub async fn add_appointment(
tower_id: TowerId,
tower_net_addr: &NetAddr,
proxy: &Option<ProxyInfo>,
tower_net_addr: &str,
appointment: &Appointment,
signature: &str,
) -> Result<(u32, AppointmentReceipt), AddAppointmentError> {
log::debug!(
"Sending appointment {} to tower {tower_id}",
appointment.locator
"Sending appointment {} to tower {}",
appointment.locator,
tower_id
);
let (response, receipt) =
send_appointment(tower_id, tower_net_addr, proxy, appointment, signature).await?;
log::debug!("Appointment accepted and signed by {tower_id}");
send_appointment(tower_id, tower_net_addr, appointment, signature).await?;
log::debug!("Appointment accepted and signed by {}", tower_id);
log::debug!("Remaining slots: {}", response.available_slots);
log::debug!("Start block: {}", response.start_block);
@ -110,22 +76,19 @@ pub async fn add_appointment(
/// Handles the logic of interacting with the `add_appointment` endpoint of the tower.
pub async fn send_appointment(
tower_id: TowerId,
tower_net_addr: &NetAddr,
proxy: &Option<ProxyInfo>,
tower_net_addr: &str,
appointment: &Appointment,
signature: &str,
) -> Result<(common_msgs::AddAppointmentResponse, AppointmentReceipt), AddAppointmentError> {
let request_data = common_msgs::AddAppointmentRequest {
appointment: Some(appointment.clone().into()),
signature: signature.to_owned(),
signature: signature.into(),
};
match process_post_response(
post_request(
tower_net_addr,
Endpoint::AddAppointment,
&format!("{}/add_appointment", tower_net_addr),
&request_data,
proxy,
)
.await,
)
@ -133,7 +96,7 @@ pub async fn send_appointment(
{
ApiResponse::Response::<common_msgs::AddAppointmentResponse>(r) => {
let receipt = AppointmentReceipt::with_signature(
signature.to_owned(),
signature.into(),
r.start_block,
r.signature.clone(),
);
@ -154,72 +117,23 @@ pub async fn send_appointment(
}
}
/// A generic function to send a request to a tower.
async fn request<S: Serialize>(
tower_net_addr: &NetAddr,
endpoint: Endpoint,
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() {
reqwest::Client::builder()
.proxy(
reqwest::Proxy::http(proxy.get_socks_addr())
.map_err(|e| RequestError::ConnectionError(format!("{e}")))?,
/// Generic function to post different types of requests to the tower.
pub async fn post_request<S: Serialize>(endpoint: &str, data: S) -> Result<Response, RequestError> {
reqwest::Client::new()
.post(endpoint)
.json(&data)
.send()
.await
.map_err(|e| {
log::error!("{}", e);
if e.is_connect() | e.is_timeout() {
RequestError::ConnectionError(
"Cannot connect to the tower. Connection refused".into(),
)
.build()
.map_err(|e| RequestError::ConnectionError(format!("{e}")))?
} else {
reqwest::Client::new()
}
} else {
// If there is no proxy we only build the client as long as the address is not onion
if tower_net_addr.is_onion() {
return Err(RequestError::ConnectionError(
"Cannot connect to an onion address without a proxy".to_owned(),
));
}
reqwest::Client::new()
};
let mut request_builder = client.request(
method,
format!("{}{}", tower_net_addr.net_addr(), endpoint.path()),
);
if let Some(data) = data {
request_builder = request_builder.json(&data);
}
request_builder.send().await.map_err(|e| {
log::debug!("An error ocurred when sending data to the tower: {e}");
if e.is_connect() | e.is_timeout() {
RequestError::ConnectionError(
"Cannot connect to the tower. Connection refused".to_owned(),
)
} else {
RequestError::Unexpected("Unexpected error ocurred (see logs for more info)".to_owned())
}
})
}
pub async fn post_request<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
} else {
RequestError::Unexpected("Unexpected error ocurred (see logs for more info)".into())
}
})
}
/// Generic function to process the response of a given post request.
@ -229,7 +143,7 @@ pub async fn process_post_response<T: DeserializeOwned>(
// TODO: Check if this can be switched for a map. Not sure how to handle async with maps
match post_request {
Ok(r) => r.json().await.map_err(|e| {
RequestError::DeserializeError(format!("Unexpected response body. Error: {e}"))
RequestError::DeserializeError(format!("Unexpected response body. Error: {}", e))
}),
Err(e) => Err(e),
}
@ -238,12 +152,12 @@ 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;
use teos_common::test_utils::{
generate_random_appointment, get_random_appointment_receipt,
get_random_registration_receipt, get_random_user_id,
generate_random_appointment, get_random_appointment_receipt, get_random_user_id,
};
mod request_error {
@ -253,11 +167,11 @@ mod tests {
fn test_is_connection() {
let error_message = "error_msg";
for error in [
RequestError::ConnectionError(error_message.to_owned()),
RequestError::DeserializeError(error_message.to_owned()),
RequestError::Unexpected(error_message.to_owned()),
RequestError::ConnectionError(error_message.into()),
RequestError::DeserializeError(error_message.into()),
RequestError::Unexpected(error_message.into()),
] {
if error == RequestError::ConnectionError(error_message.to_owned()) {
if error == RequestError::ConnectionError(error_message.into()) {
assert!(error.is_connection())
} else {
assert!(!error.is_connection())
@ -266,72 +180,6 @@ mod tests {
}
}
#[tokio::test]
async fn test_register() {
let (tower_sk, tower_pk) = cryptography::get_random_keypair();
let mut registration_receipt = get_random_registration_receipt();
registration_receipt.sign(&tower_sk);
let mut server = mockito::Server::new_async().await;
let api_mock = server
.mock("POST", Endpoint::Register.path().as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(json!(registration_receipt).to_string())
.create_async()
.await;
let receipt = register(
TowerId(tower_pk),
registration_receipt.user_id(),
&NetAddr::new(server.url()),
&None,
)
.await
.unwrap();
api_mock.assert_async().await;
assert_eq!(receipt, registration_receipt);
}
#[tokio::test]
async fn test_register_connection_error() {
let error = register(
get_random_user_id(),
get_random_user_id(),
&NetAddr::new("http://server_addr".to_owned()),
&None,
)
.await
.unwrap_err();
assert!(matches!(error, RequestError::ConnectionError { .. }))
}
#[tokio::test]
async fn test_register_deserialize_error() {
let mut server = mockito::Server::new_async().await;
let api_mock = server
.mock("POST", Endpoint::Register.path().as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(json!([]).to_string())
.create_async()
.await;
let error = register(
get_random_user_id(),
get_random_user_id(),
&NetAddr::new(server.url()),
&None,
)
.await
.unwrap_err();
api_mock.assert_async().await;
assert!(matches!(error, RequestError::DeserializeError { .. }))
}
#[tokio::test]
async fn test_add_appointment() {
// `add_appointment` is basically a pass trough function for `send_appointment` with some logging and a parse of the outputs
@ -343,26 +191,24 @@ mod tests {
let add_appointment_response =
get_dummy_add_appointment_response(appointment.locator, &appointment_receipt);
let mut server = mockito::Server::new_async().await;
let api_mock = server
.mock("POST", Endpoint::AddAppointment.path().as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(json!(add_appointment_response).to_string())
.create_async()
.await;
let server = MockServer::start();
let api_mock = server.mock(|when, then| {
when.method(POST).path("/add_appointment");
then.status(200)
.header("content-type", "application/json")
.json_body(json!(add_appointment_response));
});
let (response, receipt) = add_appointment(
TowerId(tower_pk),
&NetAddr::new(server.url()),
&None,
&format!("http://{}", server.address()),
&appointment,
appointment_receipt.user_signature(),
)
.await
.unwrap();
api_mock.assert_async().await;
api_mock.assert();
assert_eq!(response, add_appointment_response.available_slots);
assert_eq!(receipt, appointment_receipt);
}
@ -376,26 +222,24 @@ mod tests {
let add_appointment_response =
get_dummy_add_appointment_response(appointment.locator, &appointment_receipt);
let mut server = mockito::Server::new_async().await;
let api_mock = server
.mock("POST", Endpoint::AddAppointment.path().as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(json!(add_appointment_response).to_string())
.create_async()
.await;
let server = MockServer::start();
let api_mock = server.mock(|when, then| {
when.method(POST).path("/add_appointment");
then.status(200)
.header("content-type", "application/json")
.json_body(json!(add_appointment_response));
});
let (response, receipt) = send_appointment(
TowerId(tower_pk),
&NetAddr::new(server.url()),
&None,
&format!("http://{}", server.address()),
&appointment,
appointment_receipt.user_signature(),
)
.await
.unwrap();
api_mock.assert_async().await;
api_mock.assert();
assert_eq!(response, add_appointment_response);
assert_eq!(receipt, appointment_receipt);
}
@ -409,27 +253,25 @@ mod tests {
let add_appointment_response =
get_dummy_add_appointment_response(appointment.locator, &appointment_receipt);
let mut server = mockito::Server::new_async().await;
let api_mock = server
.mock("POST", Endpoint::AddAppointment.path().as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(json!(add_appointment_response).to_string())
.create_async()
.await;
let server = MockServer::start();
let api_mock = server.mock(|when, then| {
when.method(POST).path("/add_appointment");
then.status(200)
.header("content-type", "application/json")
.json_body(json!(add_appointment_response));
});
let tower_id = get_random_user_id();
let error = send_appointment(
tower_id,
&NetAddr::new(server.url()),
&None,
&format!("http://{}", server.address()),
&appointment,
appointment_receipt.user_signature(),
)
.await
.unwrap_err();
api_mock.assert_async().await;
api_mock.assert();
if let AddAppointmentError::SignatureError(proof) = error {
assert_eq!(
MisbehaviorProof::new(
@ -448,8 +290,7 @@ mod tests {
async fn test_send_appointment_connection_error() {
let error = send_appointment(
get_random_user_id(),
&NetAddr::new("http://server_addr".to_owned()),
&None,
"http://server_addr",
&generate_random_appointment(None),
"user_sig",
)
@ -465,26 +306,24 @@ mod tests {
#[tokio::test]
async fn test_send_appointment_deserialize_error() {
let mut server = mockito::Server::new_async().await;
let api_mock = server
.mock("POST", Endpoint::AddAppointment.path().as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(json!([]).to_string())
.create_async()
.await;
let server = MockServer::start();
let api_mock = server.mock(|when, then| {
when.method(POST).path("/add_appointment");
then.status(200)
.header("content-type", "application/json")
.json_body(json!([]));
});
let error = send_appointment(
get_random_user_id(),
&NetAddr::new(server.url()),
&None,
&format!("http://{}", server.address()),
&generate_random_appointment(None),
"user_sig",
)
.await
.unwrap_err();
api_mock.assert_async().await;
api_mock.assert();
if let AddAppointmentError::RequestError(e) = error {
assert!(matches!(e, RequestError::DeserializeError { .. }))
} else {
@ -495,193 +334,118 @@ mod tests {
#[tokio::test]
async fn test_send_appointment_api_error() {
let api_error = ApiError {
error: "error_msg".to_owned(),
error: "error_msg".into(),
error_code: 1,
};
let mut server = mockito::Server::new_async().await;
let api_mock = server
.mock("POST", Endpoint::AddAppointment.path().as_str())
.with_status(400)
.with_header("content-type", "application/json")
.with_body(json!(api_error).to_string())
.create_async()
.await;
let server = MockServer::start();
let api_mock = server.mock(|when, then| {
when.method(POST).path("/add_appointment");
then.status(400)
.header("content-type", "application/json")
.json_body(json!(api_error));
});
let error = send_appointment(
get_random_user_id(),
&NetAddr::new(server.url()),
&None,
&format!("http://{}", server.address()),
&generate_random_appointment(None),
"user_sig",
)
.await
.unwrap_err();
api_mock.assert_async().await;
api_mock.assert();
assert!(matches!(error, AddAppointmentError::ApiError { .. }));
}
#[tokio::test]
async fn test_request() {
let mut server = mockito::Server::new_async().await;
async fn test_send_appointment_unexpected() {
// An example to trigger an unexpected error would be to try to send data to a wrongly formatted url.
// This can not happen in the codebase, since the url is tested on registration, but it can be used to
// test that error path. Generally speaking, that error path should be unreachable.
let wrong_tower_net_addr = "server_addr";
// 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 server = MockServer::start();
server.mock(|when, then| {
when.method(POST).path("/add_appointment");
then.status(200).header("content-type", "application/json");
});
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!("")),
let error = send_appointment(
get_random_user_id(),
wrong_tower_net_addr,
&generate_random_appointment(None),
"user_sig",
)
.await
.unwrap_err()
.is_connection());
.unwrap_err();
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());
if let AddAppointmentError::RequestError(e) = error {
assert!(matches!(e, RequestError::Unexpected { .. }))
} else {
panic!("Funny enough, Unexpected error was expected")
}
}
#[tokio::test]
async fn test_post_request() {
let 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 server = MockServer::start();
let api_mock = server.mock(|when, then| {
when.method(POST);
then.status(200).header("content-type", "application/json");
});
let response = post_request(
&NetAddr::new(server.url()),
Endpoint::Register,
json!(""),
&None,
)
.await;
let response = post_request(&format!("http://{}", server.address()), json!(""))
.await
.unwrap();
api_mock.assert_async().await;
assert!(matches!(response, Ok(Response { .. })));
api_mock.assert();
assert!(matches!(response, Response { .. }));
}
#[tokio::test]
async fn test_post_request_connection_error() {
assert!(post_request(
&NetAddr::new("http://unreachable_url".to_owned()),
Endpoint::Register,
json!(""),
&None,
)
.await
.unwrap_err()
.is_connection());
let unreachable_server_url = "http://server_addr";
assert!(matches!(
post_request(unreachable_server_url, json!(""))
.await
.unwrap_err(),
RequestError::ConnectionError { .. }
));
}
#[tokio::test]
async fn test_post_request_unexpected_error() {
let malformed_server_url = "server_addr";
assert!(matches!(
post_request(malformed_server_url, json!(""))
.await
.unwrap_err(),
RequestError::Unexpected { .. }
));
}
#[tokio::test]
async fn test_process_post_response_json_error() {
// `process_post_response` is a pass-trough function that maps json deserialization errors from `post_request`.
// So just testing that specific case should be enough.
let mut server = mockito::Server::new_async().await;
let api_mock = server
.mock("POST", Endpoint::GetAppointment.path().as_str())
.with_status(200)
.with_header("content-type", "application/json")
.create_async()
.await;
let server = MockServer::start();
let api_mock = server.mock(|when, then| {
when.method(POST);
then.status(200).header("content-type", "application/json");
});
// 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.url()),
Endpoint::GetAppointment,
json!(""),
&None,
)
.await,
post_request(&format!("http://{}", server.address()), json!("")).await,
)
.await
.unwrap_err();
api_mock.assert_async().await;
api_mock.assert();
assert!(matches!(error, RequestError::DeserializeError { .. }));
}
}

View file

@ -1,25 +1 @@
use cln_plugin::messages;
use serde::Deserialize;
pub mod http;
#[derive(Clone, Debug, Deserialize)]
pub struct ProxyInfo {
#[serde(flatten)]
/// The proxy data
inner: messages::ProxyInfo,
/// Whether to only send data though Tor or not
pub always_use: bool,
}
impl ProxyInfo {
pub fn new(proxy: messages::ProxyInfo, always_use: bool) -> Self {
Self {
inner: proxy,
always_use,
}
}
pub fn get_socks_addr(&self) -> String {
format!("socks5h://{}:{}", self.inner.address, self.inner.port)
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
use std::collections::{HashMap, HashSet};
use std::iter::FromIterator;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use tokio::fs;
use tokio::sync::mpsc::UnboundedSender;
@ -13,123 +13,63 @@ use teos_common::receipts::{AppointmentReceipt, RegistrationReceipt};
use teos_common::{TowerId, UserId};
use crate::dbm::DBM;
use crate::net::ProxyInfo;
use crate::retrier::RetrierStatus;
use crate::{MisbehaviorProof, SubscriptionError, TowerInfo, TowerStatus, TowerSummary};
#[derive(Eq, PartialEq)]
pub enum RevocationData {
Fresh(Locator),
Stale(HashSet<Locator>),
None,
}
impl RevocationData {
pub fn is_none(&self) -> bool {
*self == RevocationData::None
}
}
impl From<RevocationData> for HashSet<Locator> {
fn from(r: RevocationData) -> Self {
match r {
RevocationData::Fresh(l) => HashSet::from_iter(vec![l]),
RevocationData::Stale(hs) => hs,
RevocationData::None => HashSet::new(),
}
}
}
impl std::fmt::Debug for RevocationData {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
match self {
RevocationData::Fresh(l) => format!("Fresh: {l}"),
RevocationData::Stale(hs) => format!(
"Stale: {:?}",
hs.iter().map(|l| l.to_string()).collect::<Vec<_>>()
),
RevocationData::None => "None".to_owned(),
}
)
}
}
/// Represents the watchtower client that is being used as the CoreLN plugin state.
#[derive(Clone)]
pub struct WTClient {
/// A [DBM] instance.
pub dbm: DBM,
pub dbm: Arc<Mutex<DBM>>,
/// A collection of towers the client is registered to.
pub towers: HashMap<TowerId, TowerSummary>,
/// Queue of unreachable towers.
pub unreachable_towers: UnboundedSender<(TowerId, RevocationData)>,
// Map of existing retriers and its state.
pub retriers: HashMap<TowerId, RetrierStatus>,
/// Queue of unreachable towers
pub unreachable_towers: UnboundedSender<TowerId>,
/// The user secret key.
pub user_sk: SecretKey,
/// The user identifier.
pub user_id: UserId,
/// Optional proxy
pub proxy: Option<ProxyInfo>,
}
impl WTClient {
pub async fn new(
data_dir: PathBuf,
unreachable_towers: UnboundedSender<(TowerId, RevocationData)>,
) -> Self {
Self::with_proxy(data_dir, unreachable_towers, None).await
}
pub async fn with_proxy(
data_dir: PathBuf,
unreachable_towers: UnboundedSender<(TowerId, RevocationData)>,
proxy: Option<ProxyInfo>,
) -> Self {
pub async fn new(data_dir: PathBuf, unreachable_towers: UnboundedSender<TowerId>) -> Self {
// Create data dir if it does not exist
fs::create_dir_all(&data_dir).await.unwrap_or_else(|e| {
log::error!("Cannot create data dir: {e:?}");
log::error!("Cannot create data dir: {:?}", e);
std::process::exit(1);
});
let dbm = DBM::new(&data_dir.join("watchtowers_db.sql3")).unwrap();
let (user_sk, user_id) = if let Some(sk) = dbm.load_client_key() {
(
let (user_sk, user_id) = match dbm.load_client_key() {
Ok(sk) => (
sk,
UserId(PublicKey::from_secret_key(&Secp256k1::new(), &sk)),
)
} else {
log::info!("Watchtower client keys not found. Creating a fresh set");
let (sk, pk) = cryptography::get_random_keypair();
dbm.store_client_key(&sk).unwrap();
(sk, UserId(pk))
),
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))
}
};
let towers = dbm.load_towers();
for (tower_id, tower) in towers.iter() {
if tower.status.is_temporary_unreachable() {
unreachable_towers
.send((
*tower_id,
RevocationData::Stale(tower.pending_appointments.iter().cloned().collect()),
))
.unwrap();
if tower.status.is_unreachable() {
unreachable_towers.send(*tower_id).unwrap();
}
}
log::info!("Plugin watchtower client initialized. User id = {user_id}");
log::info!(
"Plugin watchtower client initialized. User id = {}",
user_id
);
WTClient {
towers,
unreachable_towers,
retriers: HashMap::new(),
dbm,
dbm: Arc::new(Mutex::new(dbm)),
user_sk,
user_id,
proxy,
}
}
@ -137,7 +77,7 @@ impl WTClient {
pub fn add_update_tower(
&mut self,
tower_id: TowerId,
tower_net_addr: &str,
tower_net_addr: String,
receipt: &RegistrationReceipt,
) -> Result<(), SubscriptionError> {
if let Some(tower) = self.towers.get(&tower_id) {
@ -146,72 +86,54 @@ impl WTClient {
if receipt.subscription_expiry() <= tower.subscription_expiry {
return Err(SubscriptionError::Expiry);
} else {
let tower_info = self.dbm.load_tower_record(tower_id).unwrap();
if receipt.available_slots() <= tower_info.available_slots {
let previous_receipt = self
.dbm
.lock()
.unwrap()
.load_registration_receipt(tower_id, self.user_id)
.unwrap();
if receipt.available_slots() <= previous_receipt.available_slots() {
return Err(SubscriptionError::Slots);
}
}
}
self.dbm
.store_tower_record(tower_id, tower_net_addr, receipt)
.lock()
.unwrap()
.store_tower_record(tower_id, &tower_net_addr, receipt)
.unwrap();
if let Some(summary) = self.towers.get_mut(&tower_id) {
summary.udpate(
tower_net_addr.to_owned(),
self.towers.insert(
tower_id,
TowerSummary::new(
tower_net_addr,
receipt.available_slots(),
receipt.subscription_start(),
receipt.subscription_expiry(),
);
} else {
self.towers.insert(
tower_id,
TowerSummary::new(
tower_net_addr.to_owned(),
receipt.available_slots(),
receipt.subscription_start(),
receipt.subscription_expiry(),
),
);
};
),
);
Ok(())
}
/// Gets the latest registration receipt of a given tower.
pub fn get_registration_receipt(&self, tower_id: TowerId) -> Option<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) -> Option<TowerInfo> {
self.dbm.load_tower_record(tower_id)
}
/// Gets the given tower status (identified by tower_id), if found.
pub fn get_tower_status(&self, tower_id: &TowerId) -> Option<TowerStatus> {
Some(self.towers.get(tower_id)?.status)
pub fn load_tower_info(&self, tower_id: TowerId) -> Result<TowerInfo, DBError> {
self.dbm.lock().unwrap().load_tower_record(tower_id)
}
/// Sets the tower status to any of the `TowerStatus` variants.
pub fn set_tower_status(&mut self, tower_id: TowerId, status: TowerStatus) {
if let Some(tower) = self.towers.get_mut(&tower_id) {
if tower.status != status {
tower.status = status
} else {
log::debug!("{tower_id} status is already {status}")
}
tower.status = status
} else {
log::error!("Cannot change tower status to {status}. Unknown tower_id: {tower_id}");
log::error!(
"Cannot change tower status to {}. Unknown tower_id: {}",
status,
tower_id
);
}
}
/// Gets the given tower status (identified by tower_id), if found.
pub fn get_retrier_status(&self, tower_id: &TowerId) -> Option<&RetrierStatus> {
self.retriers.get(tower_id)
}
/// Adds an appointment receipt to the tower record.
pub fn add_appointment_receipt(
&mut self,
@ -225,32 +147,33 @@ impl WTClient {
tower.available_slots = available_slots;
self.dbm
.lock()
.unwrap()
.store_appointment_receipt(tower_id, locator, available_slots, receipt)
.unwrap();
} else {
log::error!("Cannot add appointment receipt to tower. Unknown tower_id: {tower_id}");
log::error!(
"Cannot add appointment receipt to tower. Unknown tower_id: {}",
tower_id
);
}
}
/// Gets an appointment receipt from the database (if found).
pub fn get_appointment_receipt(
&self,
tower_id: TowerId,
locator: Locator,
) -> Option<AppointmentReceipt> {
self.dbm.load_appointment_receipt(tower_id, locator)
}
/// Adds a pending appointment to the tower record.
pub fn add_pending_appointment(&mut self, tower_id: TowerId, appointment: &Appointment) {
if let Some(tower) = self.towers.get_mut(&tower_id) {
tower.pending_appointments.insert(appointment.locator);
self.dbm
.lock()
.unwrap()
.store_pending_appointment(tower_id, appointment)
.unwrap();
} else {
log::error!("Cannot add pending appointment to tower. Unknown tower_id: {tower_id}");
log::error!(
"Cannot add pending appointment to tower. Unknown tower_id: {}",
tower_id
);
}
}
@ -260,10 +183,15 @@ impl WTClient {
tower.pending_appointments.remove(&locator);
self.dbm
.lock()
.unwrap()
.delete_pending_appointment(tower_id, locator)
.unwrap();
} else {
log::error!("Cannot remove pending appointment to tower. Unknown tower_id: {tower_id}");
log::error!(
"Cannot remove pending appointment to tower. Unknown tower_id: {}",
tower_id
);
}
}
@ -273,32 +201,29 @@ impl WTClient {
tower.invalid_appointments.insert(appointment.locator);
self.dbm
.lock()
.unwrap()
.store_invalid_appointment(tower_id, appointment)
.unwrap();
} else {
log::error!("Cannot add invalid appointment to tower. Unknown tower_id: {tower_id}");
log::error!(
"Cannot add invalid appointment to tower. Unknown tower_id: {}",
tower_id
);
}
}
/// Flags a given tower as misbehaving, storing the misbehaving proof in the database.
pub fn flag_misbehaving_tower(&mut self, tower_id: TowerId, proof: MisbehaviorProof) {
if let Some(tower) = self.towers.get_mut(&tower_id) {
self.dbm.store_misbehaving_proof(tower_id, &proof).unwrap();
self.dbm
.lock()
.unwrap()
.store_misbehaving_proof(tower_id, &proof)
.unwrap();
tower.status = TowerStatus::Misbehaving;
} else {
log::error!("Cannot flag tower. Unknown tower_id: {tower_id}");
}
}
/// Removes a tower from the client (both memory and database).
///
/// Any data associated to the tower will be deleted (i.e. links to appointments)
pub fn remove_tower(&mut self, tower_id: TowerId) -> Result<(), DBError> {
if self.towers.contains_key(&tower_id) {
self.towers.remove(&tower_id);
self.dbm.remove_tower_record(tower_id)
} else {
Err(DBError::NotFound)
log::error!("Cannot flag tower. Unknown tower_id: {}", tower_id);
}
}
}
@ -324,17 +249,16 @@ mod tests {
// Adding a new tower will add a summary to towers and the full data to the
let mut receipt = get_random_registration_receipt();
let (tower_sk, tower_pk) = cryptography::get_random_keypair();
let tower_id = TowerId(tower_pk);
let tower_id = get_random_user_id();
let tower_info = TowerInfo::empty(
"talaia.watch".to_owned(),
"talaia.watch".into(),
receipt.available_slots(),
receipt.subscription_start(),
receipt.subscription_expiry(),
);
wt_client
.add_update_tower(tower_id, &tower_info.net_addr, &receipt)
.add_update_tower(tower_id, tower_info.net_addr.clone(), &receipt)
.unwrap();
assert_eq!(
wt_client.towers.get(&tower_id),
@ -346,13 +270,13 @@ mod tests {
receipt = get_registration_receipt_from_previous(&receipt);
let updated_tower_info = TowerInfo::empty(
"talaia.watch".to_owned(),
"talaia.watch".into(),
receipt.available_slots(),
receipt.subscription_start(),
receipt.subscription_expiry(),
);
wt_client
.add_update_tower(tower_id, &updated_tower_info.net_addr, &receipt)
.add_update_tower(tower_id, updated_tower_info.net_addr.clone(), &receipt)
.unwrap();
assert_eq!(
@ -365,72 +289,35 @@ mod tests {
);
// If we try to update without increasing both the end_time and the slots, this will fail
let mut receipt_same_slots = RegistrationReceipt::new(
let receipt_same_slots = RegistrationReceipt::new(
receipt.user_id(),
receipt.available_slots(),
receipt.subscription_start(),
receipt.subscription_expiry() + 1,
);
receipt_same_slots.sign(&tower_sk);
let mut receipt_same_expiry = RegistrationReceipt::new(
let receipt_same_expiry = RegistrationReceipt::new(
receipt.user_id(),
receipt.available_slots() + 1,
receipt.subscription_start(),
receipt.subscription_expiry(),
);
receipt_same_expiry.sign(&tower_sk);
assert!(matches!(
wt_client.add_update_tower(tower_id, &updated_tower_info.net_addr, &receipt),
wt_client.add_update_tower(tower_id, updated_tower_info.net_addr.clone(), &receipt),
Err(SubscriptionError::Expiry)
));
assert!(matches!(
wt_client.add_update_tower(tower_id, &updated_tower_info.net_addr, &receipt_same_slots),
Err(SubscriptionError::Slots)
));
assert!(matches!(
wt_client.add_update_tower(
tower_id,
&updated_tower_info.net_addr,
&receipt_same_expiry
updated_tower_info.net_addr.clone(),
&receipt_same_slots
),
Err(SubscriptionError::Slots)
));
assert!(matches!(
wt_client.add_update_tower(tower_id, updated_tower_info.net_addr, &receipt_same_expiry),
Err(SubscriptionError::Expiry)
));
// Decrease the slots count (simulate exhaustion) and update with more than the current count it should work
let locator = generate_random_appointment(None).locator;
wt_client.add_appointment_receipt(
tower_id,
locator,
0,
&get_random_appointment_receipt(tower_sk),
);
wt_client
.add_update_tower(tower_id, &updated_tower_info.net_addr, &receipt_same_slots)
.unwrap();
}
#[tokio::test]
async fn test_get_tower_status() {
let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap();
let mut wt_client =
WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await;
// If the tower is unknown, get_tower_status returns None
let tower_id = get_random_user_id();
assert!(wt_client.get_tower_status(&tower_id).is_none());
// Add a tower
let receipt = get_random_registration_receipt();
wt_client
.add_update_tower(tower_id, "talaia.watch", &receipt)
.unwrap();
// If the tower is known, get_tower_status matches getting the same data from the towers collection
assert_eq!(
wt_client.towers.get(&tower_id).unwrap().status,
wt_client.get_tower_status(&tower_id).unwrap()
)
}
#[tokio::test]
@ -448,7 +335,7 @@ mod tests {
let receipt = get_random_registration_receipt();
let tower_id = get_random_user_id();
wt_client
.add_update_tower(tower_id, "talaia.watch", &receipt)
.add_update_tower(tower_id, "talaia.watch".into(), &receipt)
.unwrap();
for status in [
@ -459,7 +346,7 @@ mod tests {
TowerStatus::Misbehaving,
] {
wt_client.set_tower_status(tower_id, status);
assert_eq!(status, wt_client.get_tower_status(&tower_id).unwrap());
assert_eq!(status, wt_client.towers.get(&tower_id).unwrap().status);
}
}
@ -471,6 +358,7 @@ mod tests {
let (tower_sk, tower_pk) = cryptography::get_random_keypair();
let tower_id = TowerId(tower_pk);
let tower_net_addr = "talaia.watch";
let locator = generate_random_appointment(None).locator;
let registration_receipt = get_random_registration_receipt();
@ -487,7 +375,7 @@ mod tests {
// Add the tower to the state and try again
let tower_info = TowerInfo::new(
"talaia.watch".to_owned(),
tower_net_addr.into(),
registration_receipt.available_slots(),
registration_receipt.subscription_start(),
registration_receipt.subscription_expiry(),
@ -496,7 +384,7 @@ mod tests {
Vec::new(),
);
wt_client
.add_update_tower(tower_id, &tower_info.net_addr, &registration_receipt)
.add_update_tower(tower_id, tower_net_addr.into(), &registration_receipt)
.unwrap();
wt_client.add_appointment_receipt(
tower_id,
@ -520,6 +408,7 @@ mod tests {
WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await;
let tower_id = get_random_user_id();
let tower_net_addr = "talaia.watch";
let registration_receipt = get_random_registration_receipt();
let appointment = generate_random_appointment(None);
@ -530,7 +419,7 @@ mod tests {
// Add the tower to the state and try again
let tower_info = TowerInfo::new(
"talaia.watch".to_owned(),
tower_net_addr.into(),
registration_receipt.available_slots(),
registration_receipt.subscription_start(),
registration_receipt.subscription_expiry(),
@ -540,7 +429,7 @@ mod tests {
);
wt_client
.add_update_tower(tower_id, &tower_info.net_addr, &registration_receipt)
.add_update_tower(tower_id, tower_net_addr.into(), &registration_receipt)
.unwrap();
wt_client.add_pending_appointment(tower_id, &appointment);
@ -563,6 +452,7 @@ mod tests {
WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await;
let tower_id = get_random_user_id();
let tower_net_addr = "talaia.watch";
let registration_receipt = get_random_registration_receipt();
let appointment = generate_random_appointment(None);
@ -572,7 +462,7 @@ mod tests {
// Add the tower to the state and try again
wt_client
.add_update_tower(tower_id, "talaia.watch", &registration_receipt)
.add_update_tower(tower_id, tower_net_addr.into(), &registration_receipt)
.unwrap();
wt_client.add_pending_appointment(tower_id, &appointment);
@ -584,7 +474,13 @@ mod tests {
.pending_appointments
.contains(&appointment.locator));
// This bit is tested exhaustively in the DBM.
assert!(!wt_client.dbm.appointment_exists(appointment.locator));
assert!(!wt_client
.dbm
.lock()
.unwrap()
.appointment_exists(appointment.locator));
fs::remove_dir_all(tmp_path).await.unwrap();
}
#[tokio::test]
@ -594,6 +490,7 @@ mod tests {
WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await;
let tower_id = get_random_user_id();
let tower_net_addr = "talaia.watch";
let registration_receipt = get_random_registration_receipt();
let appointment = generate_random_appointment(None);
@ -604,7 +501,7 @@ mod tests {
// Add the tower to the state and try again
let tower_info = TowerInfo::new(
"talaia.watch".to_owned(),
tower_net_addr.into(),
registration_receipt.available_slots(),
registration_receipt.subscription_start(),
registration_receipt.subscription_expiry(),
@ -614,7 +511,7 @@ mod tests {
);
wt_client
.add_update_tower(tower_id, &tower_info.net_addr, &registration_receipt)
.add_update_tower(tower_id, tower_net_addr.into(), &registration_receipt)
.unwrap();
wt_client.add_invalid_appointment(tower_id, &appointment);
@ -633,12 +530,13 @@ mod tests {
WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await;
let tower_id = get_random_user_id();
let tower_net_addr = "talaia.watch";
let registration_receipt = get_random_registration_receipt();
let appointment = generate_random_appointment(None);
wt_client
.add_update_tower(tower_id, "talaia.watch", &registration_receipt)
.add_update_tower(tower_id, tower_net_addr.into(), &registration_receipt)
.unwrap();
wt_client.add_pending_appointment(tower_id, &appointment);
@ -660,13 +558,23 @@ mod tests {
.contains(&appointment.locator));
assert!(!wt_client
.dbm
.lock()
.unwrap()
.load_appointment_locators(tower_id, crate::AppointmentStatus::Pending)
.contains(&appointment.locator));
assert!(wt_client
.dbm
.lock()
.unwrap()
.load_appointment_locators(tower_id, crate::AppointmentStatus::Invalid)
.contains(&appointment.locator));
assert!(wt_client.dbm.appointment_exists(appointment.locator));
assert!(wt_client
.dbm
.lock()
.unwrap()
.appointment_exists(appointment.locator));
fs::remove_dir_all(tmp_path).await.unwrap();
}
#[tokio::test]
@ -684,10 +592,14 @@ mod tests {
let appointment = generate_random_appointment(None);
wt_client
.add_update_tower(tower_id, tower_net_addr, &registration_receipt)
.add_update_tower(tower_id, tower_net_addr.into(), &registration_receipt)
.unwrap();
wt_client
.add_update_tower(another_tower_id, tower_net_addr, &registration_receipt)
.add_update_tower(
another_tower_id,
tower_net_addr.into(),
&registration_receipt,
)
.unwrap();
wt_client.add_pending_appointment(tower_id, &appointment);
wt_client.add_pending_appointment(another_tower_id, &appointment);
@ -711,10 +623,14 @@ mod tests {
.contains(&appointment.locator));
assert!(!wt_client
.dbm
.lock()
.unwrap()
.load_appointment_locators(tower_id, crate::AppointmentStatus::Pending)
.contains(&appointment.locator));
assert!(wt_client
.dbm
.lock()
.unwrap()
.load_appointment_locators(tower_id, crate::AppointmentStatus::Invalid)
.contains(&appointment.locator));
@ -733,15 +649,25 @@ mod tests {
.contains(&appointment.locator));
assert!(wt_client
.dbm
.lock()
.unwrap()
.load_appointment_locators(another_tower_id, crate::AppointmentStatus::Pending)
.contains(&appointment.locator));
assert!(!wt_client
.dbm
.lock()
.unwrap()
.load_appointment_locators(another_tower_id, crate::AppointmentStatus::Invalid)
.contains(&appointment.locator));
// GENERAL
assert!(wt_client.dbm.appointment_exists(appointment.locator));
assert!(wt_client
.dbm
.lock()
.unwrap()
.appointment_exists(appointment.locator));
fs::remove_dir_all(tmp_path).await.unwrap();
}
#[tokio::test]
@ -752,6 +678,7 @@ mod tests {
let (tower_sk, tower_pk) = cryptography::get_random_keypair();
let tower_id = TowerId(tower_pk);
let tower_net_addr = "talaia.watch";
// If we call this on an unknown tower it will simply do nothing
let appointment = generate_random_appointment(None);
@ -763,139 +690,19 @@ mod tests {
// // Add the tower to the state and try again
let registration_receipt = get_random_registration_receipt();
wt_client
.add_update_tower(tower_id, "talaia.watch", &registration_receipt)
.add_update_tower(tower_id, tower_net_addr.into(), &registration_receipt)
.unwrap();
wt_client.flag_misbehaving_tower(tower_id, proof.clone());
// Check data in memory
let tower_summary = wt_client.towers.get(&tower_id);
assert!(tower_summary.is_some());
assert!(tower_summary.unwrap().status.is_misbehaving());
assert_eq!(tower_summary.unwrap().status, TowerStatus::Misbehaving);
// Check data in DB
let loaded_info = wt_client.load_tower_info(tower_id).unwrap();
assert!(loaded_info.status.is_misbehaving());
assert_eq!(loaded_info.status, TowerStatus::Misbehaving);
assert_eq!(loaded_info.misbehaving_proof, Some(proof));
assert!(loaded_info.appointments.contains_key(&appointment.locator));
}
#[tokio::test]
async fn test_remove_tower() {
let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap();
let mut wt_client =
WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await;
let receipt = get_random_registration_receipt();
let (tower_sk, tower_pk) = cryptography::get_random_keypair();
let tower_id = TowerId(tower_pk);
let tower_info = TowerInfo::empty(
"talaia.watch".to_owned(),
receipt.available_slots(),
receipt.subscription_start(),
receipt.subscription_expiry(),
);
// Add the tower and check it is there
wt_client
.add_update_tower(tower_id, &tower_info.net_addr, &receipt)
.unwrap();
assert_eq!(
wt_client.towers.get(&tower_id),
Some(&TowerSummary::from(tower_info.clone()))
);
assert_eq!(wt_client.load_tower_info(tower_id).unwrap(), tower_info);
// Remove the tower and check it is not there anymore
wt_client.remove_tower(tower_id).unwrap();
assert!(wt_client.load_tower_info(tower_id).is_none());
assert!(!wt_client.towers.contains_key(&tower_id));
// Try again but this time with an associated appointment to check that it also gets removed
wt_client
.add_update_tower(tower_id, &tower_info.net_addr, &receipt)
.unwrap();
let locator = generate_random_appointment(None).locator;
let registration_receipt = get_random_registration_receipt();
let appointment_receipt = get_random_appointment_receipt(tower_sk);
// If we call this on an unknown tower it will simply do nothing
wt_client.add_appointment_receipt(
tower_id,
locator,
registration_receipt.available_slots(),
&appointment_receipt,
);
assert!(wt_client.dbm.appointment_receipt_exists(locator, tower_id));
// Remove and check both the tower and the appointment
wt_client.remove_tower(tower_id).unwrap();
assert!(wt_client.load_tower_info(tower_id).is_none());
assert!(!wt_client.towers.contains_key(&tower_id));
assert!(!wt_client.dbm.appointment_receipt_exists(locator, tower_id));
}
#[tokio::test]
async fn test_remove_tower_shared_appointment() {
// Lets test removing a tower that has associated data shared with another tower.
// For instance, having an appointment that was sent to two towers, and then deleting one of them
// should only remove the link between the tower and the appointment, but not delete the data.
let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap();
let mut wt_client =
WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await;
let receipt = get_random_registration_receipt();
let (tower1_sk, tower1_pk) = cryptography::get_random_keypair();
let tower1_id = TowerId(tower1_pk);
let (tower2_sk, tower2_pk) = cryptography::get_random_keypair();
let tower2_id = TowerId(tower2_pk);
wt_client
.add_update_tower(tower1_id, "talaia.watch", &receipt)
.unwrap();
wt_client
.add_update_tower(tower2_id, "talaia.watch", &receipt)
.unwrap();
let locator = generate_random_appointment(None).locator;
let registration_receipt = get_random_registration_receipt();
let appointment_receipt_1 = get_random_appointment_receipt(tower1_sk);
let appointment_receipt_2 = get_random_appointment_receipt(tower2_sk);
wt_client.add_appointment_receipt(
tower1_id,
locator,
registration_receipt.available_slots(),
&appointment_receipt_1,
);
wt_client.add_appointment_receipt(
tower2_id,
locator,
registration_receipt.available_slots(),
&appointment_receipt_2,
);
// Check that the data exists in both towers
assert!(wt_client.dbm.appointment_receipt_exists(locator, tower1_id));
assert!(wt_client.dbm.appointment_receipt_exists(locator, tower2_id));
// Remove tower1 and check that the appointment receipt can still be found for tower2
wt_client.remove_tower(tower1_id).unwrap();
assert!(wt_client.load_tower_info(tower1_id).is_none());
assert!(!wt_client.dbm.appointment_receipt_exists(locator, tower1_id));
assert!(wt_client.dbm.appointment_receipt_exists(locator, tower2_id));
}
#[tokio::test]
async fn test_remove_inexistent_tower() {
let tmp_path = TempDir::new(&format!("watchtower_{}", get_random_user_id())).unwrap();
let mut wt_client =
WTClient::new(tmp_path.path().to_path_buf(), unbounded_channel().0).await;
assert!(matches!(
wt_client.remove_tower(get_random_user_id()),
Err(DBError::NotFound)
));
}
}

View file

@ -1,10 +1,12 @@
from pathlib import Path
import os
import json
import pytest
import logging
import subprocess
from pyln.testing.fixtures import * # noqa: F401,F403
from pyln.testing.utils import BITCOIND_CONFIG, TailableProc
from pyln.testing.utils import DEVELOPER, BITCOIND_CONFIG, TIMEOUT, TailableProc
WT_PLUGIN = Path("~/.cargo/bin/watchtower-client").expanduser()
TEOSD_CONFIG = {
"btc_network": "regtest",
"polling_delta": 0,
@ -26,11 +28,8 @@ class TeosCLI:
def _call(self, method_name, *args):
try:
r = subprocess.run(
["teos-cli", f"--datadir={self.datadir}/teos", method_name, *args],
capture_output=True,
text=True,
)
r = subprocess.run(["teos-cli", f"--datadir={self.datadir}/teos", method_name, *args], capture_output=True,
text=True)
if r.returncode != 0:
result = ValueError(f"Unknown method {method_name}")
else:
@ -58,9 +57,9 @@ class TeosCLI:
class TeosD(TailableProc):
def __init__(self, bitcoind_rpcport, directory="/tmp/watchtower-test"):
TailableProc.__init__(self, directory, verbose=True)
self.teos_dir = os.path.join(directory, "teos")
self.prefix = "teosd"
TailableProc.__init__(self, self.teos_dir)
self.cli = TeosCLI(directory)
if not os.path.exists(self.teos_dir):
@ -81,14 +80,11 @@ class TeosD(TailableProc):
if overwrite_key:
self.cmd_line.append("--overwritekey")
TailableProc.start(self)
self.wait_for_log("Tower ready")
self.wait_for_log("Tower ready", timeout=TIMEOUT)
logging.info("TeosD started")
def stop(self):
self.cli.stop()
self.wait_for_log("Shutting down tower")
return TailableProc.stop(self)
@ -116,7 +112,14 @@ def pytest_runtest_makereport(item, call):
setattr(item, "rep_" + rep.when, rep)
@pytest.fixture(scope="function", autouse=True)
def log_name(request):
# Here logging is used, you can use whatever you want to use for logs
logging.info("Starting '{}'".format(request.node.name))
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")

View file

@ -1,19 +1,17 @@
[tool.poetry]
name = "tests"
version = "0.1.2"
version = "0.1.0"
description = "watchtower-plugin tests"
authors = ["Sergi Delgado Segura <sergi.delgado.s@gmail.com>"]
license = "MIT"
[tool.poetry.dependencies]
python = "^3.9"
black = "^22.6.0"
[tool.poetry.dev-dependencies]
pytest = "^7.1.2"
pytest-timeout = "^2.1.0"
pyln-testing = "^24.2.1"
pyln-client = "^24.2.1"
pyln-testing = "^0.10.2"
[build-system]

View file

@ -1,5 +1,5 @@
import time
import pytest
from conftest import WT_PLUGIN
def change_endianness(x):
@ -15,6 +15,7 @@ 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.
@ -25,16 +26,10 @@ def test_watchtower(node_factory, bitcoind, teosd):
commitment transaction.
"""
l1, l2 = node_factory.line_graph(
2,
opts=[
{"broken_log": r"Could not find resolution for output [0-9]?: did \*we\* cheat\?"},
{"plugin": WT_PLUGIN},
],
)
l1, l2 = node_factory.line_graph(2, opts=[{"allow_broken_log": True}, {"plugin": "watchtower-client"}])
# We need to register l2 with the tower
tower_id = teosd.cli.gettowerinfo()["tower_id"]
tower_id = teosd.cli.get_tower_info()["tower_id"]
l2.rpc.registertower(tower_id)
# Force a new commitment
@ -52,32 +47,30 @@ 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 _: {"result": None, "error": None, "id": "pytest"})
l2.daemon.rpcproxy.mock_rpc("sendrawtransaction", lambda: None)
l2.start()
# The tower will react once the dispute gets confirmed. For now it is still watching for it
assert l2.rpc.getappointment(tower_id, locator)["status"] == "being_watched"
# Confirm the dispute so the tower can react with the penalty
bitcoind.generate_block()
l1.daemon.wait_for_log("State changed from FUNDING_SPEND_SEEN to ONCHAIN")
bitcoind.generate_block(1)
time.sleep(1)
penalty_txid = bitcoind.rpc.getrawmempool()[0]
# The channel still exists between the two peers, but it's on chain
assert l1.rpc.listpeerchannels()["channels"][0]["state"] == "ONCHAIN"
assert l1.rpc.listpeers()["peers"][0]["channels"][0]["state"] == "ONCHAIN"
assert l2.rpc.getappointment(tower_id, locator)["status"] == "dispute_responded"
# Generate blocks until the penalty gets irrevocably resolved
for i in range(101):
bitcoind.generate_block()
for i in range(100):
bitcoind.generate_block(1)
time.sleep(0.1)
if i < 100:
assert l2.rpc.getappointment(tower_id, locator)["status"] == "dispute_responded"
else:
# Once the channel gets irrevocably resolved the tower will forget about it
assert l2.rpc.getappointment(tower_id, locator) == {
"error": "Appointment not found",
"error_code": 36,
}
assert l2.rpc.getappointment(tower_id, locator)["status"] == "not_found"
# Make sure the penalty outputs are in l2's wallet
fund_txids = [o["txid"] for o in l2.rpc.listfunds()["outputs"]]
@ -93,14 +86,15 @@ def test_unreachable_watchtower(node_factory, bitcoind, teosd):
opts=[
{},
{
"plugin": WT_PLUGIN,
"plugin": "watchtower-client",
"allow_broken_log": True,
"dev-watchtower-max-retry-interval": max_interval_time,
},
],
)
# We need to register l2 with the tower
tower_id = teosd.cli.gettowerinfo()["tower_id"]
tower_id = teosd.cli.get_tower_info()["tower_id"]
l2.rpc.registertower(tower_id)
# Stop the tower
@ -111,30 +105,23 @@ def test_unreachable_watchtower(node_factory, bitcoind, teosd):
assert l2.rpc.gettowerinfo(tower_id)["status"] == "temporary_unreachable"
assert l2.rpc.gettowerinfo(tower_id)["pending_appointments"]
# Start the tower and check the automatic backoff works
# Start the tower and check the automatic backoff works (wait while are pending appointments)
teosd.start()
l2.daemon.wait_for_log(f"Retry strategy succeeded for {tower_id}")
while l2.rpc.gettowerinfo(tower_id)["pending_appointments"]:
time.sleep(1)
assert l2.rpc.gettowerinfo(tower_id)["status"] == "reachable"
assert not l2.rpc.gettowerinfo(tower_id)["pending_appointments"]
def test_auto_retry_watchtower(node_factory, bitcoind, teosd):
def test_retry_watchtower(node_factory, bitcoind, teosd):
# The plugin is set to give up on retrying straight-away so we can test this fast.
l1, l2 = node_factory.line_graph(
2,
opts=[
{},
{
"plugin": WT_PLUGIN,
"broken_log": r"plugin-watchtower-client: Data was send to an idle retrier. This should have never happened. Please report!.*",
"watchtower-max-retry-time": 1,
"watchtower-auto-retry-delay": 1,
},
],
2, opts=[{}, {"plugin": "watchtower-client", "allow_broken_log": True, "watchtower-max-retry-time": 0}]
)
# We need to register l2 with the tower
tower_id = teosd.cli.gettowerinfo()["tower_id"]
tower_id = teosd.cli.get_tower_info()["tower_id"]
l2.rpc.registertower(tower_id)
# Stop the tower
@ -142,63 +129,23 @@ def test_auto_retry_watchtower(node_factory, bitcoind, teosd):
# Make a new payment with an unreachable tower
l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"])
# Wait until the tower has been flagged as unreachable
l2.daemon.wait_for_log("Starting to idle")
assert l2.rpc.gettowerinfo(tower_id)["status"] == "unreachable"
assert l2.rpc.gettowerinfo(tower_id)["pending_appointments"]
# Start the tower and retry it
teosd.start()
l2.daemon.wait_for_log(f"Finished idling. Flagging {tower_id} for retry")
l2.daemon.wait_for_log(f"Retry strategy succeeded for {tower_id}")
assert l2.rpc.gettowerinfo(tower_id)["status"] == "reachable"
def test_manually_retry_watchtower(node_factory, bitcoind, teosd):
# The plugin is set to give up on retrying straight-away so we can test this fast.
l1, l2 = node_factory.line_graph(
2,
opts=[
{},
{
"plugin": WT_PLUGIN,
"watchtower-max-retry-time": 0,
},
],
)
# We need to register l2 with the tower
tower_id = teosd.cli.gettowerinfo()["tower_id"]
l2.rpc.registertower(tower_id)
# Stop the tower
teosd.stop()
# Make a new payment with an unreachable tower
l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"])
# Wait until the tower has been flagged as unreachable
l2.daemon.wait_for_log("Starting to idle")
assert l2.rpc.gettowerinfo(tower_id)["status"] == "unreachable"
assert l2.rpc.gettowerinfo(tower_id)["pending_appointments"]
# Start the tower and retry it
teosd.start()
# Manual retry
l2.rpc.retrytower(tower_id)
l2.daemon.wait_for_log(f"Manually finished idling. Flagging {tower_id} for retry")
l2.daemon.wait_for_log(f"Retry strategy succeeded for {tower_id}")
time.sleep(2)
assert l2.rpc.gettowerinfo(tower_id)["status"] == "reachable"
assert not l2.rpc.gettowerinfo(tower_id)["pending_appointments"]
def test_misbehaving_watchtower(node_factory, bitcoind, teosd, directory):
l1, l2 = node_factory.line_graph(2, opts=[{}, {"plugin": WT_PLUGIN}])
l1, l2 = node_factory.line_graph(2, opts=[{}, {"plugin": "watchtower-client", "allow_broken_log": True}])
# We need to register l2 with the tower
tower_id = teosd.cli.gettowerinfo()["tower_id"]
tower_id = teosd.cli.get_tower_info()["tower_id"]
l2.rpc.registertower(tower_id)
# Restart overwriting the tower private key
@ -209,35 +156,3 @@ def test_misbehaving_watchtower(node_factory, bitcoind, teosd, directory):
l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"])
assert l2.rpc.gettowerinfo(tower_id)["status"] == "misbehaving"
assert l2.rpc.gettowerinfo(tower_id)["misbehaving_proof"]
def test_get_appointment(node_factory, bitcoind, teosd, directory):
l1, l2 = node_factory.line_graph(2, opts=[{}, {"plugin": WT_PLUGIN}])
# We need to register l2 with the tower
tower_id = teosd.cli.gettowerinfo()["tower_id"]
l2.rpc.registertower(tower_id)
# Force a new commitment
l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"])
tx = l1.rpc.dev_sign_last_tx(l2.info["id"])["tx"]
# Now make sure it is out of date
l1.rpc.pay(l2.rpc.invoice(25000000, "lbl2", "desc2")["bolt11"])
# Now l1 cheats
dispute_txid = bitcoind.rpc.sendrawtransaction(tx)
locator = change_endianness(dispute_txid[32:])
# Check the appointment before mining a block
appointment = l2.rpc.getappointment(tower_id, locator)["appointment"]
assert "locator" in appointment and "encrypted_blob" in appointment and "to_self_delay" in appointment
# And after. Now this should be a tracker
bitcoind.generate_block()
teosd.wait_for_log("New tracker added")
tracker = l2.rpc.getappointment(tower_id, locator)["appointment"]
assert "dispute_txid" in tracker and "penalty_txid" in tracker and "penalty_rawtx" in tracker
# Manually stop l2, otherwise the tower may be stopped before the tower client and we may get some BROKEN logs.
l2.stop()