├── .github └── workflows │ ├── build.yaml │ └── cln-plugin.yaml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── DEPENDENCIES.md ├── INSTALL.md ├── LICENSE ├── README.md ├── coffee.yml ├── contrib └── init │ ├── README.md │ └── teosd.service ├── docker ├── Dockerfile ├── README.md └── entrypoint.sh ├── rust-toolchain.toml ├── teos-common ├── Cargo.toml ├── build.rs ├── proto │ └── common │ │ └── teos │ │ └── v2 │ │ ├── appointment.proto │ │ └── user.proto └── src │ ├── appointment.rs │ ├── constants.rs │ ├── cryptography.rs │ ├── dbm.rs │ ├── errors.rs │ ├── lib.rs │ ├── net │ ├── http.rs │ └── mod.rs │ ├── receipts.rs │ ├── ser.rs │ └── test_utils.rs ├── teos ├── Cargo.toml ├── build.rs ├── proto │ └── teos │ │ └── v2 │ │ ├── appointment.proto │ │ ├── tower_services.proto │ │ └── user.proto └── src │ ├── api │ ├── http.rs │ ├── internal.rs │ ├── mod.rs │ ├── serde.rs │ └── tor.rs │ ├── bitcoin_cli.rs │ ├── carrier.rs │ ├── chain_monitor.rs │ ├── cli.rs │ ├── cli_config.rs │ ├── conf_template.toml │ ├── config.rs │ ├── dbm.rs │ ├── errors.rs │ ├── extended_appointment.rs │ ├── gatekeeper.rs │ ├── lib.rs │ ├── main.rs │ ├── responder.rs │ ├── rpc_errors.rs │ ├── test_utils.rs │ ├── tls.rs │ ├── tx_index.rs │ └── watcher.rs └── watchtower-plugin ├── Cargo.toml ├── README.md ├── src ├── constants.rs ├── convert.rs ├── dbm.rs ├── lib.rs ├── main.rs ├── net │ ├── http.rs │ └── mod.rs ├── retrier.rs ├── ser.rs ├── test_utils.rs └── wt_client.rs └── tests ├── conftest.py ├── pyproject.toml └── test.py /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | platform: [ ubuntu-latest, macos-latest, windows-latest ] 15 | toolchain: [ stable ] 16 | include: 17 | - platform: windows-latest 18 | arguments: --workspace --exclude watchtower-plugin 19 | 20 | runs-on: ${{ matrix.platform }} 21 | steps: 22 | - name: Checkout source code 23 | uses: actions/checkout@v4 24 | - name: Install Rust ${{ matrix.toolchain }} toolchain 25 | uses: dtolnay/rust-toolchain@master 26 | with: 27 | toolchain: ${{ matrix.toolchain }} 28 | - name: Install Protoc 29 | uses: arduino/setup-protoc@v3 30 | with: 31 | repo-token: ${{ secrets.GITHUB_TOKEN }} 32 | - name: Build on Rust ${{ matrix.toolchain }} 33 | run: | 34 | cargo build ${{ matrix.arguments }} --verbose --color always 35 | - name: Test on Rust ${{ matrix.toolchain }} 36 | run: | 37 | cargo test ${{ matrix.arguments }} --verbose --color always 38 | 39 | lint: 40 | runs-on: ubuntu-latest 41 | steps: 42 | - name: Checkout source code 43 | uses: actions/checkout@v4 44 | - name: Install Rust stable toolchain 45 | uses: dtolnay/rust-toolchain@master 46 | with: 47 | toolchain: stable 48 | components: rustfmt, clippy 49 | - name: Install Protoc 50 | uses: arduino/setup-protoc@v3 51 | with: 52 | repo-token: ${{ secrets.GITHUB_TOKEN }} 53 | - name: Run rustfmt 54 | run: | 55 | cargo fmt --verbose --check -- --color always 56 | - name: Run clippy 57 | run: | 58 | cargo clippy --all-features --all-targets --color always -- --deny warnings 59 | 60 | python-lint: 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: Checkout source code 64 | uses: actions/checkout@v4 65 | - name: Run black 66 | uses: psf/black@stable 67 | with: 68 | src: "./watchtower-plugin/tests" 69 | options: "--check -l 120" -------------------------------------------------------------------------------- /.github/workflows/cln-plugin.yaml: -------------------------------------------------------------------------------- 1 | name: CI tests for CLN watchtower-plugin 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | env: 10 | bitcoind_version: "27.0" 11 | cln_version: "24.11.1" 12 | 13 | jobs: 14 | cache-cln: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-python@v5 19 | with: 20 | python-version: '3.9' 21 | check-latest: true 22 | - uses: arduino/setup-protoc@v3 23 | with: 24 | repo-token: ${{ secrets.GITHUB_TOKEN }} 25 | - name: Create CLN cache 26 | id: cache-cln 27 | uses: actions/cache@v4 28 | env: 29 | cache-name: cache-cln-dev 30 | with: 31 | path: lightning 32 | key: ${{ runner.os }}-build-${{ env.cache-name }}-v${{ env.cln_version }} 33 | - name: Compile CLN 34 | env: 35 | PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring 36 | if: ${{ steps.cache-cln.outputs.cache-hit != 'true' }} 37 | run: | 38 | sudo apt-get update && sudo apt-get install -y gettext 39 | git clone https://github.com/ElementsProject/lightning.git && cd lightning && git checkout v${{ env.cln_version }} 40 | pip install --user poetry && poetry install 41 | cargo update -p time 42 | ./configure && poetry run make 43 | 44 | cln-plugin: 45 | needs: cache-cln 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v4 49 | - uses: actions/setup-python@v5 50 | with: 51 | python-version: '3.9' 52 | check-latest: true 53 | - uses: arduino/setup-protoc@v3 54 | with: 55 | repo-token: ${{ secrets.GITHUB_TOKEN }} 56 | - name: Install Rust toolchain 57 | uses: dtolnay/rust-toolchain@master 58 | with: 59 | toolchain: 1.81.0 60 | components: rustfmt, clippy 61 | - name: Install bitcoind 62 | run: | 63 | wget https://bitcoincore.org/bin/bitcoin-core-${{ env.bitcoind_version }}/bitcoin-${{ env.bitcoind_version }}-x86_64-linux-gnu.tar.gz 64 | tar -xzf bitcoin-${{ env.bitcoind_version }}-x86_64-linux-gnu.tar.gz 65 | ln -s $(pwd)/bitcoin-${{ env.bitcoind_version }}/bin/bitcoin* /usr/local/bin 66 | - name: Load CLN cache 67 | id: cache-cln 68 | uses: actions/cache@v4 69 | env: 70 | cache-name: cache-cln-dev 71 | with: 72 | path: lightning 73 | key: ${{ runner.os }}-build-${{ env.cache-name }}-v${{ env.cln_version }} 74 | - name: Link CLN 75 | run: | 76 | source $HOME/.cargo/env 77 | cd lightning && sudo make install 78 | - name: Install teos and the plugin 79 | run: | 80 | cargo install --locked --path teos 81 | cargo install --locked --path watchtower-plugin 82 | - name: Add test dependencies 83 | run: | 84 | cd watchtower-plugin/tests 85 | pip install --user poetry && poetry install 86 | - name: Run tests 87 | run: | 88 | cd watchtower-plugin/tests 89 | VALGRIND=0 SLOW_MACHINE=1 poetry run pytest test.py --log-cli-level=INFO -s 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | __pycache__ 3 | .vscode 4 | .idea -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Eye of Satoshi 2 | 3 | The following is a set of guidelines for contributing to `rust-teos`. 4 | 5 | ## Code Style Guidelines 6 | We use `rustfmt` as our base code formatter. Before submitting a PR make sure you have properly formatted your code by running: 7 | 8 | ```bash 9 | cargo fmt 10 | ``` 11 | 12 | In addition, we use [clippy](https://github.com/rust-lang/rust-clippy/) to catch common mistakes and improve the code: 13 | 14 | ```bash 15 | cargo clippy 16 | ``` 17 | 18 | On top of that, there are a few rules to also have in mind. 19 | 20 | ### Code Spacing 21 | Blocks of code should be created to separate logical sections: 22 | 23 | ```rust 24 | let mut tx_tracker_map = self.tx_tracker_map.lock().unwrap(); 25 | if let Some(map) = tx_tracker_map.get_mut(&tracker.penalty_tx.txid()) { 26 | map.insert(uuid); 27 | } else { 28 | tx_tracker_map.insert(tracker.penalty_tx.txid(), HashSet::from_iter(vec![uuid])); 29 | } 30 | 31 | let mut unconfirmed_txs = self.unconfirmed_txs.lock().unwrap(); 32 | if confirmations == 0 { 33 | unconfirmed_txs.insert(tracker.penalty_tx.txid()); 34 | } 35 | ``` 36 | 37 | ## Code Documentation 38 | Code should be documented, specially if its visibility is public. 39 | 40 | Here's an example of struct docs: 41 | 42 | ```rust 43 | /// Component in charge of keeping track of triggered appointments. 44 | /// 45 | /// The [Responder] receives data from the [Watcher](crate::watcher::Watcher) in form of a [Breach]. 46 | /// From there, a [TransactionTracker] is created and the penalty transaction is sent to the network via the [Carrier]. 47 | /// The [Transaction] is then monitored to make sure it makes it to a block and it gets [irrevocably resolved](https://github.com/lightning/bolts/blob/master/05-onchain.md#general-nomenclature). 48 | #[derive(Debug)] 49 | pub struct Responder { 50 | /// A map holding a summary of every tracker ([TransactionTracker]) hold by the [Responder], identified by [UUID]. 51 | /// The identifiers match those used by the [Watcher](crate::watcher::Watcher). 52 | trackers: Mutex>, 53 | /// A map between [Txid]s and [UUID]s. 54 | tx_tracker_map: Mutex>>, 55 | /// A collection of transactions yet to get a single confirmation. 56 | /// Only keeps track of penalty transactions being monitored by the [Responder]. 57 | unconfirmed_txs: Mutex>, 58 | /// A collection of [Transaction]s that have missed some confirmation, along with the missed count. 59 | /// Only keeps track of penalty transactions being monitored by the [Responder]. 60 | missed_confirmations: Mutex>, 61 | /// A [Carrier] instance. Data is sent to the `bitcoind` through it. 62 | carrier: Mutex, 63 | /// A [Gatekeeper] instance. Data regarding users is requested to it. 64 | gatekeeper: Arc, 65 | /// A [DBM] (database manager) instance. Used to persist tracker data into disk. 66 | dbm: Arc>, 67 | /// The last known block header. 68 | last_known_block_header: Mutex, 69 | } 70 | ``` 71 | 72 | ## Test Coverage 73 | 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. 74 | 75 | ## Git conventions 76 | 77 | ### Commits, titles, and descriptions 78 | 79 | - Changes must be split logically in commits, such that a commit is self-contained 80 | - 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) 81 | - 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** 82 | - Descriptions can be provided to give more context about what has been fixed and how 83 | 84 | ### Pull requests 85 | 86 | - 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 87 | - 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) 88 | - **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: 89 | 90 | ``` 91 | 886b0ff Adds X functionality to component Y 92 | 801ff5d Fixes the previous commit because Z 93 | 67ac345 Addresses review comments 94 | 7dc7fcd Updates X because G was missing 95 | b60999c Adds missing test 96 | ... 97 | ``` 98 | 99 | - Create a new branch to work on your pull request. **DO NOT** work from the master branch of your fork* 100 | - **DO NOT** merge master into your branch, rebase master instead* 101 | 102 | \* If you're not sure how to handle this, check external documentation on how to manage multiple remotes for the same repository. 103 | 104 | ### Signing Commits 105 | 106 | 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). 107 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "teos", 6 | "teos-common", 7 | "watchtower-plugin" 8 | ] 9 | -------------------------------------------------------------------------------- /DEPENDENCIES.md: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | 3 | `rust-teos` has the following system-wide dependencies: 4 | 5 | - `rust` 6 | - `bitcoind` 7 | 8 | ### Minimum Supported Rust Version (MSRV) 9 | Refer to [toolchain](./rust-toolchain.toml) 10 | 11 | ### Installing Rust 12 | Refer to [rust-lang.org](https://www.rust-lang.org/tools/install). 13 | 14 | ### Installing bitcoind 15 | 16 | `rust-teos` runs on top of a Bitcoin Core node. Other underlying Bitcoin nodes are not supported at the moment. 17 | 18 | You can get Bitcoin Core from [bitcoincore.org](https://bitcoincore.org/en/download/). 19 | 20 | Bitcoin needs to be running with the following options enabled: 21 | 22 | - `server` to run rpc commands 23 | 24 | Here's an example of a `bitcoin.conf` you can use for mainnet. **DO NOT USE THE PROVIDED RPC USER AND PASSWORD.** 25 | 26 | ``` 27 | # [rpc] 28 | server=1 29 | rpcuser=user 30 | rpcpassword=passwd 31 | rpcservertimeout=600 32 | 33 | # [others] 34 | daemon=1 35 | debug=1 36 | maxtxfee=1 37 | ``` 38 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Install 2 | 3 | The tower can be installed and tested using cargo: 4 | 5 | ``` 6 | git clone https://github.com/talaia-labs/rust-teos.git 7 | cd rust-teos 8 | cargo install --locked --path teos 9 | ``` 10 | 11 | You can run tests with: 12 | 13 | ``` 14 | cargo test 15 | ``` 16 | 17 | Please refer to the cargo documentation for more detailed instructions. 18 | 19 | # Systemd setup for backend 20 | 21 | Refer to [contrib](contrib/init/README.md) for a detailed explanation of how to set up your systemd service for `teosd`. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Talaia Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Eye of Satoshi (rust-teos) 2 | 3 | The Eye of Satoshi is a Lightning watchtower compliant with [BOLT13](https://github.com/sr-gi/bolt13), written in Rust. 4 | 5 | [![discord](https://img.shields.io/discord/991334710611550208?logo=discord&style=plastic)](https://discord.gg/EyVbrNMDUP) 6 | [![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) 7 | [![release](https://img.shields.io/github/v/release/talaia-labs/rust-teos?style=plastic)](https://github.com/talaia-labs/rust-teos/releases/latest) 8 | 9 | 10 | `rust-teos` consists of two main crates: 11 | 12 | - `teos`: including the tower's main functionality (server-side) and a CLI. Compiling this crate will generate two binaries: `teosd` and `teos-cli`. 13 | - `teos-common`: including shared functionality between server and client-side (useful to build a client). 14 | 15 | ## Dependencies 16 | 17 | Refer to [DEPENDENCIES.md](DEPENDENCIES.md) 18 | 19 | ## Installation 20 | Refer to [INSTALL.md](INSTALL.md) 21 | 22 | ## Running TEOS 23 | 24 | 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. 25 | 26 | Please see [Docker instructions](docker/README.md) for instructions on how to set up `teosd` in Docker. 27 | 28 | ### Starting the tower daemon ♖ 29 | 30 | Once installed, you can start the tower by running: 31 | 32 | ``` 33 | teosd 34 | ``` 35 | 36 | ### Configuration file and command line parameters 37 | 38 | `teosd` comes with a default configuration that can be found at [teos/src/config.rs](teos/src/config.rs). 39 | 40 | The configuration includes, amongst others, where your data folder is placed, what network it connects to, etc. 41 | 42 | To change the configuration defaults you can: 43 | 44 | - Define a configuration file named `teos.toml` following the template (check [conf_template.toml](teos/src/conf_template.toml)) and place it in the `data_dir` (that defaults to `~/.teos/`). 45 | 46 | and/or 47 | 48 | - Add some global options when running the daemon (run `teosd -h` for more info). 49 | 50 | ### Passing command-line options to `teosd` 51 | 52 | Some configuration options can also be specified when running `teosd`. We can, for instance, change the tower data directory as follows: 53 | 54 | ``` 55 | teosd --datadir= 56 | ``` 57 | 58 | ### Running `teosd` in another network 59 | 60 | By default, `teosd` runs on `mainnet`. In order to run it on another network, you need to change the network parameter in the configuration file or pass the network parameter as a command-line option. Notice that if `teosd` does not find a `bitcoind` node running in the same network that it is set to run, it will refuse to run. 61 | 62 | The configuration file option to change the network where `teosd` will run is `btc_network`: 63 | 64 | ``` 65 | btc_network = mainnet 66 | ``` 67 | 68 | For regtest, it should look like: 69 | 70 | ``` 71 | btc_network = regtest 72 | ``` 73 | 74 | ### Running `teosd` with Tor 75 | 76 | This requires a Tor daemon running on the same machine as `teosd` and a control port open on that daemon. 77 | 78 | Download Tor from the [torproject site](https://www.torproject.org/download/). 79 | 80 | 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)): 81 | 82 | ``` 83 | ## The port on which Tor will listen for local connections from Tor 84 | ## controller applications, as documented in control-spec.txt. 85 | ControlPort 9051 86 | 87 | ## If you enable the controlport, be sure to enable one of these 88 | ## authentication methods, to prevent attackers from accessing it. 89 | CookieAuthentication 1 90 | CookieAuthFileGroupReadable 1 91 | ``` 92 | 93 | Once the Tor daemon is running, and the control port is open, make sure to enable `--torsupport` when running `teosd`. 94 | 95 | ### Tower id and signing key 96 | 97 | `teosd` needs a pair of keys that will serve as tower id and signing key. The former can be used by users to identify the tower, whereas the latter is used by the tower to sign responses. These keys are automatically generated on the first run and can be refreshed by running `teosd` with the `--overwritekey` flag. Notice that once a key is overwritten you won't be able to use the previous key again*. 98 | 99 | \* Old keys are actually kept in the tower's database as a fail-safe in case you overwrite them by mistake. However, there is no automated way of switching back to an old key. Feel free to open an issue if you overwrote your key by mistake and need support to recover it. 100 | 101 | ## Interacting with a TEOS instance 102 | 103 | You can interact with a `teosd` instance (either run by yourself or someone else) by using `teos-cli`. This is an admin tool that has privileged access to the watchtower, and it should therefore only be used within a trusted environment (for example, the same machine). 104 | 105 | While `teos-cli` works independently of `teosd`, it shares the same configuration file by default, of which it only uses a subset of its settings. The folder can be changed using the `--datadir` command-line argument if desired. 106 | 107 | For help on the available arguments and commands, you can run: 108 | 109 | ``` 110 | teos-cli -h 111 | ``` 112 | 113 | ### Running teos-cli remotely 114 | 115 | To run `teos-cli` remotely, you'll need to take one extra step. When `teosd` is started up, self-signed certificates are automatically generated for a user to make a secure connection to the remote TEOS watchtower. When the CLI is run locally, it knows where to find these files. But if run remotely, these files need to be copied over to the machine where the CLI is being run. 116 | 117 | The files are generated to the data directory (by default stored at `~/.teos/`). To run remotely, users need to copy the `client.pem`, `client-key.pem`, and `ca.pem` files to the corresponding watchtower data directory on the machine where the CLI is being run. That is, by default, to `~/.teos/` on the remote machine. 118 | 119 | ## Interacting with TEOS as a client 120 | ### TEOS clients 121 | 122 | Here is a list of the available clients for `teos`: 123 | 124 | - [watchtower-client for CLN](watchtower-plugin/) 125 | 126 | ## Contributing 127 | Refer to [CONTRIBUTING.md](CONTRIBUTING.md) 128 | -------------------------------------------------------------------------------- /coffee.yml: -------------------------------------------------------------------------------- 1 | --- 2 | plugin: 3 | name: rust-teos 4 | version: 0.2.0 5 | lang: rust 6 | install: | 7 | cargo build --release --locked --package watchtower-plugin 8 | cp target/release/watchtower-client . 9 | cargo clean 10 | main: watchtower-client 11 | -------------------------------------------------------------------------------- /contrib/init/README.md: -------------------------------------------------------------------------------- 1 | **This document guides you into how to set-up a systemd service to run `teosd`.** 2 | 3 | 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). 4 | 5 | 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: 6 | 7 | ``` 8 | cp teosd.service /etc/systemd/system 9 | ``` 10 | 11 | You can also create a file called `teosd.service` in the systemd folder and copy the content of [teosd.service](teosd.service) to it: 12 | 13 | ``` 14 | sudo vim /etc/systemd/system/teosd.service 15 | ``` 16 | 17 | 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: 18 | 19 | ``` 20 | [Service] 21 | ExecStart=/home//.cargo/bin/teosd 22 | SyslogIdentifier= 23 | 24 | # Directory creation and permissions 25 | #################################### 26 | User= 27 | Group= 28 | ``` 29 | 30 | The next step is enabling the service. You can do so by running: 31 | 32 | ``` 33 | sudo systemctl enable teosd.service 34 | ``` 35 | 36 | Finally, you can start the service by running: 37 | 38 | ``` 39 | sudo systemctl start teosd.service 40 | ``` 41 | 42 | From that point on, the tower will be run every time your system is turned on, and restarted if needed. 43 | -------------------------------------------------------------------------------- /contrib/init/teosd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=The Eye of Satoshi daemon 3 | Requires=bitcoind.service 4 | After=bitcoind.service 5 | Wants=network-online.target 6 | After=network-online.target 7 | 8 | [Service] 9 | ExecStart=/home/teos/.cargo/bin/teosd 10 | StandardOutput=journal 11 | StandardError=journal 12 | SyslogIdentifier=teos 13 | 14 | # Process management 15 | #################### 16 | Type=simple 17 | Restart=on-failure 18 | TimeoutSec=300 19 | RestartSec=60 20 | 21 | # Directory creation and permissions 22 | #################################### 23 | User=teos 24 | Group=teos 25 | 26 | # Hardening measures 27 | #################### 28 | # Provide a private /tmp and /var/tmp. 29 | PrivateTmp=true 30 | 31 | # Mount /usr, /boot/ and /etc read-only for the process. 32 | ProtectSystem=full 33 | 34 | # Disallow the process and all of its children to gain 35 | # new privileges through execve(). 36 | NoNewPrivileges=true 37 | 38 | # Use a new /dev namespace only populated with API pseudo devices 39 | # such as /dev/null, /dev/zero and /dev/random. 40 | PrivateDevices=true 41 | 42 | # Deny the creation of writable and executable memory mappings. 43 | MemoryDenyWriteExecute=true 44 | 45 | [Install] 46 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the rust image as the base image for the build stage 2 | FROM rust:latest AS builder 3 | 4 | # Copy the rust-teos source code 5 | COPY . /tmp/rust-teos 6 | 7 | # Install the dependencies required for building rust-teos 8 | RUN apt-get update\ 9 | && apt-get -y --no-install-recommends install libffi-dev libssl-dev musl-tools pkg-config 10 | 11 | RUN cd /tmp/rust-teos \ 12 | && rustup target add x86_64-unknown-linux-musl \ 13 | # Rustfmt is needed to format the grpc stubs generated by tonic 14 | && rustup component add rustfmt \ 15 | # Cross compile with musl as the target, so teosd can run on alpine 16 | && RUSTFLAGS='-C target-feature=+crt-static' cargo build --manifest-path=teos/Cargo.toml --locked --release --target x86_64-unknown-linux-musl 17 | 18 | # Use a new stage with a smaller base image to reduce image size 19 | FROM alpine:latest 20 | 21 | RUN apk update && apk upgrade 22 | 23 | # UID and GID for the teosd user 24 | ENV TEOS_UID=1001 TEOS_GID=1001 25 | 26 | # Copy the teos binaries from the build stage to the new stage 27 | COPY --from=builder \ 28 | /tmp/rust-teos/target/x86_64-unknown-linux-musl/release/teosd \ 29 | /tmp/rust-teos/target/x86_64-unknown-linux-musl/release/teos-cli /usr/local/bin/ 30 | 31 | # Copy the entrypoint script to the container 32 | COPY docker/entrypoint.sh /entrypoint.sh 33 | 34 | # Set the entrypoint script as executable and add running user 35 | RUN chmod +x /entrypoint.sh \ 36 | && addgroup -g ${TEOS_GID} -S teos \ 37 | && adduser -S -G teos -u ${TEOS_UID} teos 38 | 39 | # Expose the default port used by teosd 40 | EXPOSE 9814/tcp 41 | 42 | # Switch user so that we don't run stuff as root 43 | USER teos 44 | 45 | # Create the teos data directory 46 | RUN mkdir /home/teos/.teos 47 | 48 | # Start teosd when the container starts 49 | ENTRYPOINT [ "/entrypoint.sh" ] 50 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | ## Running `teosd` in a docker container 2 | A `teos` image can be built from the Dockerfile located in `docker`. You can create the image by running: 3 | 4 | cd rust-teos 5 | docker build -f docker/Dockerfile -t teos . 6 | 7 | Then we can create a container by running: 8 | 9 | docker run -it teos 10 | 11 | One way to feed `teos` custom config options is to set environment variables: 12 | 13 | docker run -it -e teos 14 | 15 | Notice that the ENV variables are optional, if unset the corresponding default setting is used. The following ENVs are available: 16 | 17 | ``` 18 | - API_BIND= 19 | - API_PORT= 20 | - RPC_BIND= 21 | - RPC_PORT= 22 | - BTC_NETWORK= 23 | - BTC_RPC_CONNECT= 24 | - BTC_RPC_PORT= 25 | - BTC_RPC_USER= 26 | - BTC_RPC_PASSWORD= 27 | # The following options can be set turned on by setting them to "true" 28 | - DEBUG= 29 | - DEPS_DEBUG= 30 | - OVERWRITE_KEY= 31 | - FORCE_UPDATE= 32 | ``` 33 | 34 | ### Volume persistence 35 | 36 | You may also want to run docker with a volume, so you can have data persistence in `teosd` databases and keys. 37 | If so, run: 38 | 39 | docker volume create teos-data 40 | 41 | And add the the mount parameter to `docker run`: 42 | 43 | -v teos-data:/home/teos/.teos 44 | 45 | If you are running `teosd` and `bitcoind` in the same machine, continue reading for how to create the container based on your OS. 46 | 47 | ### `bitcoind` running on the same machine (UNIX) 48 | The easiest way to run both together in the same machine using UNIX is to set the container to use the host network. 49 | 50 | For example, if both `teosd` and `bitcoind` are running on default settings, run: 51 | 52 | ``` 53 | docker run \ 54 | --network=host \ 55 | --name teos \ 56 | -v teos-data:/home/teos/.teos \ 57 | -e BTC_RPC_USER= \ 58 | -e BTC_RPC_PASSWORD= \ 59 | -it teos 60 | ``` 61 | 62 | Notice that you may still need to set your RPC authentication details, since, hopefully, your credentials won't match the `teosd` defaults. 63 | 64 | ### `bitcoind` running on the same machine (OSX or Windows) 65 | 66 | Docker for OSX and Windows does not allow to use the host network (nor to use the `docker0` bridge interface). To work around this 67 | you can use the special `host.docker.internal` domain: 68 | 69 | ``` 70 | docker run \ 71 | -p 9814:9814 \ 72 | -p 8814:8814 \ 73 | --name teos \ 74 | -v teos-data:/home/teos/.teos \ 75 | -e BTC_RPC_CONNECT=host.docker.internal \ 76 | -e BTC_RPC_USER= \ 77 | -e BTC_RPC_PASSWORD= \ 78 | -e API_BIND=0.0.0.0 \ 79 | -e RPC_BIND=0.0.0.0 \ 80 | -it teos 81 | ``` 82 | 83 | 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. 84 | Otherwise it will bind to `localhost` and we won't be able to send requests to the tower from the host. 85 | 86 | ### Interacting with a TEOS instance 87 | 88 | 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: 89 | 90 | 1) You can open a shell to the Docker instance by calling: 91 | 92 | `docker exec -it sh` 93 | 94 | Then you can use the `teos-cli` binary from inside the container as you would use it from your host machine. 95 | 96 | 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). 97 | 98 | ### Plugging in Tor 99 | 100 | 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. 101 | 102 | 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. 103 | 104 | 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: 105 | 106 | ``` 107 | HiddenServiceDir /var/lib/tor/teosd # Path for Linux. This may differ depending on your OS. 108 | HiddenServicePort 9814 127.0.0.1:9814 109 | ``` 110 | 111 | Then restart Tor. 112 | 113 | If all works correctly, the hidden service public key will be located in the `HiddenServiceDir` you set above, in the file called `hostname`. 114 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Define the start command 4 | START_COMMAND="teosd" 5 | 6 | # Set the API bind address 7 | if [[ ! -z ${API_BIND} ]]; then 8 | START_COMMAND="$START_COMMAND --apibind $API_BIND" 9 | fi 10 | 11 | # Set the API port 12 | if [[ ! -z ${API_PORT} ]]; then 13 | START_COMMAND="$START_COMMAND --apiport $API_PORT" 14 | fi 15 | 16 | # Set the RPC bind address 17 | if [[ ! -z ${RPC_BIND} ]]; then 18 | START_COMMAND="$START_COMMAND --rpcbind $RPC_BIND" 19 | fi 20 | 21 | # Set the RPC port 22 | if [[ ! -z ${RPC_PORT} ]]; then 23 | START_COMMAND="$START_COMMAND --rpcport $RPC_PORT" 24 | fi 25 | 26 | # Set the Bitcoin network 27 | if [[ ! -z ${BTC_NETWORK} ]]; then 28 | START_COMMAND="$START_COMMAND --btcnetwork $BTC_NETWORK" 29 | fi 30 | 31 | # Set the Bitcoin RPC credentials 32 | if [[ ! -z ${BTC_RPC_USER} ]]; then 33 | START_COMMAND="$START_COMMAND --btcrpcuser $BTC_RPC_USER" 34 | fi 35 | 36 | if [[ ! -z ${BTC_RPC_PASSWORD} ]]; then 37 | START_COMMAND="$START_COMMAND --btcrpcpassword $BTC_RPC_PASSWORD" 38 | fi 39 | 40 | # Set the Bitcoin RPC connection details 41 | if [[ ! -z ${BTC_RPC_CONNECT} ]]; then 42 | START_COMMAND="$START_COMMAND --btcrpcconnect $BTC_RPC_CONNECT" 43 | fi 44 | 45 | if [[ ! -z ${BTC_RPC_PORT} ]]; then 46 | START_COMMAND="$START_COMMAND --btcrpcport $BTC_RPC_PORT" 47 | fi 48 | 49 | if [ "${DEBUG}" == "true" ]; then 50 | START_COMMAND="$START_COMMAND --debug" 51 | fi 52 | 53 | if [ "${DEPS_DEBUG}" == "true" ]; then 54 | START_COMMAND="$START_COMMAND --depsdebug" 55 | fi 56 | 57 | if [ "${OVERWRITE_KEY}" == "true" ]; then 58 | START_COMMAND="$START_COMMAND --overwritekey" 59 | fi 60 | 61 | if [ "${FORCE_UPDATE}" == "true" ]; then 62 | START_COMMAND="$START_COMMAND --forceupdate" 63 | fi 64 | 65 | # Start the TEOS daemon 66 | $START_COMMAND 67 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.81.0" 3 | components = [ 4 | "rustfmt", 5 | "clippy", 6 | ] 7 | -------------------------------------------------------------------------------- /teos-common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "teos-common" 3 | version = "0.2.0" 4 | authors = ["Sergi Delgado Segura "] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | # General 11 | hex = { version = "0.4.3", features = [ "serde" ] } 12 | prost = "0.12" 13 | rusqlite = { version = "0.26.0", features = [ "bundled", "limits" ] } 14 | serde = "1.0.130" 15 | serde_json = "1.0" 16 | tonic = "0.11" 17 | 18 | # Crypto 19 | rand = "0.8.4" 20 | chacha20poly1305 = "0.8.0" 21 | 22 | # Bitcoin and Lightning 23 | bitcoin = { version = "0.32.0", features = [ "serde" ] } 24 | lightning = "0.1.0" 25 | 26 | [build-dependencies] 27 | tonic-build = "0.11" 28 | -------------------------------------------------------------------------------- /teos-common/build.rs: -------------------------------------------------------------------------------- 1 | fn main() -> Result<(), Box> { 2 | tonic_build::configure() 3 | .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") 4 | .type_attribute("AppointmentData.appointment_data", "#[serde(untagged)]") 5 | .field_attribute("AppointmentData.appointment_data", "#[serde(flatten)]") 6 | .field_attribute("appointment_data", "#[serde(rename = \"appointment\")]") 7 | .field_attribute("user_id", "#[serde(with = \"hex::serde\")]") 8 | .field_attribute("locator", "#[serde(with = \"hex::serde\")]") 9 | .field_attribute( 10 | "locators", 11 | "#[serde(with = \"crate::ser::serde_vec_bytes\")]", 12 | ) 13 | .field_attribute("encrypted_blob", "#[serde(with = \"hex::serde\")]") 14 | .field_attribute("dispute_txid", "#[serde(with = \"crate::ser::serde_be\")]") 15 | .field_attribute("penalty_txid", "#[serde(with = \"crate::ser::serde_be\")]") 16 | .field_attribute("penalty_rawtx", "#[serde(with = \"hex::serde\")]") 17 | .field_attribute( 18 | "GetAppointmentResponse.status", 19 | "#[serde(with = \"crate::ser::serde_status\")]", 20 | ) 21 | .compile( 22 | &[ 23 | "proto/common/teos/v2/appointment.proto", 24 | "proto/common/teos/v2/user.proto", 25 | ], 26 | &["proto/common/teos/v2"], 27 | )?; 28 | 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /teos-common/proto/common/teos/v2/appointment.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package common.teos.v2; 3 | 4 | message Appointment { 5 | /* 6 | Contains the basic information about an appointment (Watcher) and it's used for messages like 7 | AddAppointmentRequest or encapsulated inside AppointmentData for GetAppointmentResponse 8 | */ 9 | 10 | bytes locator = 1; 11 | bytes encrypted_blob = 2; 12 | uint32 to_self_delay = 3; 13 | 14 | } 15 | 16 | message Tracker { 17 | // It's the equivalent of an appointment message from data held by the Responder. 18 | 19 | bytes dispute_txid = 1; 20 | bytes penalty_txid = 2; 21 | bytes penalty_rawtx = 3; 22 | } 23 | 24 | message AppointmentData { 25 | /* 26 | Encapsulates the data for a GetAppointmentResponse, given it can be an appointment (data is on the Watcher) or a 27 | tracker (data is on the Responder). 28 | */ 29 | 30 | oneof appointment_data { 31 | Appointment appointment = 1; 32 | Tracker tracker = 2; 33 | } 34 | } 35 | 36 | message AddAppointmentRequest { 37 | // Request to add an appointment to the backend, contains the appointment data and the user signature. 38 | 39 | Appointment appointment = 1; 40 | string signature = 2; 41 | } 42 | 43 | message AddAppointmentResponse { 44 | /* 45 | Response to an AddAppointmentRequest, contains the locator to identify the added appointment, the tower signature, 46 | the block at which the tower has started (or will start) watching for the appointment, and the updated subscription 47 | information. 48 | */ 49 | 50 | bytes locator = 1; 51 | uint32 start_block = 2; 52 | string signature = 3; 53 | uint32 available_slots = 4; 54 | uint32 subscription_expiry = 5; 55 | } 56 | 57 | message GetAppointmentRequest { 58 | // Request to get information about an appointment. Contains the appointment locator and a signature by the user. 59 | 60 | bytes locator = 1; 61 | string signature = 2; 62 | } 63 | 64 | message GetAppointmentResponse { 65 | // Response to a GetAppointmentRequest. Contains the appointment data encapsulated in an AppointmentData message. 66 | 67 | AppointmentData appointment_data = 1; 68 | enum AppointmentStatus { 69 | NOT_FOUND = 0; 70 | BEING_WATCHED = 1; 71 | DISPUTE_RESPONDED = 2; 72 | 73 | } 74 | AppointmentStatus status = 2; 75 | } -------------------------------------------------------------------------------- /teos-common/proto/common/teos/v2/user.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package common.teos.v2; 3 | 4 | message RegisterRequest { 5 | // Requests a user registration with the tower. Contains the user id in the form of a compressed ECDSA public key. 6 | 7 | bytes user_id = 1; 8 | } 9 | 10 | message RegisterResponse { 11 | // Response to a RegisterRequest, contains the registration information alongside the tower signature of the agreement. 12 | 13 | bytes user_id = 1; 14 | uint32 available_slots = 2; 15 | uint32 subscription_start = 3; 16 | uint32 subscription_expiry = 4; 17 | string subscription_signature = 5; 18 | } 19 | 20 | message GetSubscriptionInfoRequest { 21 | // Request to get a specific user's subscription info. 22 | 23 | string signature = 1; 24 | } 25 | 26 | message GetSubscriptionInfoResponse { 27 | // Response with the information the tower has about a specific user 28 | 29 | uint32 available_slots = 1; 30 | uint32 subscription_expiry = 2; 31 | repeated bytes locators = 3; 32 | } -------------------------------------------------------------------------------- /teos-common/src/appointment.rs: -------------------------------------------------------------------------------- 1 | //! Logic related to appointments shared between users and the towers. 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use std::array::TryFromSliceError; 5 | use std::{convert::TryInto, fmt}; 6 | 7 | use bitcoin::Txid; 8 | 9 | use crate::protos as msgs; 10 | 11 | pub const LOCATOR_LEN: usize = 16; 12 | 13 | /// User identifier for appointments. 14 | #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Serialize, Deserialize)] 15 | pub struct Locator([u8; LOCATOR_LEN]); 16 | 17 | impl Locator { 18 | /// Creates a new [Locator]. 19 | pub fn new(txid: Txid) -> Self { 20 | Locator(txid[..LOCATOR_LEN].try_into().unwrap()) 21 | } 22 | 23 | /// Encodes a locator into its byte representation. 24 | pub fn to_vec(&self) -> Vec { 25 | self.0.to_vec() 26 | } 27 | 28 | /// Builds a locator from its byte representation. 29 | pub fn from_slice(data: &[u8]) -> Result { 30 | data.try_into().map(Self) 31 | } 32 | } 33 | 34 | impl fmt::Display for Locator { 35 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 36 | write!(f, "{}", hex::encode(self.to_vec())) 37 | } 38 | } 39 | 40 | impl AsRef<[u8]> for Locator { 41 | fn as_ref(&self) -> &[u8] { 42 | &self.0 43 | } 44 | } 45 | 46 | impl hex::FromHex for Locator { 47 | type Error = String; 48 | 49 | fn from_hex>(hex: T) -> Result { 50 | let raw_locator = hex::decode(hex).map_err(|_| "Locator is not hex encoded")?; 51 | Locator::from_slice(&raw_locator) 52 | .map_err(|_| "Locator cannot be built from the given data".into()) 53 | } 54 | } 55 | 56 | /// Contains data regarding an appointment between a client and the tower. 57 | /// 58 | /// An appointment is requested for every new channel update. 59 | #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] 60 | pub struct Appointment { 61 | /// The user identifier for the appointment. 62 | pub locator: Locator, 63 | /// The encrypted blob of data to be handed to the tower. 64 | /// Should match an encrypted penalty transaction. 65 | pub encrypted_blob: Vec, 66 | /// The delay of the `to_self` output in the penalty transaction. 67 | /// Can be used by the tower to decide whether the job is worth accepting or not 68 | /// (useful for accountable towers). Currently not used. 69 | pub to_self_delay: u32, 70 | } 71 | 72 | /// Represents all the possible states of an appointment in the tower, or in a response to a client request. 73 | #[derive(Serialize, Deserialize, Debug)] 74 | pub enum AppointmentStatus { 75 | NotFound = 0, 76 | BeingWatched = 1, 77 | DisputeResponded = 2, 78 | } 79 | 80 | impl From for AppointmentStatus { 81 | fn from(x: i32) -> Self { 82 | match x { 83 | 1 => AppointmentStatus::BeingWatched, 84 | 2 => AppointmentStatus::DisputeResponded, 85 | _ => AppointmentStatus::NotFound, 86 | } 87 | } 88 | } 89 | 90 | impl std::str::FromStr for AppointmentStatus { 91 | type Err = String; 92 | 93 | fn from_str(s: &str) -> Result { 94 | match s { 95 | "being_watched" => Ok(AppointmentStatus::BeingWatched), 96 | "dispute_responded" => Ok(AppointmentStatus::DisputeResponded), 97 | "not_found" => Ok(AppointmentStatus::NotFound), 98 | _ => Err(format!("Unknown status: {s}")), 99 | } 100 | } 101 | } 102 | 103 | impl fmt::Display for AppointmentStatus { 104 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 105 | let s = match self { 106 | AppointmentStatus::BeingWatched => "being_watched", 107 | AppointmentStatus::DisputeResponded => "dispute_responded", 108 | AppointmentStatus::NotFound => "not_found", 109 | }; 110 | write!(f, "{s}") 111 | } 112 | } 113 | 114 | impl Appointment { 115 | /// Creates a new [Appointment] instance. 116 | pub fn new(locator: Locator, encrypted_blob: Vec, to_self_delay: u32) -> Self { 117 | Appointment { 118 | locator, 119 | encrypted_blob, 120 | to_self_delay, 121 | } 122 | } 123 | 124 | /// Serializes an appointment to be signed. 125 | /// The serialization follows the same ordering as the fields in the appointment: 126 | /// 127 | /// `locator || encrypted_blob || to_self_delay` 128 | /// 129 | /// All values are big endian. 130 | pub fn to_vec(&self) -> Vec { 131 | let mut result = self.locator.to_vec(); 132 | result.extend(&self.encrypted_blob); 133 | result.extend(self.to_self_delay.to_be_bytes().to_vec()); 134 | result 135 | } 136 | } 137 | 138 | impl From for msgs::Appointment { 139 | fn from(a: Appointment) -> Self { 140 | Self { 141 | locator: a.locator.to_vec(), 142 | encrypted_blob: a.encrypted_blob.clone(), 143 | to_self_delay: a.to_self_delay, 144 | } 145 | } 146 | } 147 | 148 | /// Computes the number of slots an appointment takes from a user subscription. 149 | /// 150 | /// This is based on the [encrypted_blob](Appointment::encrypted_blob) size and the slot size that was defined by the [Gatekeeper](crate::gatekeeper::Gatekeeper). 151 | pub fn compute_appointment_slots(blob_size: usize, blob_max_size: usize) -> u32 { 152 | (blob_size as f32 / blob_max_size as f32).ceil() as u32 153 | } 154 | -------------------------------------------------------------------------------- /teos-common/src/constants.rs: -------------------------------------------------------------------------------- 1 | //! Shared constant values. 2 | 3 | // LN general nomenclature 4 | /// Number of blocks required to consider a transaction irrevocable. 5 | pub const IRREVOCABLY_RESOLVED: u32 = 100; 6 | 7 | // Temporary constants, may be changed 8 | /// Maximum size of encrypted blobs in appointments. 9 | pub const ENCRYPTED_BLOB_MAX_SIZE: usize = 2048; 10 | -------------------------------------------------------------------------------- /teos-common/src/cryptography.rs: -------------------------------------------------------------------------------- 1 | //! Cryptography module, used in the interaction between users and towers. 2 | 3 | use chacha20poly1305::aead::{Aead, NewAead}; 4 | use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; 5 | use rand::distributions::Uniform; 6 | use rand::Rng; 7 | 8 | use bitcoin::consensus; 9 | use bitcoin::hashes::{sha256, Hash}; 10 | use bitcoin::secp256k1::{Error, PublicKey, Secp256k1, SecretKey}; 11 | use bitcoin::{Transaction, Txid}; 12 | use lightning::util::message_signing; 13 | 14 | /// Enum representing the possible errors when decrypting an encrypted blob. 15 | #[derive(Debug)] 16 | pub enum DecryptingError { 17 | AED(chacha20poly1305::aead::Error), 18 | Encode(bitcoin::consensus::encode::Error), 19 | } 20 | 21 | /// Shadows [message_signing::sign]. 22 | pub fn sign(msg: &[u8], sk: &SecretKey) -> String { 23 | message_signing::sign(msg, sk) 24 | } 25 | 26 | /// Shadows [message_signing::verify]. 27 | pub fn verify(msg: &[u8], sig: &str, pk: &PublicKey) -> bool { 28 | message_signing::recover_pk(msg, sig).map_or_else(|_| false, |x| x == *pk) 29 | } 30 | 31 | /// Shadows [message_signing::recover_pk]. 32 | pub fn recover_pk(msg: &[u8], sig: &str) -> Result { 33 | message_signing::recover_pk(msg, sig) 34 | } 35 | 36 | /// Encrypts a given message under a given secret using `chacha20poly1305`. 37 | /// 38 | /// The key material used is: 39 | /// - The dispute txid as encryption key. 40 | /// - `[0; 12]` as IV. 41 | /// 42 | /// The message to be encrypted is expected to be the penalty transaction. 43 | pub fn encrypt( 44 | message: &Transaction, 45 | secret: &Txid, 46 | ) -> Result, chacha20poly1305::aead::Error> { 47 | // Defaults is [0; 12] 48 | let nonce = Nonce::default(); 49 | let k = sha256::Hash::hash(secret.as_byte_array()); 50 | let key = Key::from_slice(k.as_byte_array()); 51 | 52 | let cypher = ChaCha20Poly1305::new(key); 53 | cypher.encrypt(&nonce, consensus::serialize(message).as_ref()) 54 | } 55 | 56 | /// Decrypts an encrypted blob of data using `chacha20poly1305` and a given secret. 57 | /// 58 | /// The key material used is: 59 | /// - The dispute txid as decryption key. 60 | /// - `[0; 12]` as IV. 61 | /// 62 | /// The result is expected to be a penalty transaction. 63 | pub fn decrypt(encrypted_blob: &[u8], secret: &Txid) -> Result { 64 | // Defaults is [0; 12] 65 | let nonce = Nonce::default(); 66 | let k = sha256::Hash::hash(secret.as_byte_array()); 67 | let key = Key::from_slice(k.as_byte_array()); 68 | 69 | let cypher = ChaCha20Poly1305::new(key); 70 | 71 | match cypher.decrypt(&nonce, encrypted_blob.as_ref()) { 72 | Ok(tx_bytes) => consensus::deserialize(&tx_bytes).map_err(DecryptingError::Encode), 73 | Err(e) => Err(DecryptingError::AED(e)), 74 | } 75 | } 76 | 77 | /// Utility function to create a vector of pseudo random bytes. 78 | /// 79 | /// Mainly used for testing purposes. 80 | pub fn get_random_bytes(size: usize) -> Vec { 81 | let mut rng = rand::thread_rng(); 82 | let uniform_u8 = Uniform::new(u8::MIN, u8::MAX); 83 | (&mut rng).sample_iter(uniform_u8).take(size).collect() 84 | } 85 | 86 | /// Gets a key pair generated in a pseudorandom way. 87 | pub fn get_random_keypair() -> (SecretKey, PublicKey) { 88 | loop { 89 | if let Ok(sk) = SecretKey::from_slice(&get_random_bytes(32)) { 90 | return (sk, PublicKey::from_secret_key(&Secp256k1::new(), &sk)); 91 | } 92 | } 93 | } 94 | 95 | #[cfg(test)] 96 | mod tests { 97 | use std::str::FromStr; 98 | 99 | use super::*; 100 | use bitcoin::consensus; 101 | use bitcoin::hashes::hex::FromHex; 102 | 103 | const HEX_TX: &str = "010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff54038e830a1b4d696e656420627920416e74506f6f6c373432c2005b005e7a0ae3fabe6d6d7841cd582ead8ea5dd8e3de1173cae6fcd2a53c7362ebb7fb6f815604fe07cbe0200000000000000ac0e060005f90000ffffffff04d9476026000000001976a91411dbe48cc6b617f9c6adaf4d9ed5f625b1c7cb5988ac0000000000000000266a24aa21a9ed7248c6efddd8d99bfddd7f499f0b915bffa8253003cc934df1ff14a81301e2340000000000000000266a24b9e11b6d7054937e13f39529d6ad7e685e9dd4efa426f247d5f5a5bed58cdddb2d0fa60100000000000000002b6a2952534b424c4f434b3a054a68aa5368740e8b3e3c67bce45619c2cfd07d4d4f0936a5612d2d0034fa0a0120000000000000000000000000000000000000000000000000000000000000000000000000"; 104 | const HEX_TXID: &str = "d6ac4a5e61657c4c604dcde855a1db74ec6b3e54f32695d72c5e11c7761ea1b4"; 105 | const ENC_BLOB: &str = "f64d730654738fdbcd9e65068be17bc1abb44e74f8977985cce48e77209cf97292c862e4eb7190aedc6c53ceddda6871a3988d1d9608e2d0dd7a1f59769e410618a7029001479ac3b9d699b11a08b0ccb04e56bfee88461d9cd3207623a4a543996dd3805323c93cd62069636305aaf159e9cca1063ad1f097c16fb3c2ebbcf09be96512c5d7c195c684569cbe8b7979870b04cada9806b7610569c66021afcc63f46dd4af75716950c4de094334cdf7d9e532820afe29d2621dd79920c7e0ecc10853517dd84ca9d699f712c229e86954c227cba1d0fc87c8d48ac05e2de8a6bc980afdfafcd7064e411c8d76065c06cc7f233e869eaff5bd8ccb5d8f0090d91a8f017355cc115863356ecf06cdda9b309096ea766d033dbd4f70a789a5b03138cfc7e2900a79bb465abf07a7ac45c41b4b30c008d4b299aad9d001cf45acd07e47cdd63c3b13d4b0788b041735225b5db1a43a2142311f695478168e31deb260702976fd70d0724ded84a7c3f89b"; 106 | 107 | #[test] 108 | fn test_encrypt() { 109 | let expected_enc_blob = Vec::from_hex(ENC_BLOB).unwrap(); 110 | let tx_bytes = Vec::from_hex(HEX_TX).unwrap(); 111 | 112 | let tx: Transaction = consensus::deserialize(&tx_bytes).unwrap(); 113 | let txid = bitcoin::Txid::from_str(HEX_TXID).unwrap(); 114 | assert_eq!(encrypt(&tx, &txid).unwrap(), expected_enc_blob); 115 | } 116 | 117 | #[test] 118 | fn test_decrypt() { 119 | let expected_tx = consensus::deserialize(&Vec::from_hex(HEX_TX).unwrap()).unwrap(); 120 | 121 | let encrypted_blob = Vec::from_hex(ENC_BLOB).unwrap(); 122 | let txid = bitcoin::Txid::from_str(HEX_TXID).unwrap(); 123 | assert_eq!(decrypt(&encrypted_blob, &txid).unwrap(), expected_tx); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /teos-common/src/dbm.rs: -------------------------------------------------------------------------------- 1 | //! Logic related to a common database manager, component in charge of persisting data on disk. This is the base of more complex managers 2 | //! that can be used by both clients and towers. 3 | //! 4 | 5 | use rusqlite::ffi::{SQLITE_CONSTRAINT_FOREIGNKEY, SQLITE_CONSTRAINT_PRIMARYKEY}; 6 | use rusqlite::{Connection, Error as SqliteError, ErrorCode, Params}; 7 | 8 | /// Packs the errors than can raise when interacting with the underlying database. 9 | #[derive(Debug)] 10 | pub enum Error { 11 | AlreadyExists, 12 | MissingForeignKey, 13 | MissingField, 14 | NotFound, 15 | Unknown(SqliteError), 16 | } 17 | 18 | pub trait DatabaseConnection { 19 | fn get_connection(&self) -> &Connection; 20 | fn get_mut_connection(&mut self) -> &mut Connection; 21 | } 22 | 23 | pub trait DatabaseManager: Sized { 24 | fn create_tables(&mut self, tables: Vec<&str>) -> Result<(), SqliteError>; 25 | fn store_data(&self, query: &str, params: P) -> Result<(), Error>; 26 | fn remove_data(&self, query: &str, params: P) -> Result<(), Error>; 27 | fn update_data(&self, query: &str, params: P) -> Result<(), Error>; 28 | } 29 | 30 | impl DatabaseManager for T { 31 | /// Creates the database tables if not present. 32 | fn create_tables(&mut self, tables: Vec<&str>) -> Result<(), SqliteError> { 33 | let tx = self.get_mut_connection().transaction().unwrap(); 34 | for table in tables.iter() { 35 | tx.execute(table, [])?; 36 | } 37 | tx.commit() 38 | } 39 | 40 | /// Generic method to store data into the database. 41 | fn store_data(&self, query: &str, params: P) -> Result<(), Error> { 42 | match self.get_connection().execute(query, params) { 43 | Ok(_) => Ok(()), 44 | Err(e) => match e { 45 | SqliteError::SqliteFailure(ie, _) => match ie.code { 46 | ErrorCode::ConstraintViolation => match ie.extended_code { 47 | SQLITE_CONSTRAINT_FOREIGNKEY => Err(Error::MissingForeignKey), 48 | SQLITE_CONSTRAINT_PRIMARYKEY => Err(Error::AlreadyExists), 49 | _ => Err(Error::Unknown(e)), 50 | }, 51 | _ => Err(Error::Unknown(e)), 52 | }, 53 | _ => Err(Error::Unknown(e)), 54 | }, 55 | } 56 | } 57 | 58 | /// Generic method to remove data from the database. 59 | fn remove_data(&self, query: &str, params: P) -> Result<(), Error> { 60 | match self.get_connection().execute(query, params).unwrap() { 61 | 0 => Err(Error::NotFound), 62 | _ => Ok(()), 63 | } 64 | } 65 | 66 | /// Generic method to update data from the database. 67 | fn update_data(&self, query: &str, params: P) -> Result<(), Error> { 68 | // Updating data is fundamentally the same as deleting it in terms of interface. 69 | // A query is sent and either no row is modified or some rows are 70 | self.remove_data(query, params) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /teos-common/src/errors.rs: -------------------------------------------------------------------------------- 1 | /// General errors [1, 32] 2 | pub const MISSING_FIELD: u8 = 1; 3 | pub const EMPTY_FIELD: u8 = 2; 4 | pub const WRONG_FIELD_TYPE: u8 = 3; 5 | pub const WRONG_FIELD_SIZE: u8 = 4; 6 | pub const WRONG_FIELD_FORMAT: u8 = 5; 7 | pub const INVALID_REQUEST_FORMAT: u8 = 6; 8 | pub const INVALID_SIGNATURE_OR_SUBSCRIPTION_ERROR: u8 = 7; 9 | pub const SERVICE_UNAVAILABLE: u8 = 32; 10 | 11 | /// Appointment errors [33, 64] 12 | pub const APPOINTMENT_FIELD_TOO_SMALL: u8 = 33; 13 | pub const APPOINTMENT_FIELD_TOO_BIG: u8 = 34; 14 | pub const APPOINTMENT_ALREADY_TRIGGERED: u8 = 35; 15 | pub const APPOINTMENT_NOT_FOUND: u8 = 36; 16 | 17 | /// Registration errors [65, 96] 18 | pub const REGISTRATION_RESOURCE_EXHAUSTED: u8 = 65; 19 | 20 | /// UNHANDLED 21 | pub const UNEXPECTED_ERROR: u8 = 255; 22 | -------------------------------------------------------------------------------- /teos-common/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The Eye of Satoshi - Lightning watchtower. 2 | //! 3 | //! Functionality shared between users and towers. 4 | 5 | // FIXME: This is a temporary fix. See https://github.com/tokio-rs/prost/issues/661 6 | #[allow(clippy::derive_partial_eq_without_eq)] 7 | pub mod protos { 8 | tonic::include_proto!("common.teos.v2"); 9 | } 10 | 11 | pub mod appointment; 12 | pub mod constants; 13 | pub mod cryptography; 14 | pub mod dbm; 15 | pub mod errors; 16 | pub mod net; 17 | pub mod receipts; 18 | pub mod ser; 19 | pub mod test_utils; 20 | 21 | use std::fmt; 22 | use std::{convert::TryFrom, str::FromStr}; 23 | 24 | use serde::{Deserialize, Serialize}; 25 | use serde_json::json; 26 | 27 | use bitcoin::secp256k1::{Error, PublicKey}; 28 | 29 | pub const USER_ID_LEN: usize = 33; 30 | pub use UserId as TowerId; 31 | 32 | /// User identifier. A wrapper around a [PublicKey]. 33 | #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)] 34 | pub struct UserId(pub PublicKey); 35 | 36 | impl UserId { 37 | /// Encodes the user id in its byte representation. 38 | pub fn to_vec(&self) -> Vec { 39 | self.0.serialize().to_vec() 40 | } 41 | 42 | /// Builds a user id from its byte representation. 43 | pub fn from_slice(data: &[u8]) -> Result { 44 | Ok(UserId(PublicKey::from_slice(data)?)) 45 | } 46 | } 47 | 48 | impl std::str::FromStr for UserId { 49 | type Err = String; 50 | 51 | fn from_str(s: &str) -> Result { 52 | PublicKey::from_str(s) 53 | .map_err(|_| { 54 | "Provided public key does not match expected format (33-byte hex string)".into() 55 | }) 56 | .map(Self) 57 | } 58 | } 59 | 60 | impl fmt::Display for UserId { 61 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 62 | write!(f, "{}", self.0) 63 | } 64 | } 65 | 66 | impl TryFrom for UserId { 67 | type Error = String; 68 | 69 | fn try_from(value: serde_json::Value) -> Result { 70 | match value { 71 | serde_json::Value::String(s) => UserId::from_str(&s), 72 | serde_json::Value::Array(mut a) => { 73 | let param_count = a.len(); 74 | if param_count == 1 { 75 | UserId::try_from(a.pop().unwrap()) 76 | } else { 77 | Err(format!( 78 | "Unexpected json format. Expected a single parameter. Received: {param_count}" 79 | )) 80 | } 81 | } 82 | serde_json::Value::Object(mut m) => { 83 | let param_count = m.len(); 84 | if param_count > 1 { 85 | Err(format!( 86 | "Unexpected json format. Expected a single parameter. Received: {param_count}" 87 | )) 88 | } else { 89 | UserId::try_from(json!(m 90 | .remove("user_id") 91 | .or_else(|| m.remove("tower_id")) 92 | .ok_or("user_id or tower_id not found")?)) 93 | } 94 | } 95 | _ => Err(format!( 96 | "Unexpected request format. Expected: user_id/tower_id. Received: '{value}'" 97 | )), 98 | } 99 | } 100 | } 101 | 102 | #[cfg(test)] 103 | mod tests { 104 | use super::*; 105 | use serde_json::json; 106 | use std::collections::HashMap; 107 | 108 | use crate::test_utils::get_random_user_id; 109 | 110 | #[test] 111 | fn try_from_json_string() { 112 | let user_id = get_random_user_id(); 113 | assert_eq!(UserId::try_from(json!(user_id.to_string())), Ok(user_id)); 114 | } 115 | 116 | #[test] 117 | fn try_from_json_wrong_string() { 118 | let user_id = "not_a_user_id"; 119 | assert!(matches!( 120 | UserId::try_from(json!(user_id.to_string())), 121 | Err(..) 122 | )); 123 | } 124 | 125 | #[test] 126 | fn try_from_json_array() { 127 | let user_id = get_random_user_id(); 128 | assert_eq!(UserId::try_from(json!([user_id.to_string()])), Ok(user_id)); 129 | } 130 | 131 | #[test] 132 | fn try_from_json_array_empty() { 133 | assert!(matches!(UserId::try_from(json!([])), Err(..))); 134 | } 135 | 136 | #[test] 137 | fn try_from_json_array_too_many_elements() { 138 | let user_id = get_random_user_id(); 139 | assert!(matches!( 140 | UserId::try_from(json!([user_id.to_string(), user_id.to_string()])), 141 | Err(..) 142 | )); 143 | } 144 | 145 | #[test] 146 | fn try_from_json_dict() { 147 | let user_id = get_random_user_id(); 148 | assert_eq!( 149 | UserId::try_from(json!(HashMap::from([("tower_id", user_id.to_string())]))), 150 | Ok(user_id) 151 | ); 152 | assert_eq!( 153 | UserId::try_from(json!(HashMap::from([("user_id", user_id.to_string())]))), 154 | Ok(user_id) 155 | ); 156 | } 157 | 158 | #[test] 159 | fn try_from_json_empty_dict() { 160 | assert!(matches!( 161 | UserId::try_from(json!(HashMap::::new())), 162 | Err(..) 163 | )); 164 | } 165 | 166 | #[test] 167 | fn try_from_json_wrong_dict() { 168 | let user_id = get_random_user_id(); 169 | assert!(matches!( 170 | UserId::try_from(json!(HashMap::from([("random_key", user_id.to_string())]))), 171 | Err(..) 172 | )); 173 | } 174 | 175 | #[test] 176 | fn try_from_json_dict_too_many_keys() { 177 | let user_id = get_random_user_id(); 178 | 179 | assert!(matches!( 180 | UserId::try_from(json!(HashMap::from([ 181 | ("tower_id", user_id.to_string()), 182 | ("user_id", user_id.to_string()) 183 | ]))), 184 | Err(..) 185 | )); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /teos-common/src/net/http.rs: -------------------------------------------------------------------------------- 1 | pub enum Endpoint { 2 | Register, 3 | AddAppointment, 4 | GetAppointment, 5 | GetSubscriptionInfo, 6 | Ping, 7 | } 8 | 9 | impl std::fmt::Display for Endpoint { 10 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 11 | write!( 12 | f, 13 | "{}", 14 | match self { 15 | Endpoint::Register => "register", 16 | Endpoint::AddAppointment => "add_appointment", 17 | Endpoint::GetAppointment => "get_appointment", 18 | Endpoint::GetSubscriptionInfo => "get_subscription_info", 19 | Endpoint::Ping => "ping", 20 | } 21 | ) 22 | } 23 | } 24 | 25 | impl Endpoint { 26 | pub fn path(&self) -> String { 27 | format!("/{self}") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /teos-common/src/net/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod http; 2 | 3 | use serde::Serialize; 4 | use std::fmt; 5 | 6 | /// Represents all types of teos network addresses 7 | #[derive(Clone, Serialize, Debug, PartialEq, Eq)] 8 | pub enum AddressType { 9 | IpV4 = 0, 10 | TorV3 = 1, 11 | } 12 | 13 | impl From for AddressType { 14 | fn from(x: i32) -> Self { 15 | match x { 16 | 0 => AddressType::IpV4, 17 | 1 => AddressType::TorV3, 18 | x => panic!("Unknown address type {}", x), 19 | } 20 | } 21 | } 22 | 23 | impl std::str::FromStr for AddressType { 24 | type Err = String; 25 | 26 | fn from_str(s: &str) -> Result { 27 | match s { 28 | "ipv4" => Ok(AddressType::IpV4), 29 | "torv3" => Ok(AddressType::TorV3), 30 | _ => Err(format!("Unknown type: {s}")), 31 | } 32 | } 33 | } 34 | 35 | impl fmt::Display for AddressType { 36 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 37 | let s = match self { 38 | AddressType::IpV4 => "ipv4", 39 | AddressType::TorV3 => "torv3", 40 | }; 41 | write!(f, "{s}") 42 | } 43 | } 44 | 45 | impl AddressType { 46 | pub fn get_type(net_addr: &str) -> AddressType { 47 | if net_addr.contains(".onion:") { 48 | AddressType::TorV3 49 | } else { 50 | AddressType::IpV4 51 | } 52 | } 53 | 54 | pub fn is_tor(&self) -> bool { 55 | self == &AddressType::TorV3 56 | } 57 | 58 | pub fn is_clearnet(&self) -> bool { 59 | self == &AddressType::IpV4 60 | } 61 | } 62 | 63 | #[derive(Clone, Serialize, Debug, PartialEq, Eq)] 64 | pub struct NetAddr { 65 | net_addr: String, 66 | #[serde(skip)] 67 | addr_type: AddressType, 68 | } 69 | 70 | impl NetAddr { 71 | pub fn new(net_addr: String) -> Self { 72 | NetAddr { 73 | addr_type: AddressType::get_type(&net_addr), 74 | net_addr, 75 | } 76 | } 77 | 78 | pub fn net_addr(&self) -> &str { 79 | &self.net_addr 80 | } 81 | 82 | pub fn addr_type(&self) -> &AddressType { 83 | &self.addr_type 84 | } 85 | 86 | pub fn is_onion(&self) -> bool { 87 | self.addr_type().is_tor() 88 | } 89 | } 90 | 91 | #[cfg(test)] 92 | pub mod tests { 93 | use super::*; 94 | 95 | pub const TORV3_ADDR: &str = 96 | "recnedb7xfhzjdrcgxongzli3a6qyrv5jwgowoho3v5g3rwk7kkglrid.onion:9814"; 97 | pub const IPV4_ADDR: &str = "teos.talaia.watch:9814"; 98 | 99 | #[test] 100 | fn test_get_type() { 101 | assert_eq!(AddressType::get_type(TORV3_ADDR), AddressType::TorV3); 102 | assert_eq!(AddressType::get_type(IPV4_ADDR), AddressType::IpV4); 103 | } 104 | 105 | #[test] 106 | fn test_is_tor() { 107 | assert!(NetAddr::new(TORV3_ADDR.to_owned()).addr_type.is_tor()); 108 | assert!(!NetAddr::new(IPV4_ADDR.to_owned()).addr_type.is_tor()); 109 | } 110 | 111 | #[test] 112 | fn test_is_clearnet() { 113 | assert!(!NetAddr::new(TORV3_ADDR.to_owned()).addr_type.is_clearnet()); 114 | assert!(NetAddr::new(IPV4_ADDR.to_owned()).addr_type.is_clearnet()); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /teos-common/src/receipts.rs: -------------------------------------------------------------------------------- 1 | //! Receipts issued by towers and handed to users as commitment proof. 2 | 3 | use serde::Serialize; 4 | 5 | use bitcoin::secp256k1::SecretKey; 6 | 7 | use crate::{cryptography, UserId}; 8 | 9 | /// Proof that a user has registered with a tower. This serves two purposes: 10 | /// 11 | /// - First, the user is able to prove that the tower agreed on providing a service. If a tower refuses to accept appointments 12 | /// from a user (claiming the subscription has expired) but the expiry time has still not passed and the tower cannot 13 | /// provide the relevant appointments signed by the user, it means it is cheating. 14 | /// - Second, it serves as proof, alongside an appointment receipt, that an appointment was not fulfilled. A registration receipt 15 | /// specifies a subscription period (`subscription_start` - `subscription_expiry`) and the appointment a `start_block` so inclusion 16 | /// can be proved. 17 | /// 18 | /// TODO: / DISCUSS: In order to minimize the amount of receipts the user has to store, the tower could batch subscription receipts 19 | /// 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 20 | /// before the tower wipes their data, then the tower can create a new receipt with (S, E') for E' > E instead of a second receipt (E, E'). 21 | // Notice this only applies as long as there is no gap between the two subscriptions. 22 | #[derive(Serialize, Debug, Eq, PartialEq, Clone)] 23 | pub struct RegistrationReceipt { 24 | user_id: UserId, 25 | available_slots: u32, 26 | subscription_start: u32, 27 | subscription_expiry: u32, 28 | #[serde(rename = "subscription_signature")] 29 | signature: Option, 30 | } 31 | 32 | impl RegistrationReceipt { 33 | pub fn new( 34 | user_id: UserId, 35 | available_slots: u32, 36 | subscription_start: u32, 37 | subscription_expiry: u32, 38 | ) -> Self { 39 | RegistrationReceipt { 40 | user_id, 41 | available_slots, 42 | subscription_start, 43 | subscription_expiry, 44 | signature: None, 45 | } 46 | } 47 | 48 | pub fn with_signature( 49 | user_id: UserId, 50 | available_slots: u32, 51 | subscription_start: u32, 52 | subscription_expiry: u32, 53 | signature: String, 54 | ) -> Self { 55 | RegistrationReceipt { 56 | user_id, 57 | available_slots, 58 | subscription_start, 59 | subscription_expiry, 60 | signature: Some(signature), 61 | } 62 | } 63 | 64 | pub fn user_id(&self) -> UserId { 65 | self.user_id 66 | } 67 | 68 | pub fn available_slots(&self) -> u32 { 69 | self.available_slots 70 | } 71 | 72 | pub fn subscription_start(&self) -> u32 { 73 | self.subscription_start 74 | } 75 | 76 | pub fn subscription_expiry(&self) -> u32 { 77 | self.subscription_expiry 78 | } 79 | 80 | pub fn signature(&self) -> Option { 81 | self.signature.clone() 82 | } 83 | 84 | pub fn to_vec(&self) -> Vec { 85 | let mut ser = Vec::new(); 86 | ser.extend_from_slice(&self.user_id.to_vec()); 87 | ser.extend_from_slice(&self.available_slots.to_be_bytes()); 88 | ser.extend_from_slice(&self.subscription_start.to_be_bytes()); 89 | ser.extend_from_slice(&self.subscription_expiry.to_be_bytes()); 90 | 91 | ser 92 | } 93 | 94 | pub fn sign(&mut self, sk: &SecretKey) { 95 | self.signature = Some(cryptography::sign(&self.to_vec(), sk)); 96 | } 97 | 98 | pub fn verify(&self, id: &UserId) -> bool { 99 | if let Some(signature) = self.signature() { 100 | cryptography::verify(&self.to_vec(), &signature, &id.0) 101 | } else { 102 | false 103 | } 104 | } 105 | } 106 | 107 | /// Proof that a certain state was backed up with the tower. 108 | /// 109 | /// Appointment receipts can be used alongside a registration receipt that covers it, and on chain data (a breach not being reacted with a penalty), to prove a tower has not reacted to a channel breach. 110 | #[derive(Debug, Clone, PartialEq, Eq, Serialize)] 111 | pub struct AppointmentReceipt { 112 | user_signature: String, 113 | start_block: u32, 114 | signature: Option, 115 | } 116 | 117 | impl AppointmentReceipt { 118 | pub fn new(user_signature: String, start_block: u32) -> Self { 119 | AppointmentReceipt { 120 | user_signature, 121 | start_block, 122 | signature: None, 123 | } 124 | } 125 | 126 | pub fn with_signature(user_signature: String, start_block: u32, signature: String) -> Self { 127 | AppointmentReceipt { 128 | user_signature, 129 | start_block, 130 | signature: Some(signature), 131 | } 132 | } 133 | 134 | pub fn user_signature(&self) -> &str { 135 | &self.user_signature 136 | } 137 | 138 | pub fn start_block(&self) -> u32 { 139 | self.start_block 140 | } 141 | 142 | pub fn signature(&self) -> Option { 143 | self.signature.clone() 144 | } 145 | 146 | pub fn to_vec(&self) -> Vec { 147 | let mut ser = Vec::new(); 148 | ser.extend_from_slice(self.user_signature.as_bytes()); 149 | ser.extend_from_slice(&self.start_block.to_be_bytes()); 150 | 151 | ser 152 | } 153 | 154 | pub fn sign(&mut self, sk: &SecretKey) { 155 | self.signature = Some(cryptography::sign(&self.to_vec(), sk)); 156 | } 157 | 158 | pub fn verify(&self, id: &UserId) -> bool { 159 | if let Some(signature) = self.signature() { 160 | cryptography::verify(&self.to_vec(), &signature, &id.0) 161 | } else { 162 | false 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /teos-common/src/ser.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use serde::{ser::SerializeSeq, Serializer}; 4 | 5 | use crate::appointment::Locator; 6 | 7 | pub fn serialize_locators(hs: &HashSet, s: S) -> Result 8 | where 9 | S: Serializer, 10 | { 11 | let mut seq = s.serialize_seq(Some(hs.len()))?; 12 | for element in hs.iter() { 13 | seq.serialize_element(&hex::encode(element))?; 14 | } 15 | seq.end() 16 | } 17 | 18 | pub mod serde_be { 19 | use super::*; 20 | use serde::de::{self, Deserializer}; 21 | 22 | pub fn serialize(v: &[u8], s: S) -> Result 23 | where 24 | S: Serializer, 25 | { 26 | let mut v = v.to_owned(); 27 | v.reverse(); 28 | hex::serialize(v, s) 29 | } 30 | 31 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 32 | where 33 | D: Deserializer<'de>, 34 | { 35 | struct BEVisitor; 36 | 37 | impl<'de> de::Visitor<'de> for BEVisitor { 38 | type Value = Vec; 39 | 40 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 41 | formatter.write_str("a hex encoded string") 42 | } 43 | 44 | fn visit_str(self, v: &str) -> Result 45 | where 46 | E: de::Error, 47 | { 48 | let mut v = 49 | hex::decode(v).map_err(|_| E::custom("cannot deserialize the given value"))?; 50 | v.reverse(); 51 | Ok(v) 52 | } 53 | } 54 | 55 | deserializer.deserialize_any(BEVisitor) 56 | } 57 | } 58 | 59 | pub mod serde_vec_bytes { 60 | use super::*; 61 | use serde::de::{self, Deserializer, SeqAccess}; 62 | 63 | pub fn serialize(v: &[Vec], s: S) -> Result 64 | where 65 | S: Serializer, 66 | { 67 | let mut seq = s.serialize_seq(Some(v.len()))?; 68 | for element in v.iter() { 69 | seq.serialize_element(&hex::encode(element))?; 70 | } 71 | seq.end() 72 | } 73 | 74 | pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> 75 | where 76 | D: Deserializer<'de>, 77 | { 78 | struct VecVisitor; 79 | 80 | impl<'de> de::Visitor<'de> for VecVisitor { 81 | type Value = Vec>; 82 | 83 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 84 | formatter.write_str("a hex encoded string") 85 | } 86 | 87 | fn visit_seq(self, mut seq: A) -> Result 88 | where 89 | A: SeqAccess<'de>, 90 | { 91 | let mut result = Vec::new(); 92 | while let Some(v) = seq.next_element::()? { 93 | result 94 | .push(hex::decode(v).map_err(|_| { 95 | de::Error::custom("cannot deserialize the given value") 96 | })?); 97 | } 98 | 99 | Ok(result) 100 | } 101 | } 102 | 103 | deserializer.deserialize_any(VecVisitor) 104 | } 105 | } 106 | 107 | pub mod serde_status { 108 | use super::*; 109 | use serde::de::{self, Deserializer}; 110 | use std::str::FromStr; 111 | 112 | use crate::appointment::AppointmentStatus; 113 | 114 | pub fn serialize(status: &i32, serializer: S) -> Result 115 | where 116 | S: Serializer, 117 | { 118 | serializer.serialize_str(&AppointmentStatus::from(*status).to_string()) 119 | } 120 | 121 | pub fn deserialize<'de, D>(deserializer: D) -> Result 122 | where 123 | D: Deserializer<'de>, 124 | { 125 | struct StatusVisitor; 126 | 127 | impl<'de> de::Visitor<'de> for StatusVisitor { 128 | type Value = i32; 129 | 130 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 131 | formatter.write_str("a string containing the status") 132 | } 133 | 134 | fn visit_str(self, v: &str) -> Result 135 | where 136 | E: de::Error, 137 | { 138 | let status = AppointmentStatus::from_str(v) 139 | .map_err(|_| E::custom("given status is unknown"))?; 140 | Ok(status as i32) 141 | } 142 | } 143 | 144 | deserializer.deserialize_any(StatusVisitor) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /teos-common/src/test_utils.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use bitcoin::script::PushBytesBuf; 4 | use hex::FromHex; 5 | use rand::distributions::Standard; 6 | use rand::prelude::Distribution; 7 | use rand::Rng; 8 | 9 | use bitcoin::hashes::Hash; 10 | use bitcoin::secp256k1::SecretKey; 11 | use bitcoin::{consensus, Amount, ScriptBuf, Transaction, TxOut, Txid}; 12 | 13 | use crate::appointment::{Appointment, Locator}; 14 | use crate::cryptography; 15 | use crate::receipts::{AppointmentReceipt, RegistrationReceipt}; 16 | use crate::UserId; 17 | 18 | pub static TXID_HEX: &str = "338bda693c4a26e0d41a01f7f2887aaf48bf0bdf93e6415c9110b29349349d3e"; 19 | pub static TX_HEX: &str = "010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff54038e830a1b4d696e656420627920416e74506f6f6c373432c2005b005e7a0ae3fabe6d6d7841cd582ead8ea5dd8e3de1173cae6fcd2a53c7362ebb7fb6f815604fe07cbe0200000000000000ac0e060005f90000ffffffff04d9476026000000001976a91411dbe48cc6b617f9c6adaf4d9ed5f625b1c7cb5988ac0000000000000000266a24aa21a9ed7248c6efddd8d99bfddd7f499f0b915bffa8253003cc934df1ff14a81301e2340000000000000000266a24b9e11b6d7054937e13f39529d6ad7e685e9dd4efa426f247d5f5a5bed58cdddb2d0fa60100000000000000002b6a2952534b424c4f434b3a054a68aa5368740e8b3e3c67bce45619c2cfd07d4d4f0936a5612d2d0034fa0a0120000000000000000000000000000000000000000000000000000000000000000000000000"; 20 | 21 | pub fn get_random_int() -> T 22 | where 23 | Standard: Distribution, 24 | { 25 | let mut rng = rand::thread_rng(); 26 | rng.gen() 27 | } 28 | 29 | pub fn get_random_user_id() -> UserId { 30 | let (_, pk) = cryptography::get_random_keypair(); 31 | 32 | UserId(pk) 33 | } 34 | 35 | pub fn get_random_locator() -> Locator { 36 | let mut rng = rand::thread_rng(); 37 | 38 | Locator::from_slice(&rng.gen::<[u8; 16]>()).unwrap() 39 | } 40 | 41 | pub fn generate_random_appointment(dispute_txid: Option<&Txid>) -> Appointment { 42 | let dispute_txid = match dispute_txid { 43 | Some(l) => *l, 44 | None => { 45 | let prev_txid_bytes = cryptography::get_random_bytes(32); 46 | Txid::from_slice(&prev_txid_bytes).unwrap() 47 | } 48 | }; 49 | 50 | let tx_bytes = Vec::from_hex(TX_HEX).unwrap(); 51 | let mut penalty_tx: Transaction = consensus::deserialize(&tx_bytes).unwrap(); 52 | let size = get_random_int::() % 81; 53 | let mut push_bytes_buf = PushBytesBuf::new(); 54 | PushBytesBuf::extend_from_slice(&mut push_bytes_buf, &cryptography::get_random_bytes(size)) 55 | .unwrap(); 56 | let script_pubkey = ScriptBuf::new_op_return(push_bytes_buf); 57 | 58 | // Append a random-sized OP_RETURN to make each transcation random in size. 59 | penalty_tx.output.push(TxOut { 60 | value: Amount::from_sat(0), 61 | script_pubkey, 62 | }); 63 | 64 | let mut raw_locator: [u8; 16] = cryptography::get_random_bytes(16).try_into().unwrap(); 65 | raw_locator.copy_from_slice(&dispute_txid[..16]); 66 | let locator = Locator::from_slice(&raw_locator).unwrap(); 67 | 68 | let encrypted_blob = cryptography::encrypt(&penalty_tx, &dispute_txid).unwrap(); 69 | Appointment::new(locator, encrypted_blob, get_random_int()) 70 | } 71 | 72 | pub fn get_random_registration_receipt() -> RegistrationReceipt { 73 | let (sk, _) = cryptography::get_random_keypair(); 74 | let start = get_random_int(); 75 | let mut receipt = 76 | RegistrationReceipt::new(get_random_user_id(), get_random_int(), start, start + 420); 77 | receipt.sign(&sk); 78 | 79 | receipt 80 | } 81 | 82 | pub fn get_registration_receipt_from_previous(r: &RegistrationReceipt) -> RegistrationReceipt { 83 | let (sk, _) = cryptography::get_random_keypair(); 84 | let mut receipt = RegistrationReceipt::new( 85 | r.user_id(), 86 | r.available_slots() + 1 + get_random_int::() as u32, 87 | r.subscription_start(), 88 | r.subscription_expiry() + 1 + get_random_int::() as u32, 89 | ); 90 | receipt.sign(&sk); 91 | 92 | receipt 93 | } 94 | 95 | pub fn get_random_appointment_receipt(tower_sk: SecretKey) -> AppointmentReceipt { 96 | let mut receipt = AppointmentReceipt::new("user_sig".into(), 42); 97 | receipt.sign(&tower_sk); 98 | 99 | receipt 100 | } 101 | -------------------------------------------------------------------------------- /teos/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "teos" 3 | version = "0.2.0" 4 | authors = ["Sergi Delgado Segura "] 5 | license = "MIT" 6 | edition = "2021" 7 | default-run="teosd" 8 | 9 | [[bin]] 10 | name = "teos-cli" 11 | path = "src/cli.rs" 12 | 13 | [[bin]] 14 | name = "teosd" 15 | path = "src/main.rs" 16 | 17 | [dependencies] 18 | # General 19 | hex = { version = "0.4.3", features = [ "serde" ] } 20 | home = "0.5.3" 21 | log = "0.4" 22 | prost = "0.12" 23 | rcgen = { version = "0.13.1", features = ["pem", "x509-parser"] } 24 | rusqlite = { version = "0.26.0", features = [ "bundled", "limits" ] } 25 | serde = "1.0.130" 26 | serde_json = "1.0" 27 | simple_logger = "2.1.0" 28 | structopt = "0.3" 29 | toml = "0.5" 30 | tonic = { version = "0.11", features = [ "tls", "transport" ] } 31 | tokio = { version = "1.5", features = [ "rt-multi-thread" ] } 32 | triggered = "0.1.2" 33 | warp = "0.3.5" 34 | torut = "0.2.1" 35 | base64 = "0.22.1" 36 | 37 | # Bitcoin and Lightning 38 | bitcoin = { version = "0.32.0" } 39 | bitcoincore-rpc = "0.19.0" 40 | lightning = "0.1.0" 41 | lightning-net-tokio = "0.1.0" 42 | lightning-block-sync = { version = "0.1.0", features = [ "rpc-client" ] } 43 | 44 | # Local 45 | teos-common = { path = "../teos-common" } 46 | 47 | [build-dependencies] 48 | tonic-build = "0.11" 49 | 50 | [dev-dependencies] 51 | jsonrpc-http-server = "17.1.0" 52 | rand = "0.8.4" 53 | tempdir = "0.3.7" 54 | tokio-stream = { version = "0.1.5", features = [ "net" ] } 55 | -------------------------------------------------------------------------------- /teos/build.rs: -------------------------------------------------------------------------------- 1 | fn main() -> Result<(), Box> { 2 | tonic_build::configure() 3 | .extern_path(".common.teos.v2", "::teos-common::protos") 4 | .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") 5 | .field_attribute("user_id", "#[serde(with = \"hex::serde\")]") 6 | .field_attribute("tower_id", "#[serde(with = \"hex::serde\")]") 7 | .field_attribute( 8 | "user_ids", 9 | "#[serde(serialize_with = \"teos_common::ser::serde_vec_bytes::serialize\")]", 10 | ) 11 | .field_attribute( 12 | "GetUserResponse.appointments", 13 | "#[serde(serialize_with = \"teos_common::ser::serde_vec_bytes::serialize\")]", 14 | ) 15 | .field_attribute( 16 | "NetworkAddress.address_type", 17 | "#[serde(rename = \"type\", with = \"crate::api::serde::serde_address_type\")]", 18 | ) 19 | .compile( 20 | &[ 21 | "proto/teos/v2/appointment.proto", 22 | "proto/teos/v2/tower_services.proto", 23 | "proto/teos/v2/user.proto", 24 | ], 25 | &["proto/teos/v2", "../teos-common/proto/"], 26 | )?; 27 | 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /teos/proto/teos/v2/appointment.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package teos.v2; 3 | 4 | import "common/teos/v2/appointment.proto"; 5 | 6 | message GetAppointmentsRequest { 7 | // Request the information of appointments with specific locator. 8 | 9 | bytes locator = 1; 10 | } 11 | 12 | message GetAppointmentsResponse { 13 | // Response with the information of all appointments with a specific locator. 14 | 15 | repeated common.teos.v2.AppointmentData appointments = 1; 16 | } 17 | 18 | message GetAllAppointmentsResponse { 19 | // Response with data about all the appointments in the tower. 20 | 21 | repeated common.teos.v2.AppointmentData appointments = 1; 22 | } -------------------------------------------------------------------------------- /teos/proto/teos/v2/tower_services.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package teos.v2; 3 | 4 | import "appointment.proto"; 5 | import "user.proto"; 6 | import "common/teos/v2/appointment.proto"; 7 | import "common/teos/v2/user.proto"; 8 | import "google/protobuf/empty.proto"; 9 | 10 | message NetworkAddress { 11 | // Tower public API endpoint. 12 | enum AddressType { 13 | IpV4 = 0; 14 | TorV3 = 1; 15 | } 16 | AddressType address_type = 1; 17 | string address = 2; 18 | uint32 port = 3; 19 | 20 | } 21 | 22 | message GetTowerInfoResponse { 23 | // Response with information about the tower. 24 | bytes tower_id = 1; 25 | uint32 n_registered_users = 2; 26 | uint32 n_watcher_appointments = 3; 27 | uint32 n_responder_trackers = 4; 28 | bool bitcoind_reachable = 5; 29 | repeated NetworkAddress addresses = 6; 30 | } 31 | 32 | service PublicTowerServices { 33 | // Public tower services, only reachable from the public API. 34 | 35 | rpc register(common.teos.v2.RegisterRequest) returns (common.teos.v2.RegisterResponse) {} 36 | rpc add_appointment(common.teos.v2.AddAppointmentRequest) returns (common.teos.v2.AddAppointmentResponse) {} 37 | rpc get_appointment(common.teos.v2.GetAppointmentRequest) returns (common.teos.v2.GetAppointmentResponse) {} 38 | rpc get_subscription_info(common.teos.v2.GetSubscriptionInfoRequest) returns (common.teos.v2.GetSubscriptionInfoResponse) {} 39 | } 40 | 41 | service PrivateTowerServices { 42 | // Private tower services, only reachable from the private API. 43 | 44 | rpc get_all_appointments(google.protobuf.Empty) returns (GetAllAppointmentsResponse) {} 45 | rpc get_appointments(GetAppointmentsRequest) returns (GetAppointmentsResponse) {} 46 | rpc get_tower_info(google.protobuf.Empty) returns (GetTowerInfoResponse) {} 47 | rpc get_users(google.protobuf.Empty) returns (GetUsersResponse) {} 48 | rpc get_user(GetUserRequest) returns (GetUserResponse) {} 49 | rpc stop(google.protobuf.Empty) returns (google.protobuf.Empty) {} 50 | } -------------------------------------------------------------------------------- /teos/proto/teos/v2/user.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package teos.v2; 3 | 4 | message GetUserRequest { 5 | // Request to get information about a specific user. Contains the user id. 6 | 7 | bytes user_id = 1; 8 | } 9 | 10 | message GetUserResponse { 11 | // Response with the information the tower has about a specific user 12 | 13 | uint32 available_slots = 1; 14 | uint32 subscription_expiry = 2; 15 | repeated bytes appointments = 3; 16 | } 17 | 18 | message GetUsersResponse { 19 | // Response with information about all the users registered with the tower. Contains a list of user ids. 20 | 21 | repeated bytes user_ids = 1; 22 | } -------------------------------------------------------------------------------- /teos/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod http; 2 | pub mod internal; 3 | pub mod serde; 4 | pub mod tor; 5 | -------------------------------------------------------------------------------- /teos/src/api/serde.rs: -------------------------------------------------------------------------------- 1 | use crate::protos as msgs; 2 | 3 | use teos_common::net::AddressType; 4 | 5 | impl msgs::NetworkAddress { 6 | pub fn from_ipv4(address: String, port: u16) -> Self { 7 | Self { 8 | address_type: AddressType::IpV4 as i32, 9 | address, 10 | port: port as u32, 11 | } 12 | } 13 | 14 | pub fn from_torv3(address: String, port: u16) -> Self { 15 | Self { 16 | address_type: AddressType::TorV3 as i32, 17 | address, 18 | port: port as u32, 19 | } 20 | } 21 | } 22 | 23 | pub mod serde_address_type { 24 | use serde::de::{self, Deserializer}; 25 | use serde::Serializer; 26 | use std::str::FromStr; 27 | 28 | use super::AddressType; 29 | 30 | pub fn serialize(status: &i32, serializer: S) -> Result 31 | where 32 | S: Serializer, 33 | { 34 | serializer.serialize_str(&AddressType::from(*status).to_string()) 35 | } 36 | 37 | pub fn deserialize<'de, D>(deserializer: D) -> Result 38 | where 39 | D: Deserializer<'de>, 40 | { 41 | struct StatusVisitor; 42 | 43 | impl<'de> de::Visitor<'de> for StatusVisitor { 44 | type Value = i32; 45 | 46 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 47 | formatter.write_str("a string containing the address type") 48 | } 49 | 50 | fn visit_str(self, v: &str) -> Result 51 | where 52 | E: de::Error, 53 | { 54 | let status = AddressType::from_str(v) 55 | .map_err(|_| E::custom("given address type is unknown"))?; 56 | Ok(status as i32) 57 | } 58 | } 59 | 60 | deserializer.deserialize_any(StatusVisitor) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /teos/src/api/tor.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | use std::io::{Error, ErrorKind}; 3 | use std::net::SocketAddr; 4 | use std::path::PathBuf; 5 | 6 | use tokio::fs; 7 | use tokio::net::TcpStream; 8 | use torut::control::UnauthenticatedConn; 9 | use torut::onion::TorSecretKeyV3; 10 | use triggered::{Listener, Trigger}; 11 | 12 | pub struct TorAPI { 13 | sk: TorSecretKeyV3, 14 | api_endpoint: SocketAddr, 15 | onion_port: u16, 16 | tor_control_port: u16, 17 | } 18 | 19 | impl TorAPI { 20 | pub async fn new( 21 | api_endpoint: SocketAddr, 22 | onion_port: u16, 23 | tor_control_port: u16, 24 | path: PathBuf, 25 | ) -> Self { 26 | let key = if let Some(key) = TorAPI::load_sk(path.clone()).await { 27 | key 28 | } else { 29 | log::info!("Generating fresh Tor secret key"); 30 | let key = TorSecretKeyV3::generate(); 31 | TorAPI::store_sk(&key, path).await; 32 | key 33 | }; 34 | 35 | Self { 36 | sk: key, 37 | api_endpoint, 38 | onion_port, 39 | tor_control_port, 40 | } 41 | } 42 | 43 | pub fn get_onion_address(&self) -> String { 44 | self.sk.public().get_onion_address().to_string() 45 | } 46 | 47 | /// Loads a Tor key from disk (if found). 48 | async fn load_sk(path: PathBuf) -> Option { 49 | log::info!("Loading Tor secret key from disk"); 50 | let key = fs::read(path.join("onion_v3_sk")) 51 | .await 52 | .map_err(|e| log::warn!("Tor secret key cannot be loaded. {e}")) 53 | .ok()?; 54 | let key: [u8; 64] = key 55 | .try_into() 56 | .map_err(|_| log::error!("Cannot convert loaded data into Tor secret key")) 57 | .ok()?; 58 | 59 | Some(TorSecretKeyV3::from(key)) 60 | } 61 | 62 | /// Stores a Tor key to disk. 63 | async fn store_sk(key: &TorSecretKeyV3, path: PathBuf) { 64 | if let Err(e) = fs::write(path.join("onion_v3_sk"), key.as_bytes()).await { 65 | log::error!("Cannot store Tor secret key. {e}"); 66 | } 67 | } 68 | 69 | /// Tries to connect to the Tor control port 70 | async fn connect_tor_cp(&self) -> Result { 71 | let sock = TcpStream::connect(format!("127.0.0.1:{}", self.tor_control_port)) 72 | .await 73 | .map_err(|_| { 74 | Error::new( 75 | ErrorKind::ConnectionRefused, 76 | "failed to connect to Tor control port", 77 | ) 78 | })?; 79 | Ok(sock) 80 | } 81 | 82 | /// Expose an onion service that re-directs to the public api. 83 | pub async fn expose_onion_service( 84 | &self, 85 | service_ready: Trigger, 86 | shutdown_signal_tor: Listener, 87 | ) -> Result<(), Error> { 88 | let stream = self 89 | .connect_tor_cp() 90 | .await 91 | .map_err(|e| Error::new(ErrorKind::ConnectionRefused, e))?; 92 | 93 | let mut unauth_conn = UnauthenticatedConn::new(stream); 94 | 95 | let pre_auth = unauth_conn 96 | .load_protocol_info() 97 | .await 98 | .map_err(|e| Error::new(ErrorKind::ConnectionRefused, e))?; 99 | 100 | let auth_data = pre_auth 101 | .make_auth_data()? 102 | .expect("failed to make auth data"); 103 | 104 | unauth_conn.authenticate(&auth_data).await.map_err(|_| { 105 | Error::new( 106 | ErrorKind::PermissionDenied, 107 | "failed to authenticate with Tor", 108 | ) 109 | })?; 110 | 111 | let mut auth_conn = unauth_conn.into_authenticated().await; 112 | 113 | auth_conn.set_async_event_handler(Some(|_| async move { Ok(()) })); 114 | 115 | auth_conn 116 | .add_onion_v3( 117 | &self.sk, 118 | false, 119 | false, 120 | false, 121 | None, 122 | &mut [(self.onion_port, self.api_endpoint)].iter(), 123 | ) 124 | .await 125 | .map_err(|e| { 126 | Error::new( 127 | ErrorKind::Other, 128 | format!("failed to create onion hidden service: {e}"), 129 | ) 130 | })?; 131 | 132 | log::info!( 133 | "Onion service: {}:{}", 134 | self.get_onion_address(), 135 | self.onion_port 136 | ); 137 | service_ready.trigger(); 138 | shutdown_signal_tor.await; 139 | 140 | auth_conn 141 | .del_onion( 142 | &self 143 | .sk 144 | .public() 145 | .get_onion_address() 146 | .get_address_without_dot_onion(), 147 | ) 148 | .await 149 | .unwrap(); 150 | Ok(()) 151 | } 152 | } 153 | 154 | #[cfg(test)] 155 | mod tests { 156 | use super::*; 157 | use tempdir::TempDir; 158 | 159 | use teos_common::test_utils::get_random_user_id; 160 | 161 | #[tokio::test] 162 | async fn test_store_load_sk() { 163 | let key = TorSecretKeyV3::generate(); 164 | let tmp_path = TempDir::new(&format!("data_dir_{}", get_random_user_id())).unwrap(); 165 | 166 | TorAPI::store_sk(&key, tmp_path.path().into()).await; 167 | let loaded_key = TorAPI::load_sk(tmp_path.path().into()).await; 168 | 169 | assert_eq!(key, loaded_key.unwrap()) 170 | } 171 | 172 | #[tokio::test] 173 | async fn test_load_sk_inexistent() { 174 | let tmp_path = TempDir::new(&format!("data_dir_{}", get_random_user_id())).unwrap(); 175 | let loaded_key = TorAPI::load_sk(tmp_path.path().into()).await; 176 | 177 | assert_eq!(loaded_key, None); 178 | } 179 | 180 | #[tokio::test] 181 | async fn test_load_sk_wrong_format() { 182 | let tmp_path = TempDir::new(&format!("data_dir_{}", get_random_user_id())).unwrap(); 183 | fs::write(tmp_path.path().join("onion_v3_sk"), "random stuff") 184 | .await 185 | .unwrap(); 186 | let loaded_key = TorAPI::load_sk(tmp_path.path().into()).await; 187 | 188 | assert_eq!(loaded_key, None); 189 | } 190 | 191 | #[tokio::test] 192 | async fn test_connect_tor_cp_fail() { 193 | let wrong_cp = 9000; 194 | let tmp_path = TempDir::new(&format!("data_dir_{}", get_random_user_id())).unwrap(); 195 | let tor_api = TorAPI::new( 196 | "127.0.1.1:9814".parse().unwrap(), 197 | 9814, 198 | wrong_cp, 199 | tmp_path.path().into(), 200 | ) 201 | .await; 202 | 203 | match tor_api.connect_tor_cp().await { 204 | Ok(_) => {} 205 | Err(e) => { 206 | assert_eq!("failed to connect to Tor control port", e.to_string()) 207 | } 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /teos/src/bitcoin_cli.rs: -------------------------------------------------------------------------------- 1 | //! Logic related to the BitcoindClient, an simple bitcoind client implementation. 2 | 3 | // This is an adaptation of a bitcoind client with the minimal functionality required by the tower 4 | // The original piece of software can be found at https://github.com/lightningdevkit/ldk-sample/blob/main/src/bitcoind_client.rs 5 | 6 | /* This file is licensed under either of 7 | * Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) or 8 | * MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT) 9 | * at your option. 10 | */ 11 | 12 | use base64::{engine::general_purpose::URL_SAFE as BASE64, Engine}; 13 | use std::convert::TryInto; 14 | use std::io::{Error, ErrorKind}; 15 | use std::sync::Arc; 16 | use tokio::sync::Mutex; 17 | 18 | use bitcoin::hash_types::{BlockHash, Txid}; 19 | use bitcoin::Transaction; 20 | use bitcoincore_rpc::{Auth, RawTx}; 21 | use lightning::util::ser::Writeable; 22 | use lightning_block_sync::http::{HttpEndpoint, JsonResponse}; 23 | use lightning_block_sync::rpc::RpcClient; 24 | use lightning_block_sync::{AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource}; 25 | 26 | /// A simple implementation of a bitcoind client (`bitcoin-cli`) with the minimal functionality required by the tower. 27 | pub struct BitcoindClient<'a> { 28 | /// The underlying RPC client. 29 | bitcoind_rpc_client: Arc>, 30 | /// The hostname to connect to. 31 | host: &'a str, 32 | /// The port to connect to. 33 | port: u16, 34 | /// The RPC user `bitcoind` is configured with. 35 | rpc_user: String, 36 | /// The RPC password for the given user. 37 | rpc_password: String, 38 | } 39 | 40 | impl BlockSource for &BitcoindClient<'_> { 41 | /// Gets a block header given its hash. 42 | fn get_header<'a>( 43 | &'a self, 44 | header_hash: &'a BlockHash, 45 | height_hint: Option, 46 | ) -> AsyncBlockSourceResult<'a, BlockHeaderData> { 47 | Box::pin(async move { 48 | let rpc = self.bitcoind_rpc_client.lock().await; 49 | rpc.get_header(header_hash, height_hint).await 50 | }) 51 | } 52 | 53 | /// Gets a block given its hash. 54 | fn get_block<'a>( 55 | &'a self, 56 | header_hash: &'a BlockHash, 57 | ) -> AsyncBlockSourceResult<'a, BlockData> { 58 | Box::pin(async move { 59 | let rpc = self.bitcoind_rpc_client.lock().await; 60 | rpc.get_block(header_hash).await 61 | }) 62 | } 63 | 64 | /// Get the best block known by our node. 65 | fn get_best_block(&self) -> AsyncBlockSourceResult<(BlockHash, Option)> { 66 | Box::pin(async move { 67 | let rpc = self.bitcoind_rpc_client.lock().await; 68 | rpc.get_best_block().await 69 | }) 70 | } 71 | } 72 | 73 | // TODO: This is not being used atm since we're using bitcoincore-rpc. 74 | // Not deleting it since wd should need it once both get merged. 75 | impl<'a> BitcoindClient<'a> { 76 | /// Creates a new [BitcoindClient] instance. 77 | pub async fn new( 78 | host: &'a str, 79 | port: u16, 80 | auth: Auth, 81 | teos_network: &'a str, 82 | ) -> std::io::Result> { 83 | let http_endpoint = HttpEndpoint::for_host(host.to_owned()).with_port(port); 84 | let (rpc_user, rpc_password) = { 85 | let (user, pass) = auth.get_user_pass().map_err(|e| { 86 | Error::new( 87 | ErrorKind::InvalidInput, 88 | format!("Cannot read cookie file. {}", e), 89 | ) 90 | })?; 91 | if user.is_none() { 92 | Err(Error::new( 93 | ErrorKind::InvalidInput, 94 | "Empty btc_rpc_user parsed from rpc_cookie".to_string(), 95 | )) 96 | } else if pass.is_none() { 97 | Err(Error::new( 98 | ErrorKind::InvalidInput, 99 | "Empty btc_rpc_password parsed from rpc_cookie", 100 | )) 101 | } else { 102 | Ok((user.unwrap(), pass.unwrap())) 103 | } 104 | }?; 105 | 106 | let rpc_credentials = BASE64.encode(format!("{}:{}", rpc_user, rpc_password)); 107 | let bitcoind_rpc_client = RpcClient::new(&rpc_credentials, http_endpoint); 108 | 109 | let client = Self { 110 | bitcoind_rpc_client: Arc::new(Mutex::new(bitcoind_rpc_client)), 111 | host, 112 | port, 113 | rpc_user, 114 | rpc_password, 115 | }; 116 | 117 | // Test that bitcoind is reachable. 118 | let btc_network = client.get_chain().await?; 119 | 120 | // Assert teos runs on the same chain/network as bitcoind. 121 | if btc_network != teos_network { 122 | Err(Error::new( 123 | ErrorKind::InvalidInput, 124 | format!("bitcoind is running on {btc_network} but teosd is set to run on {teos_network}"), 125 | )) 126 | } else { 127 | Ok(client) 128 | } 129 | } 130 | 131 | /// Gets a fresh RPC client. 132 | pub fn get_new_rpc_client(&self) -> RpcClient { 133 | let http_endpoint = HttpEndpoint::for_host(self.host.to_owned()).with_port(self.port); 134 | let rpc_credentials = BASE64.encode(format!("{}:{}", self.rpc_user, self.rpc_password)); 135 | 136 | RpcClient::new(&rpc_credentials, http_endpoint) 137 | } 138 | 139 | /// Gets the hash of the chain tip and its height. 140 | pub async fn get_best_block_hash_and_height( 141 | &self, 142 | ) -> Result<(BlockHash, Option), std::io::Error> { 143 | let rpc = self.bitcoind_rpc_client.lock().await; 144 | rpc.call_method::<(BlockHash, Option)>("getblockchaininfo", &[]) 145 | .await 146 | } 147 | 148 | /// Sends a transaction to the network. 149 | pub async fn send_raw_transaction(&self, raw_tx: &Transaction) -> Result { 150 | let rpc = self.bitcoind_rpc_client.lock().await; 151 | 152 | let raw_tx_json = serde_json::json!(raw_tx.encode().raw_hex()); 153 | rpc.call_method::("sendrawtransaction", &[raw_tx_json]) 154 | .await 155 | } 156 | 157 | /// Gets a transaction given its id. 158 | pub async fn get_raw_transaction(&self, txid: &Txid) -> Result { 159 | let rpc = self.bitcoind_rpc_client.lock().await; 160 | 161 | let txid_hex = serde_json::json!(txid.encode().raw_hex()); 162 | rpc.call_method::("getrawtransaction", &[txid_hex]) 163 | .await 164 | } 165 | 166 | /// Gets bitcoind's network. 167 | pub async fn get_chain(&self) -> std::io::Result { 168 | // A wrapper type to extract "chain" key from getblockchaininfo JsonResponse. 169 | struct BtcNetwork(String); 170 | impl TryInto for JsonResponse { 171 | type Error = std::io::Error; 172 | fn try_into(self) -> std::io::Result { 173 | Ok(BtcNetwork(self.0["chain"].as_str().unwrap().to_string())) 174 | } 175 | } 176 | 177 | // Ask the RPC client for the network bitcoind is running on. 178 | let rpc = self.bitcoind_rpc_client.lock().await; 179 | let btc_network = rpc 180 | .call_method::("getblockchaininfo", &[]) 181 | .await?; 182 | 183 | Ok(btc_network.0) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /teos/src/chain_monitor.rs: -------------------------------------------------------------------------------- 1 | //! Logic related to the ChainMonitor, the component in charge of querying block data from `bitcoind`. 2 | //! 3 | 4 | use std::ops::Deref; 5 | use std::sync::{Arc, Condvar, Mutex}; 6 | use std::time; 7 | use tokio::time::timeout; 8 | use triggered::Listener; 9 | 10 | use lightning::chain; 11 | use lightning_block_sync::poll::{ChainTip, Poll, ValidatedBlockHeader}; 12 | use lightning_block_sync::{BlockSourceErrorKind, Cache, SpvClient}; 13 | 14 | use crate::dbm::DBM; 15 | 16 | /// Component in charge of monitoring the chain for new blocks. 17 | /// 18 | /// Takes care of polling `bitcoind` for new tips and hand it to subscribers. 19 | /// It is mainly a wrapper around [chain::Listen] that provides some logging. 20 | pub struct ChainMonitor<'a, P, C, L> 21 | where 22 | P: Poll, 23 | C: Cache, 24 | L: Deref, 25 | L::Target: chain::Listen, 26 | { 27 | /// A bitcoin client to poll best tips from. 28 | spv_client: SpvClient<'a, P, C, L>, 29 | /// The last known block header by the [ChainMonitor]. 30 | last_known_block_header: ValidatedBlockHeader, 31 | /// A [DBM] (database manager) instance. Used to persist block data into disk. 32 | dbm: Arc>, 33 | /// The time between polls. 34 | polling_delta: time::Duration, 35 | /// A signal from the main thread indicating the tower is shuting down. 36 | shutdown_signal: Listener, 37 | /// A flag that indicates wether bitcoind is reachable or not. 38 | bitcoind_reachable: Arc<(Mutex, Condvar)>, 39 | } 40 | 41 | impl<'a, P, C, L> ChainMonitor<'a, P, C, L> 42 | where 43 | P: Poll, 44 | C: Cache, 45 | L: Deref, 46 | L::Target: chain::Listen, 47 | { 48 | /// Creates a new [ChainMonitor] instance. 49 | pub async fn new( 50 | spv_client: SpvClient<'a, P, C, L>, 51 | last_known_block_header: ValidatedBlockHeader, 52 | dbm: Arc>, 53 | polling_delta_sec: u16, 54 | shutdown_signal: Listener, 55 | bitcoind_reachable: Arc<(Mutex, Condvar)>, 56 | ) -> ChainMonitor<'a, P, C, L> { 57 | ChainMonitor { 58 | spv_client, 59 | last_known_block_header, 60 | dbm, 61 | polling_delta: time::Duration::from_secs(polling_delta_sec as u64), 62 | shutdown_signal, 63 | bitcoind_reachable, 64 | } 65 | } 66 | 67 | /// Polls the best chain tip from bitcoind. Serves the data to its listeners (through [chain::Listen]) and logs data about the polled tips. 68 | pub async fn poll_best_tip(&mut self) { 69 | let (reachable, notifier) = &*self.bitcoind_reachable; 70 | match self.spv_client.poll_best_tip().await { 71 | Ok((chain_tip, _)) => { 72 | match chain_tip { 73 | ChainTip::Common => log::debug!("No new best tip found"), 74 | 75 | ChainTip::Better(new_best) => { 76 | log::debug!("Updating best tip: {}", new_best.header.block_hash()); 77 | self.last_known_block_header = new_best; 78 | self.dbm 79 | .lock() 80 | .unwrap() 81 | .store_last_known_block(&new_best.header.block_hash()) 82 | .unwrap(); 83 | } 84 | ChainTip::Worse(worse) => { 85 | // This would happen both if a block has less chainwork than the previous one, or if it has the same chainwork 86 | // but it forks from the parent. In both cases, it'll be detected as a reorg once (if) the new chain grows past 87 | // the current tip. 88 | log::warn!("Worse tip found: {:?}", worse.header.block_hash()); 89 | 90 | if worse.chainwork == self.last_known_block_header.chainwork { 91 | log::warn!("New tip has the same work as the previous one") 92 | } else { 93 | log::warn!("New tip has less work than the previous one") 94 | } 95 | } 96 | } 97 | *reachable.lock().unwrap() = true; 98 | notifier.notify_all(); 99 | } 100 | Err(e) => match e.kind() { 101 | BlockSourceErrorKind::Persistent => { 102 | // FIXME: This may need finer catching 103 | log::error!("Unexpected persistent error: {e:?}"); 104 | } 105 | BlockSourceErrorKind::Transient => { 106 | // Treating all transient as connection errors at least for now. 107 | log::error!("Connection lost with bitcoind"); 108 | *reachable.lock().unwrap() = false; 109 | } 110 | }, 111 | }; 112 | } 113 | 114 | /// Monitors `bitcoind` polling the best chain tip every [polling_delta](Self::polling_delta). 115 | pub async fn monitor_chain(&mut self) { 116 | loop { 117 | self.poll_best_tip().await; 118 | // Sleep for self.polling_delta seconds or shutdown if the signal is received. 119 | if timeout(self.polling_delta, self.shutdown_signal.clone()) 120 | .await 121 | .is_ok() 122 | { 123 | log::debug!("Received shutting down signal. Shutting down"); 124 | break; 125 | } 126 | } 127 | } 128 | } 129 | 130 | #[cfg(test)] 131 | mod tests { 132 | use super::*; 133 | use std::cell::RefCell; 134 | use std::collections::HashSet; 135 | use std::iter::FromIterator; 136 | use std::thread; 137 | 138 | use bitcoin::BlockHash; 139 | use bitcoin::Network; 140 | use lightning_block_sync::{poll::ChainPoller, SpvClient, UnboundedCache}; 141 | 142 | use crate::test_utils::{Blockchain, START_HEIGHT}; 143 | 144 | pub(crate) struct DummyListener { 145 | pub connected_blocks: RefCell>, 146 | pub disconnected_blocks: RefCell>, 147 | } 148 | 149 | impl DummyListener { 150 | fn new() -> Self { 151 | Self { 152 | connected_blocks: RefCell::new(HashSet::new()), 153 | disconnected_blocks: RefCell::new(HashSet::new()), 154 | } 155 | } 156 | } 157 | 158 | impl chain::Listen for DummyListener { 159 | fn filtered_block_connected( 160 | &self, 161 | header: &bitcoin::block::Header, 162 | _: &chain::transaction::TransactionData, 163 | _: u32, 164 | ) { 165 | self.connected_blocks 166 | .borrow_mut() 167 | .insert(header.block_hash()); 168 | } 169 | 170 | fn block_disconnected(&self, header: &bitcoin::block::Header, _: u32) { 171 | self.disconnected_blocks 172 | .borrow_mut() 173 | .insert(header.block_hash()); 174 | } 175 | } 176 | 177 | #[tokio::test] 178 | async fn test_poll_best_tip_common() { 179 | let mut chain = Blockchain::default().with_height(START_HEIGHT); 180 | let tip = chain.tip(); 181 | 182 | let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); 183 | let (_, shutdown_signal) = triggered::trigger(); 184 | let listener = DummyListener::new(); 185 | 186 | let poller = ChainPoller::new(&mut chain, Network::Bitcoin); 187 | let cache = &mut UnboundedCache::new(); 188 | let spv_client = SpvClient::new(tip, poller, cache, &listener); 189 | let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); 190 | 191 | let mut cm = 192 | ChainMonitor::new(spv_client, tip, dbm, 1, shutdown_signal, bitcoind_reachable).await; 193 | 194 | // If there's no new block nothing gets connected nor disconnected 195 | cm.poll_best_tip().await; 196 | assert!(listener.connected_blocks.borrow().is_empty()); 197 | assert!(listener.disconnected_blocks.borrow().is_empty()); 198 | } 199 | 200 | #[tokio::test] 201 | async fn test_poll_best_tip_better() { 202 | let mut chain = Blockchain::default().with_height(START_HEIGHT); 203 | let new_tip = chain.tip(); 204 | let old_tip = chain.at_height(START_HEIGHT - 1); 205 | 206 | let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); 207 | let (_, shutdown_signal) = triggered::trigger(); 208 | let listener = DummyListener::new(); 209 | 210 | let poller = ChainPoller::new(&mut chain, Network::Bitcoin); 211 | let cache = &mut UnboundedCache::new(); 212 | let spv_client = SpvClient::new(old_tip, poller, cache, &listener); 213 | let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); 214 | 215 | let mut cm = ChainMonitor::new( 216 | spv_client, 217 | old_tip, 218 | dbm, 219 | 1, 220 | shutdown_signal, 221 | bitcoind_reachable, 222 | ) 223 | .await; 224 | 225 | // If a new (best) block gets mined, it should be connected 226 | cm.poll_best_tip().await; 227 | assert_eq!(cm.last_known_block_header, new_tip); 228 | assert_eq!( 229 | cm.dbm.lock().unwrap().load_last_known_block().unwrap(), 230 | new_tip.deref().header.block_hash() 231 | ); 232 | assert!(listener 233 | .connected_blocks 234 | .borrow() 235 | .contains(&new_tip.deref().header.block_hash())); 236 | assert!(listener.disconnected_blocks.borrow().is_empty()); 237 | } 238 | 239 | #[tokio::test] 240 | async fn test_poll_best_tip_worse() { 241 | let mut chain = Blockchain::default().with_height(START_HEIGHT); 242 | let best_tip = chain.tip(); 243 | chain.disconnect_tip(); 244 | 245 | let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); 246 | let (_, shutdown_signal) = triggered::trigger(); 247 | let listener = DummyListener::new(); 248 | 249 | let poller = ChainPoller::new(&mut chain, Network::Bitcoin); 250 | let cache = &mut UnboundedCache::new(); 251 | let spv_client = SpvClient::new(best_tip, poller, cache, &listener); 252 | let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); 253 | 254 | let mut cm = ChainMonitor::new( 255 | spv_client, 256 | best_tip, 257 | dbm, 258 | 1, 259 | shutdown_signal, 260 | bitcoind_reachable, 261 | ) 262 | .await; 263 | 264 | // If a new (worse, just one) block gets mined, nothing gets connected nor disconnected 265 | cm.poll_best_tip().await; 266 | assert_eq!(cm.last_known_block_header, best_tip); 267 | assert!(cm.dbm.lock().unwrap().load_last_known_block().is_none()); 268 | assert!(listener.connected_blocks.borrow().is_empty()); 269 | assert!(listener.disconnected_blocks.borrow().is_empty()); 270 | } 271 | 272 | #[tokio::test] 273 | async fn test_poll_best_tip_reorg() { 274 | let mut chain = Blockchain::default().with_height(START_HEIGHT); 275 | let old_best = chain.tip(); 276 | // Reorg 277 | chain.disconnect_tip(); 278 | let new_blocks = (0..2) 279 | .map(|_| chain.generate(None).block_hash()) 280 | .collect::>(); 281 | 282 | let new_best = chain.tip(); 283 | 284 | let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); 285 | let (_, shutdown_signal) = triggered::trigger(); 286 | let listener = DummyListener::new(); 287 | 288 | let poller = ChainPoller::new(&mut chain, Network::Bitcoin); 289 | let cache = &mut UnboundedCache::new(); 290 | let spv_client = SpvClient::new(old_best, poller, cache, &listener); 291 | let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); 292 | 293 | let mut cm = ChainMonitor::new( 294 | spv_client, 295 | old_best, 296 | dbm, 297 | 1, 298 | shutdown_signal, 299 | bitcoind_reachable, 300 | ) 301 | .await; 302 | 303 | // If a a reorg is found (tip is disconnected and a new best is found), both data should be connected and disconnected 304 | cm.poll_best_tip().await; 305 | assert_eq!(cm.last_known_block_header, new_best); 306 | assert_eq!( 307 | cm.dbm.lock().unwrap().load_last_known_block().unwrap(), 308 | new_best.deref().header.block_hash() 309 | ); 310 | assert_eq!(*listener.connected_blocks.borrow(), new_blocks); 311 | assert_eq!( 312 | *listener.disconnected_blocks.borrow(), 313 | HashSet::from_iter([old_best.deref().header.block_hash()]) 314 | ); 315 | } 316 | 317 | #[tokio::test] 318 | async fn test_poll_best_tip_bitcoind_unreachable() { 319 | let mut chain = Blockchain::default().unreachable(); 320 | let chain_offline = chain.unreachable.clone(); 321 | let tip = chain.tip(); 322 | 323 | let dbm = Arc::new(Mutex::new(DBM::in_memory().unwrap())); 324 | let (_, shutdown_signal) = triggered::trigger(); 325 | let listener = DummyListener::new(); 326 | 327 | let poller = ChainPoller::new(&mut chain, Network::Bitcoin); 328 | let cache = &mut UnboundedCache::new(); 329 | let spv_client = SpvClient::new(tip, poller, cache, &listener); 330 | let bitcoind_reachable = Arc::new((Mutex::new(true), Condvar::new())); 331 | 332 | let mut cm = ChainMonitor::new( 333 | spv_client, 334 | tip, 335 | dbm, 336 | 1, 337 | shutdown_signal, 338 | bitcoind_reachable.clone(), 339 | ) 340 | .await; 341 | 342 | // Our block source was defined as unreachable (bitcoind is off). Check that the unreachable flag is set after polling. 343 | cm.poll_best_tip().await; 344 | let (reachable, _) = &*bitcoind_reachable.clone(); 345 | assert!(!*reachable.lock().unwrap()); 346 | 347 | // Set a thread to block on bitcoind unreachable to check that it gets notified once bitcoind comes back online 348 | let t = thread::spawn(move || { 349 | let (lock, notifier) = &*bitcoind_reachable; 350 | let mut reachable = lock.lock().unwrap(); 351 | while !*reachable { 352 | reachable = notifier.wait(reachable).unwrap(); 353 | } 354 | }); 355 | 356 | // Set bitcoind as reachable again and check back 357 | *chain_offline.lock().unwrap() = false; 358 | cm.poll_best_tip().await; 359 | assert!(*reachable.lock().unwrap()); 360 | 361 | // This would hang if the cm didn't notify their subscribers about the bitcoind status, so it serves as out assert. 362 | t.join().unwrap(); 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /teos/src/cli.rs: -------------------------------------------------------------------------------- 1 | use hex::FromHex; 2 | use serde_json::to_string_pretty as pretty_json; 3 | use std::str::FromStr; 4 | use structopt::StructOpt; 5 | use tokio::fs; 6 | use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; 7 | use tonic::Request; 8 | 9 | use teos::cli_config::{Command, Config, Opt}; 10 | use teos::config; 11 | use teos::protos as msgs; 12 | use teos::protos::private_tower_services_client::PrivateTowerServicesClient; 13 | use teos_common::appointment::Locator; 14 | use teos_common::UserId; 15 | 16 | /// Prints the cli error to standard error and exits the process 17 | fn handle_error(error: T) { 18 | eprintln!("{}", error); 19 | std::process::exit(1); 20 | } 21 | 22 | #[tokio::main] 23 | async fn main() { 24 | let opt = Opt::from_args(); 25 | let path = config::data_dir_absolute_path(opt.data_dir.clone()); 26 | 27 | // Create data dir if it does not exist 28 | fs::create_dir_all(&path).await.unwrap_or_else(|e| { 29 | eprintln!("Cannot create data dir: {e:?}"); 30 | std::process::exit(1); 31 | }); 32 | 33 | let command = opt.command.clone(); 34 | 35 | // Load conf (from file or defaults) and patch it with the command line parameters received (if any) 36 | let mut conf = config::from_file::(&path.join("teos.toml")); 37 | conf.patch_with_options(opt); 38 | 39 | let key = fs::read(&path.join("client-key.pem")) 40 | .await 41 | .expect("unable to read client key from disk"); 42 | let certificate = fs::read(path.join("client.pem")) 43 | .await 44 | .expect("unable to read client cert from disk"); 45 | let ca_cert = Certificate::from_pem( 46 | fs::read(path.join("ca.pem")) 47 | .await 48 | .expect("unable to read ca cert from disk"), 49 | ); 50 | 51 | let tls = ClientTlsConfig::new() 52 | .domain_name("localhost") 53 | .ca_certificate(ca_cert) 54 | .identity(Identity::from_pem(certificate, key)); 55 | 56 | let channel = Channel::from_shared(format!("https://{}:{}", conf.rpc_bind, conf.rpc_port)) 57 | .expect("Cannot create channel from endpoint") 58 | .tls_config(tls) 59 | .unwrap_or_else(|e| { 60 | eprintln!("Could not configure tls: {e:?}"); 61 | std::process::exit(1); 62 | }) 63 | .connect() 64 | .await 65 | .unwrap_or_else(|_| { 66 | eprintln!("Could not connect to tower. Is teosd running?"); 67 | std::process::exit(1); 68 | }); 69 | 70 | let mut client = PrivateTowerServicesClient::new(channel); 71 | 72 | match command { 73 | Command::GetAllAppointments => { 74 | let appointments = client.get_all_appointments(Request::new(())).await.unwrap(); 75 | println!("{}", pretty_json(&appointments.into_inner()).unwrap()); 76 | } 77 | Command::GetAppointments(appointments_data) => { 78 | match Locator::from_hex(&appointments_data.locator) { 79 | Ok(locator) => { 80 | match client 81 | .get_appointments(Request::new(msgs::GetAppointmentsRequest { 82 | locator: locator.to_vec(), 83 | })) 84 | .await 85 | { 86 | Ok(appointments) => { 87 | println!("{}", pretty_json(&appointments.into_inner()).unwrap()) 88 | } 89 | Err(status) => handle_error(status.message()), 90 | } 91 | } 92 | Err(e) => handle_error(e), 93 | }; 94 | } 95 | Command::GetTowerInfo => { 96 | let info = client.get_tower_info(Request::new(())).await.unwrap(); 97 | println!("{}", pretty_json(&info.into_inner()).unwrap()) 98 | } 99 | Command::GetUsers => { 100 | let users = client.get_users(Request::new(())).await.unwrap(); 101 | println!("{}", pretty_json(&users.into_inner()).unwrap()); 102 | } 103 | Command::GetUser(user) => { 104 | match UserId::from_str(&user.user_id) { 105 | Ok(user_id) => { 106 | match client 107 | .get_user(Request::new(msgs::GetUserRequest { 108 | user_id: user_id.to_vec(), 109 | })) 110 | .await 111 | { 112 | Ok(response) => { 113 | println!("{}", pretty_json(&response.into_inner()).unwrap()) 114 | } 115 | Err(status) => handle_error(status.message()), 116 | } 117 | } 118 | Err(e) => handle_error(e), 119 | }; 120 | } 121 | Command::Stop => { 122 | println!("Shutting down tower"); 123 | client.stop(Request::new(())).await.unwrap(); 124 | } 125 | }; 126 | } 127 | -------------------------------------------------------------------------------- /teos/src/cli_config.rs: -------------------------------------------------------------------------------- 1 | //! Logic related to the tower CLI configuration and command line parameter parsing. 2 | 3 | use serde::Deserialize; 4 | use structopt::StructOpt; 5 | 6 | #[derive(Debug, StructOpt, Clone)] 7 | #[structopt(rename_all = "lower_case")] 8 | pub enum Command { 9 | /// Gets information about all appointments stored in the tower 10 | GetAllAppointments, 11 | /// Gets information about specific appointments stored in the tower using a locator 12 | GetAppointments(GetAppointmentsData), 13 | /// Gets generic information about the tower, like tower id and aggregate data on users and appointments 14 | GetTowerInfo, 15 | /// Gets an array with the user ids of all the users registered to the tower 16 | GetUsers, 17 | /// Gets information about a specific user 18 | GetUser(GetUserData), 19 | /// Requests a graceful shutdown of the tower 20 | Stop, 21 | } 22 | 23 | #[derive(Debug, StructOpt, Clone)] 24 | #[structopt(rename_all = "snake_case")] 25 | pub struct GetUserData { 26 | /// The user identifier (33-byte compressed public key). 27 | pub user_id: String, 28 | } 29 | 30 | #[derive(Debug, StructOpt, Clone)] 31 | pub struct GetAppointmentsData { 32 | /// The locator of the appointments (16-byte hexadecimal string). 33 | pub locator: String, 34 | } 35 | 36 | /// Holds all the command line options and commands. 37 | #[derive(StructOpt, Debug)] 38 | #[structopt(rename_all = "lowercase")] 39 | #[structopt( 40 | version = env!("CARGO_PKG_VERSION"), 41 | about = "The Eye of Satoshi - CLI", 42 | name = "teos-cli" 43 | )] 44 | pub struct Opt { 45 | /// Address teos RPC server is bind to [default: localhost] 46 | #[structopt(long)] 47 | pub rpc_bind: Option, 48 | 49 | /// Port teos RPC server is bind to [default: 8814] 50 | #[structopt(long)] 51 | pub rpc_port: Option, 52 | 53 | /// Specify data directory 54 | #[structopt(long, default_value = "~/.teos")] 55 | pub data_dir: String, 56 | 57 | /// Command 58 | #[structopt(subcommand)] 59 | pub command: Command, 60 | } 61 | 62 | /// Holds all configuration options. 63 | /// 64 | /// The overwrite policy goes, from less to more: 65 | /// - Defaults 66 | /// - Configuration file 67 | /// - Command line options 68 | #[derive(Debug, Deserialize, Clone, PartialEq, Eq)] 69 | #[serde(default)] 70 | pub struct Config { 71 | pub rpc_bind: String, 72 | pub rpc_port: u16, 73 | } 74 | 75 | impl Config { 76 | /// Patches the configuration options with the command line options. 77 | pub fn patch_with_options(&mut self, options: Opt) { 78 | if options.rpc_bind.is_some() { 79 | self.rpc_bind = options.rpc_bind.unwrap(); 80 | } 81 | if options.rpc_port.is_some() { 82 | self.rpc_port = options.rpc_port.unwrap(); 83 | } 84 | } 85 | } 86 | 87 | impl Default for Config { 88 | /// Sets the tower [Config] defaults. 89 | /// 90 | /// Notice the defaults are not enough, and the tower will refuse to run on them. 91 | /// For instance, the defaults do set the `bitcoind` `rpu_user` and `rpc_password` 92 | /// to empty strings so the user is forced the set them (and most importantly so the 93 | /// user does not use any values provided here). 94 | fn default() -> Self { 95 | Self { 96 | rpc_bind: "localhost".into(), 97 | rpc_port: 8814, 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /teos/src/conf_template.toml: -------------------------------------------------------------------------------- 1 | # API 2 | api_bind = "127.0.0.1" 3 | api_port = 9814 4 | tor_control_port = 9051 5 | onion_hidden_service_port = 9814 6 | tor_support = false 7 | 8 | # RPC 9 | rpc_bind = "127.0.0.1" 10 | rpc_port = 8814 11 | 12 | # bitcoind 13 | btc_network = "mainnet" 14 | btc_rpc_user = "CSW" 15 | ## Notice only user+password **OR** cookie is allowed as rpc auth, any other combination would be rejected 16 | btc_rpc_password = "NotSatoshi" 17 | btc_rpc_connect = "localhost" 18 | btc_rpc_cookie = "~/.bitcoin/.cookie" 19 | btc_rpc_port = 8332 20 | 21 | # Flags 22 | debug = false 23 | deps_debug = false 24 | overwrite_key = false 25 | 26 | # General 27 | subscription_slots = 10000 28 | subscription_duration = 4320 29 | expiry_delta = 6 30 | min_to_self_delay = 20 31 | polling_delta = 60 32 | 33 | # Internal API 34 | internal_api_bind = "127.0.0.1" 35 | internal_api_port = 50051 -------------------------------------------------------------------------------- /teos/src/config.rs: -------------------------------------------------------------------------------- 1 | //! Logic related to the tower configuration and command line parameter parsing. 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use std::path::PathBuf; 5 | use structopt::StructOpt; 6 | 7 | pub fn data_dir_absolute_path(data_dir: String) -> PathBuf { 8 | if let Some(a) = data_dir.strip_prefix('~') { 9 | if let Some(b) = data_dir.strip_prefix("~/") { 10 | home::home_dir().unwrap().join(b) 11 | } else { 12 | home::home_dir().unwrap().join(a) 13 | } 14 | } else { 15 | PathBuf::from(&data_dir) 16 | } 17 | } 18 | 19 | pub fn from_file(path: &PathBuf) -> T { 20 | match std::fs::read(path) { 21 | Ok(file_content) => toml::from_slice::(&file_content).unwrap_or_else(|e| { 22 | eprintln!("Couldn't parse config file: {e}"); 23 | T::default() 24 | }), 25 | Err(_) => T::default(), 26 | } 27 | } 28 | 29 | /// Error raised if something is wrong with the configuration. 30 | #[derive(PartialEq, Eq, Debug)] 31 | pub struct ConfigError(String); 32 | 33 | impl std::fmt::Display for ConfigError { 34 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 35 | write!(f, "Configuration error: {}", self.0) 36 | } 37 | } 38 | 39 | impl std::error::Error for ConfigError {} 40 | 41 | #[derive(PartialEq)] 42 | pub enum AuthMethod { 43 | UserPass, 44 | CookieFile, 45 | Multiple, 46 | Invalid, 47 | } 48 | 49 | /// Holds all the command line options. 50 | #[derive(StructOpt, Debug, Clone)] 51 | #[structopt(rename_all = "lowercase")] 52 | #[structopt(version = env!("CARGO_PKG_VERSION"), about = "The Eye of Satoshi - Lightning watchtower")] 53 | pub struct Opt { 54 | /// Address teos HTTP(s) API will bind to [default: localhost] 55 | #[structopt(long)] 56 | pub api_bind: Option, 57 | 58 | /// Port teos HTTP(s) API will bind to [default: 9814] 59 | #[structopt(long)] 60 | pub api_port: Option, 61 | 62 | /// Address teos RPC server will bind to [default: localhost] 63 | #[structopt(long)] 64 | pub rpc_bind: Option, 65 | 66 | /// Port teos RPC server will bind to [default: 8814] 67 | #[structopt(long)] 68 | pub rpc_port: Option, 69 | 70 | /// Network bitcoind is connected to. Either mainnet, testnet, signet or regtest [default: mainnet] 71 | #[structopt(long)] 72 | pub btc_network: Option, 73 | 74 | /// bitcoind rpcuser 75 | #[structopt(long)] 76 | pub btc_rpc_user: Option, 77 | 78 | /// bitcoind rpcpassword 79 | #[structopt(long)] 80 | pub btc_rpc_password: Option, 81 | 82 | /// bitcoind rpccookie 83 | #[structopt(long)] 84 | pub btc_rpc_cookie: Option, 85 | 86 | /// bitcoind rpcconnect [default: localhost] 87 | #[structopt(long)] 88 | pub btc_rpc_connect: Option, 89 | 90 | /// bitcoind rpcport [default: 8332] 91 | #[structopt(long)] 92 | pub btc_rpc_port: Option, 93 | 94 | /// Specify data directory 95 | #[structopt(long, default_value = "~/.teos")] 96 | pub data_dir: String, 97 | 98 | /// Runs teos in debug mode 99 | #[structopt(long)] 100 | pub debug: bool, 101 | 102 | /// Runs third party libs in debug mode 103 | #[structopt(long)] 104 | pub deps_debug: bool, 105 | 106 | /// Overwrites the tower secret key. THIS IS IRREVERSIBLE AND WILL CHANGE YOUR TOWER ID 107 | #[structopt(long)] 108 | pub overwrite_key: bool, 109 | 110 | /// If set, creates a Tor endpoint to serve API data. This endpoint is additional to the clearnet HTTP API 111 | #[structopt(long)] 112 | pub tor_support: bool, 113 | 114 | /// Forces the tower to run even if the underlying chain has gone too far out of sync. This can only happen 115 | /// if the node is being run in pruned mode. 116 | #[structopt(long)] 117 | pub force_update: bool, 118 | 119 | /// Tor control port [default: 9051] 120 | #[structopt(long)] 121 | pub tor_control_port: Option, 122 | 123 | /// Port for the onion hidden service to listen on [default: 9814] 124 | #[structopt(long)] 125 | pub onion_hidden_service_port: Option, 126 | } 127 | 128 | /// Holds all configuration options. 129 | /// 130 | /// The overwrite policy goes, from less to more: 131 | /// - Defaults 132 | /// - Configuration file 133 | /// - Command line options 134 | #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] 135 | #[serde(default)] 136 | pub struct Config { 137 | // API 138 | pub api_bind: String, 139 | pub api_port: u16, 140 | 141 | // RPC 142 | pub rpc_bind: String, 143 | pub rpc_port: u16, 144 | 145 | // Bitcoind 146 | pub btc_network: String, 147 | pub btc_rpc_user: String, 148 | pub btc_rpc_cookie: String, 149 | pub btc_rpc_password: String, 150 | pub btc_rpc_connect: String, 151 | pub btc_rpc_port: u16, 152 | 153 | // Flags 154 | pub debug: bool, 155 | pub deps_debug: bool, 156 | pub overwrite_key: bool, 157 | pub force_update: bool, 158 | 159 | // General 160 | pub subscription_slots: u32, 161 | pub subscription_duration: u32, 162 | pub expiry_delta: u32, 163 | pub min_to_self_delay: u16, 164 | pub polling_delta: u16, 165 | 166 | // Internal API 167 | pub internal_api_bind: String, 168 | pub internal_api_port: u32, 169 | 170 | // Tor 171 | pub tor_support: bool, 172 | pub tor_control_port: u16, 173 | pub onion_hidden_service_port: u16, 174 | } 175 | 176 | impl Config { 177 | /// The only combinations of valid authentication methods are: 178 | /// - User **AND** password 179 | /// - **OR** Cookie file 180 | // 181 | /// Any other combination will be rejected 182 | pub fn get_auth_method(&self) -> AuthMethod { 183 | match ( 184 | self.btc_rpc_user.is_empty(), 185 | self.btc_rpc_password.is_empty(), 186 | self.btc_rpc_cookie.is_empty(), 187 | ) { 188 | (false, false, true) => AuthMethod::UserPass, 189 | (true, true, false) => AuthMethod::CookieFile, 190 | (true, true, true) => AuthMethod::Invalid, 191 | _ => AuthMethod::Multiple, 192 | } 193 | } 194 | 195 | /// Patches the configuration options with the command line options. 196 | pub fn patch_with_options(&mut self, options: Opt) { 197 | if options.api_bind.is_some() { 198 | self.api_bind = options.api_bind.unwrap(); 199 | } 200 | if options.api_port.is_some() { 201 | self.api_port = options.api_port.unwrap(); 202 | } 203 | if options.rpc_bind.is_some() { 204 | self.rpc_bind = options.rpc_bind.unwrap(); 205 | } 206 | if options.rpc_port.is_some() { 207 | self.rpc_port = options.rpc_port.unwrap(); 208 | } 209 | if options.btc_network.is_some() { 210 | self.btc_network = options.btc_network.unwrap(); 211 | } 212 | if options.btc_rpc_user.is_some() { 213 | self.btc_rpc_user = options.btc_rpc_user.unwrap(); 214 | } 215 | if options.btc_rpc_password.is_some() { 216 | self.btc_rpc_password = options.btc_rpc_password.unwrap(); 217 | } 218 | if options.btc_rpc_cookie.is_some() { 219 | self.btc_rpc_cookie = options.btc_rpc_cookie.unwrap(); 220 | } 221 | if options.btc_rpc_connect.is_some() { 222 | self.btc_rpc_connect = options.btc_rpc_connect.unwrap(); 223 | } 224 | if options.btc_rpc_port.is_some() { 225 | self.btc_rpc_port = options.btc_rpc_port.unwrap(); 226 | } 227 | if options.tor_control_port.is_some() { 228 | self.tor_control_port = options.tor_control_port.unwrap(); 229 | } 230 | if options.onion_hidden_service_port.is_some() { 231 | self.onion_hidden_service_port = options.onion_hidden_service_port.unwrap(); 232 | } 233 | 234 | self.tor_support |= options.tor_support; 235 | self.debug |= options.debug; 236 | self.deps_debug |= options.deps_debug; 237 | self.overwrite_key = options.overwrite_key; 238 | self.force_update = options.force_update; 239 | } 240 | 241 | /// Verifies that [Config] is properly built. 242 | /// 243 | /// This includes: 244 | /// - `bitcoind` credentials have been set 245 | /// - The Bitcoin network has been properly set (to either bitcoin, testnet, signet or regtest) 246 | /// 247 | /// This will also assign the default `btc_rpc_port` depending on the network if it has not 248 | /// been overwritten at this point. 249 | pub fn verify(&mut self) -> Result<(), ConfigError> { 250 | let auth_method = self.get_auth_method(); 251 | if auth_method == AuthMethod::Invalid { 252 | return Err(ConfigError("No valid bitcoind auth provided. Set either both btc_rpc_user/btc_rpc_password or btc_rpc_cookie".to_owned())); 253 | } else if auth_method == AuthMethod::Multiple { 254 | return Err(ConfigError( 255 | "Multiple bitcoind auth provided. Pick a single one (either btc_rpc_user/btc_rpc_password or btc_rpc_cookie)" 256 | .to_owned(), 257 | )); 258 | } 259 | 260 | // Normalize the network option to the ones used by bitcoind. 261 | if ["mainnet", "testnet"].contains(&self.btc_network.as_str()) { 262 | self.btc_network = self.btc_network.trim_end_matches("net").into(); 263 | } 264 | 265 | let default_rpc_port = match self.btc_network.as_str() { 266 | "main" => 8332, 267 | "test" => 18332, 268 | "regtest" => 18443, 269 | "signet" => 38332, 270 | _ => return Err(ConfigError(format!("btc_network not recognized. Expected {{mainnet, testnet, signet, regtest}}, received {}", self.btc_network))) 271 | }; 272 | 273 | // Set the port to it's default (depending on the network) if it has not been 274 | // overwritten at this point. 275 | if self.btc_rpc_port == 0 { 276 | self.btc_rpc_port = default_rpc_port; 277 | } 278 | 279 | Ok(()) 280 | } 281 | 282 | /// Checks whether the config has been set with only with default values. 283 | pub fn is_default(&self) -> bool { 284 | self == &Config::default() 285 | } 286 | 287 | /// Logs non-default options. 288 | pub fn log_non_default_options(&self) { 289 | let json_default_config = serde_json::json!(&Config::default()); 290 | let json_config = serde_json::json!(&self); 291 | let sensitive_args = ["btc_rpc_user", "btc_rpc_password"]; 292 | 293 | for (key, value) in json_config.as_object().unwrap().iter() { 294 | if *value != json_default_config[key] { 295 | log::info!( 296 | "Custom config arg: {}: {}", 297 | key, 298 | if sensitive_args.contains(&key.as_str()) { 299 | "****".to_owned() 300 | } else { 301 | value.to_string() 302 | } 303 | ); 304 | } 305 | } 306 | } 307 | } 308 | 309 | impl Default for Config { 310 | /// Sets the tower [Config] defaults. 311 | /// 312 | /// Notice the defaults are not enough, and the tower will refuse to run on them. 313 | /// For instance, the defaults do set the `bitcoind` `rpu_user` and `rpc_password` 314 | /// to empty strings so the user is forced the set them (and most importantly so the 315 | /// user does not use any values provided here). 316 | fn default() -> Self { 317 | Self { 318 | api_bind: "127.0.0.1".into(), 319 | api_port: 9814, 320 | tor_support: false, 321 | tor_control_port: 9051, 322 | onion_hidden_service_port: 9814, 323 | rpc_bind: "127.0.0.1".into(), 324 | rpc_port: 8814, 325 | btc_network: "mainnet".into(), 326 | btc_rpc_user: String::new(), 327 | btc_rpc_password: String::new(), 328 | btc_rpc_cookie: String::new(), 329 | btc_rpc_connect: "localhost".into(), 330 | btc_rpc_port: 0, 331 | 332 | debug: false, 333 | deps_debug: false, 334 | overwrite_key: false, 335 | force_update: false, 336 | subscription_slots: 10000, 337 | subscription_duration: 4320, 338 | expiry_delta: 6, 339 | min_to_self_delay: 20, 340 | polling_delta: 60, 341 | internal_api_bind: "127.0.0.1".into(), 342 | internal_api_port: 50051, 343 | } 344 | } 345 | } 346 | 347 | #[cfg(test)] 348 | mod tests { 349 | use super::*; 350 | 351 | impl Default for Opt { 352 | fn default() -> Self { 353 | Self { 354 | api_bind: None, 355 | api_port: None, 356 | tor_support: false, 357 | tor_control_port: None, 358 | onion_hidden_service_port: None, 359 | rpc_bind: None, 360 | rpc_port: None, 361 | btc_network: None, 362 | btc_rpc_user: None, 363 | btc_rpc_password: None, 364 | btc_rpc_cookie: None, 365 | btc_rpc_connect: None, 366 | btc_rpc_port: None, 367 | data_dir: String::from("~/.teos"), 368 | 369 | debug: false, 370 | deps_debug: false, 371 | overwrite_key: false, 372 | force_update: false, 373 | } 374 | } 375 | } 376 | 377 | #[test] 378 | fn test_config_patch_with_options() { 379 | // Tests that a given Config is overwritten with Opts if the options are present 380 | let mut config = Config::default(); 381 | let config_clone = config.clone(); 382 | let mut opt = Opt::default(); 383 | 384 | let expected_value = String::from("test"); 385 | opt.api_bind = Some(expected_value.clone()); 386 | config.patch_with_options(opt); 387 | 388 | // Check the field has been updated 389 | assert_eq!(config.api_bind, expected_value); 390 | 391 | // Check the rest of fields are equal. The easiest is to just the field back and compare with a clone 392 | config.api_bind.clone_from(&config_clone.api_bind); 393 | assert_eq!(config, config_clone); 394 | } 395 | 396 | #[test] 397 | fn test_config_default_not_verify() { 398 | // Tests that the default configuration does not pass verification checks. This is on purpose so some fields are 399 | // required to be updated by the user. 400 | let mut config = Config::default(); 401 | assert!( 402 | matches!(config.verify(), Err(ConfigError(e)) if e.contains("No valid bitcoind auth provided")) 403 | ); 404 | } 405 | 406 | #[test] 407 | fn test_config_default_verify_overwrite_required() { 408 | // Tests that setting a some btc_rpc_user and btc_rpc_password results in a Config object that verifies 409 | let mut config = Config { 410 | btc_rpc_user: "user".to_owned(), 411 | btc_rpc_password: "password".to_owned(), 412 | ..Default::default() 413 | }; 414 | config.verify().unwrap(); 415 | } 416 | 417 | #[test] 418 | fn test_config_verify_wrong_network() { 419 | // Tests that setting a wrong network will make verify fail 420 | let mut config = Config { 421 | btc_rpc_user: "user".to_owned(), 422 | btc_rpc_password: "password".to_owned(), 423 | btc_network: "wrong_network".to_owned(), 424 | ..Default::default() 425 | }; 426 | assert!( 427 | matches!(config.verify(), Err(ConfigError(e)) if e.contains("btc_network not recognized")) 428 | ); 429 | } 430 | 431 | #[test] 432 | fn test_config_verify_tor_set() { 433 | let mut config = Config { 434 | btc_rpc_user: "user".to_owned(), 435 | btc_rpc_password: "password".to_owned(), 436 | tor_support: true, 437 | ..Default::default() 438 | }; 439 | 440 | config.verify().unwrap() 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /teos/src/errors.rs: -------------------------------------------------------------------------------- 1 | // Custom RPC errors [255+] 2 | #[allow(dead_code)] 3 | pub(crate) const RPC_TX_REORGED_AFTER_BROADCAST: i32 = -256; 4 | // UNHANDLED 5 | pub(crate) const UNKNOWN_JSON_RPC_EXCEPTION: i32 = -257; 6 | -------------------------------------------------------------------------------- /teos/src/extended_appointment.rs: -------------------------------------------------------------------------------- 1 | //! Logic related to appointments handled by the tower. 2 | 3 | use std::array::TryFromSliceError; 4 | use std::convert::TryInto; 5 | use std::fmt; 6 | 7 | use bitcoin::hashes::{ripemd160, Hash}; 8 | 9 | use teos_common::appointment::{Appointment, Locator}; 10 | use teos_common::UserId; 11 | 12 | /// Unique identifier used to identify appointments. 13 | #[allow(clippy::upper_case_acronyms)] 14 | #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] 15 | pub(crate) struct UUID([u8; 20]); 16 | 17 | impl UUID { 18 | /// Creates a new [UUID]. 19 | /// 20 | /// The [UUID]s are created as the `RIPEMD160(locator || user_id)`. This makes it easy to retrieve an [ExtendedAppointment] from the tower 21 | /// when a user requests it without having to perform lookups based on the [Locator], and match what [UUID] belongs to what user (if any). 22 | /// Therefore, it provides a hard-to-forge id while reducing the tower lookups and the required data to be stored (no reverse maps). 23 | pub fn new(locator: Locator, user_id: UserId) -> Self { 24 | let mut uuid_data = locator.to_vec(); 25 | uuid_data.extend(user_id.0.serialize()); 26 | UUID(ripemd160::Hash::hash(&uuid_data).to_byte_array()) 27 | } 28 | 29 | /// Serializes the [UUID] returning its byte representation. 30 | pub fn to_vec(self) -> Vec { 31 | self.0.to_vec() 32 | } 33 | 34 | /// Builds a [UUID] from its byte representation. 35 | pub fn from_slice(data: &[u8]) -> Result { 36 | data.try_into().map(Self) 37 | } 38 | } 39 | 40 | impl std::fmt::Display for UUID { 41 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 42 | write!(f, "{}", hex::encode(self.0)) 43 | } 44 | } 45 | 46 | /// An extended version of the appointment hold by the tower. 47 | /// 48 | /// The [Appointment] is extended in terms of data, that is, it provides further information only relevant to the tower. 49 | #[derive(Debug, Eq, PartialEq, Clone)] 50 | pub(crate) struct ExtendedAppointment { 51 | /// The underlying appointment extended by [ExtendedAppointment]. 52 | pub inner: Appointment, 53 | /// The user this [Appointment] belongs to. 54 | pub user_id: UserId, 55 | /// The signature provided by the user when handing the [Appointment]. 56 | pub user_signature: String, 57 | /// The block where the [Appointment] is started to be watched at by the [Watcher](crate::watcher::Watcher). 58 | pub start_block: u32, 59 | } 60 | 61 | impl ExtendedAppointment { 62 | /// Create a new [ExtendedAppointment]. 63 | pub fn new( 64 | inner: Appointment, 65 | user_id: UserId, 66 | user_signature: String, 67 | start_block: u32, 68 | ) -> Self { 69 | ExtendedAppointment { 70 | inner, 71 | user_id, 72 | user_signature, 73 | start_block, 74 | } 75 | } 76 | 77 | /// Gets the underlying appointment's locator. 78 | pub fn locator(&self) -> Locator { 79 | self.inner.locator 80 | } 81 | 82 | /// Gets the underlying appointment's encrypted data blob 83 | pub fn encrypted_blob(&self) -> &Vec { 84 | &self.inner.encrypted_blob 85 | } 86 | 87 | /// Gets the underlying appointment's `to_self_delay` 88 | pub fn to_self_delay(&self) -> u32 { 89 | self.inner.to_self_delay 90 | } 91 | 92 | pub fn uuid(&self) -> UUID { 93 | UUID::new(self.inner.locator, self.user_id) 94 | } 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use super::*; 100 | 101 | use crate::test_utils::generate_uuid; 102 | 103 | #[test] 104 | fn test_uuid_ser_deser() { 105 | let original_uuid = generate_uuid(); 106 | assert_eq!( 107 | UUID::from_slice(&original_uuid.to_vec()).unwrap(), 108 | original_uuid 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /teos/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The Eye of Satoshi - Lightning watchtower. 2 | //! 3 | //! A watchtower implementation written in Rust. 4 | 5 | // FIXME: This is a temporary fix. See https://github.com/tokio-rs/prost/issues/661 6 | #[allow(clippy::derive_partial_eq_without_eq)] 7 | pub mod protos { 8 | tonic::include_proto!("teos.v2"); 9 | } 10 | pub mod api; 11 | pub mod bitcoin_cli; 12 | pub mod carrier; 13 | pub mod chain_monitor; 14 | pub mod cli_config; 15 | pub mod config; 16 | pub mod dbm; 17 | #[doc(hidden)] 18 | mod errors; 19 | mod extended_appointment; 20 | pub mod gatekeeper; 21 | pub mod responder; 22 | #[doc(hidden)] 23 | mod rpc_errors; 24 | pub mod tls; 25 | mod tx_index; 26 | pub mod watcher; 27 | 28 | #[cfg(test)] 29 | mod test_utils; 30 | -------------------------------------------------------------------------------- /teos/src/main.rs: -------------------------------------------------------------------------------- 1 | use log::LevelFilter; 2 | use simple_logger::SimpleLogger; 3 | use std::fs; 4 | use std::io::ErrorKind; 5 | use std::ops::{Deref, DerefMut}; 6 | use std::sync::{Arc, Condvar, Mutex}; 7 | use structopt::StructOpt; 8 | use tokio::task; 9 | use tonic::transport::{Certificate, Server, ServerTlsConfig}; 10 | 11 | use bitcoin::network::Network; 12 | use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; 13 | use bitcoincore_rpc::{Auth, Client, RpcApi}; 14 | use lightning_block_sync::init::validate_best_block_header; 15 | use lightning_block_sync::poll::{ 16 | ChainPoller, Poll, Validate, ValidatedBlock, ValidatedBlockHeader, 17 | }; 18 | use lightning_block_sync::{BlockSource, BlockSourceError, SpvClient, UnboundedCache}; 19 | 20 | use teos::api::internal::InternalAPI; 21 | use teos::api::{http, tor::TorAPI}; 22 | use teos::bitcoin_cli::BitcoindClient; 23 | use teos::carrier::Carrier; 24 | use teos::chain_monitor::ChainMonitor; 25 | use teos::config::{self, AuthMethod, Config, Opt}; 26 | use teos::dbm::DBM; 27 | use teos::gatekeeper::Gatekeeper; 28 | use teos::protos as msgs; 29 | use teos::protos::private_tower_services_server::PrivateTowerServicesServer; 30 | use teos::protos::public_tower_services_server::PublicTowerServicesServer; 31 | use teos::responder::Responder; 32 | use teos::tls::tls_init; 33 | use teos::watcher::Watcher; 34 | 35 | use teos_common::constants::IRREVOCABLY_RESOLVED; 36 | use teos_common::cryptography::get_random_keypair; 37 | use teos_common::TowerId; 38 | 39 | async fn get_last_n_blocks( 40 | poller: &mut ChainPoller, 41 | mut last_known_block: ValidatedBlockHeader, 42 | n: usize, 43 | ) -> Result, BlockSourceError> 44 | where 45 | B: DerefMut + Sized + Send + Sync, 46 | T: BlockSource, 47 | { 48 | let mut last_n_blocks = Vec::with_capacity(n); 49 | for _ in 0..n { 50 | log::debug!("Fetching block #{}", last_known_block.height); 51 | let block = poller.fetch_block(&last_known_block).await?; 52 | last_known_block = poller.look_up_previous_header(&last_known_block).await?; 53 | last_n_blocks.push(block); 54 | } 55 | 56 | Ok(last_n_blocks) 57 | } 58 | 59 | fn create_new_tower_keypair(db: &DBM) -> (SecretKey, PublicKey) { 60 | let (sk, pk) = get_random_keypair(); 61 | db.store_tower_key(&sk).unwrap(); 62 | (sk, pk) 63 | } 64 | 65 | #[tokio::main] 66 | async fn main() { 67 | let opt = Opt::from_args(); 68 | let path = config::data_dir_absolute_path(opt.data_dir.clone()); 69 | let conf_file_path = path.join("teos.toml"); 70 | // Create data dir if it does not exist 71 | fs::create_dir_all(&path).unwrap_or_else(|e| { 72 | eprintln!("Cannot create data dir: {e:?}"); 73 | std::process::exit(1); 74 | }); 75 | 76 | // Load conf (from file or defaults) and patch it with the command line parameters received (if any) 77 | let mut conf = config::from_file::(&conf_file_path); 78 | let is_default = conf.is_default(); 79 | conf.patch_with_options(opt); 80 | conf.verify().unwrap_or_else(|e| { 81 | eprintln!("{e}"); 82 | std::process::exit(1); 83 | }); 84 | 85 | // Set log level 86 | SimpleLogger::new() 87 | .with_level(if conf.deps_debug { 88 | LevelFilter::Debug 89 | } else { 90 | LevelFilter::Warn 91 | }) 92 | .with_module_level( 93 | "teos", 94 | if conf.debug { 95 | LevelFilter::Debug 96 | } else { 97 | LevelFilter::Info 98 | }, 99 | ) 100 | .init() 101 | .unwrap(); 102 | 103 | // Create network dir 104 | let path_network = path.join(conf.btc_network.clone()); 105 | fs::create_dir_all(&path_network).unwrap_or_else(|e| { 106 | eprintln!("Cannot create network dir: {e:?}"); 107 | std::process::exit(1); 108 | }); 109 | 110 | // Log default data dir 111 | log::info!("Default data directory: {:?}", &path); 112 | 113 | // Log datadir path 114 | log::info!("Using data directory: {:?}", &path_network); 115 | 116 | // Log config file path based on whether the config file is found or not 117 | if is_default { 118 | log::info!("Config file: {:?} (not found, skipping)", &conf_file_path); 119 | } else { 120 | log::info!("Config file: {:?}", &conf_file_path); 121 | conf.log_non_default_options(); 122 | } 123 | 124 | let dbm = Arc::new(Mutex::new( 125 | DBM::new(path_network.join("teos_db.sql3")).unwrap(), 126 | )); 127 | 128 | // Load tower secret key or create a fresh one if none is found. If overwrite key is set, create a new 129 | // key straightaway 130 | let (tower_sk, tower_pk) = { 131 | let locked_db = dbm.lock().unwrap(); 132 | if conf.overwrite_key { 133 | log::info!("Overwriting tower keys"); 134 | create_new_tower_keypair(&locked_db) 135 | } else if let Some(sk) = locked_db.load_tower_key() { 136 | (sk, PublicKey::from_secret_key(&Secp256k1::new(), &sk)) 137 | } else { 138 | log::info!("Tower keys not found. Creating a fresh set"); 139 | create_new_tower_keypair(&locked_db) 140 | } 141 | }; 142 | log::info!("tower_id: {tower_pk}"); 143 | 144 | let btc_rpc_auth = match conf.get_auth_method() { 145 | AuthMethod::CookieFile => { 146 | Auth::CookieFile(config::data_dir_absolute_path(conf.btc_rpc_cookie)) 147 | } 148 | AuthMethod::UserPass => Auth::UserPass(conf.btc_rpc_user, conf.btc_rpc_password), 149 | // Notice an invalid conf would have failed on `Config::verify()` 150 | _ => unreachable!("A verified conf will only have one of these two auth methods"), 151 | }; 152 | 153 | // Initialize our bitcoind client 154 | let (bitcoin_cli, bitcoind_reachable) = match BitcoindClient::new( 155 | &conf.btc_rpc_connect, 156 | conf.btc_rpc_port, 157 | btc_rpc_auth.clone(), 158 | &conf.btc_network, 159 | ) 160 | .await 161 | { 162 | Ok(client) => ( 163 | Arc::new(client), 164 | Arc::new((Mutex::new(true), Condvar::new())), 165 | ), 166 | Err(e) => { 167 | let e_msg = match e.kind() { 168 | ErrorKind::InvalidData => "invalid btcrpcuser or btcrpcpassword".into(), 169 | _ => e.to_string(), 170 | }; 171 | log::error!("Failed to connect to bitcoind. Error: {e_msg}"); 172 | std::process::exit(1); 173 | } 174 | }; 175 | 176 | // FIXME: Temporary. We're using bitcoin_core_rpc and rust-lightning's rpc until they both get merged 177 | // https://github.com/rust-bitcoin/rust-bitcoincore-rpc/issues/166 178 | let schema = if !conf.btc_rpc_connect.starts_with("http") { 179 | "http://" 180 | } else { 181 | "" 182 | }; 183 | let rpc = Arc::new( 184 | Client::new( 185 | &format!("{schema}{}:{}", conf.btc_rpc_connect, conf.btc_rpc_port), 186 | btc_rpc_auth, 187 | ) 188 | .unwrap(), 189 | ); 190 | let mut derefed = bitcoin_cli.deref(); 191 | // Load last known block from DB if found. Poll it from Bitcoind otherwise. 192 | let last_known_block = dbm.lock().unwrap().load_last_known_block(); 193 | let tip = if let Some(block_hash) = last_known_block { 194 | let mut last_known_header = derefed 195 | .get_header(&block_hash, None) 196 | .await 197 | .unwrap() 198 | .validate(block_hash) 199 | .unwrap(); 200 | 201 | log::info!( 202 | "Last known block: {} (height: {})", 203 | last_known_header.header.block_hash(), 204 | last_known_header.height 205 | ); 206 | 207 | // If we are running in pruned mode some data may be missing (if we happen to have been offline for a while) 208 | if let Some(prune_height) = rpc.get_blockchain_info().unwrap().prune_height { 209 | if last_known_header.height - IRREVOCABLY_RESOLVED + 1 < prune_height as u32 { 210 | log::warn!( 211 | "Cannot load blocks in the range {}-{}. Chain has gone too far out of sync", 212 | last_known_header.height - IRREVOCABLY_RESOLVED + 1, 213 | last_known_header.height 214 | ); 215 | if conf.force_update { 216 | log::info!("Forcing a backend update"); 217 | // We want to grab the first IRREVOCABLY_RESOLVED we know about for the initial cache 218 | // So we can perform transitions from there onwards. 219 | let target_height = prune_height + IRREVOCABLY_RESOLVED as u64; 220 | let target_hash = rpc.get_block_hash(target_height).unwrap(); 221 | last_known_header = derefed 222 | .get_header( 223 | &rpc.get_block_hash(target_height).unwrap(), 224 | Some(target_height as u32), 225 | ) 226 | .await 227 | .unwrap() 228 | .validate(target_hash) 229 | .unwrap(); 230 | } else { 231 | log::error!( 232 | "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" 233 | ); 234 | std::process::exit(1); 235 | } 236 | } 237 | } 238 | last_known_header 239 | } else { 240 | validate_best_block_header(&derefed).await.unwrap() 241 | }; 242 | 243 | // DISCUSS: This is not really required (and only triggered in regtest). This is only in place so the caches can be 244 | // populated with enough blocks mainly because the size of the cache is based on the amount of blocks passed when initializing. 245 | // However, we could add an additional parameter to specify the size of the cache, and initialize with however may blocks we 246 | // could pull from the backend. Adding this functionality just for regtest seemed unnecessary though, hence the check. 247 | if tip.height < IRREVOCABLY_RESOLVED { 248 | log::error!( 249 | "Not enough blocks to start teosd (required: {IRREVOCABLY_RESOLVED}). Mine at least {} more", 250 | IRREVOCABLY_RESOLVED - tip.height 251 | ); 252 | std::process::exit(1); 253 | } 254 | 255 | log::info!( 256 | "Current chain tip: {} (height: {})", 257 | tip.header.block_hash(), 258 | tip.height 259 | ); 260 | 261 | // Build components 262 | let gatekeeper = Arc::new(Gatekeeper::new( 263 | tip.height, 264 | conf.subscription_slots, 265 | conf.subscription_duration, 266 | conf.expiry_delta, 267 | dbm.clone(), 268 | )); 269 | 270 | let mut poller = ChainPoller::new( 271 | &mut derefed, 272 | Network::from_core_arg(&conf.btc_network).unwrap(), 273 | ); 274 | let (responder, watcher) = { 275 | let last_n_blocks = get_last_n_blocks(&mut poller, tip, IRREVOCABLY_RESOLVED as usize) 276 | .await.unwrap_or_else(|e| { 277 | // 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 278 | // the end at least one has been pruned. 279 | log::error!("Couldn't load the latest {IRREVOCABLY_RESOLVED} blocks. Please try again (Error: {})", e.into_inner()); 280 | std::process::exit(1); 281 | } 282 | ); 283 | 284 | let responder = Arc::new(Responder::new( 285 | &last_n_blocks, 286 | tip.height, 287 | Carrier::new(rpc, bitcoind_reachable.clone(), tip.height), 288 | gatekeeper.clone(), 289 | dbm.clone(), 290 | )); 291 | let watcher = Arc::new(Watcher::new( 292 | gatekeeper.clone(), 293 | responder.clone(), 294 | &last_n_blocks[0..6], 295 | tip.height, 296 | tower_sk, 297 | TowerId(tower_pk), 298 | dbm.clone(), 299 | )); 300 | (responder, watcher) 301 | }; 302 | 303 | if watcher.is_fresh() & responder.is_fresh() & gatekeeper.is_fresh() { 304 | log::info!("Fresh bootstrap"); 305 | } else { 306 | log::info!("Bootstrapping from backed up data"); 307 | } 308 | 309 | let (shutdown_trigger, shutdown_signal_rpc_api) = triggered::trigger(); 310 | let shutdown_signal_internal_api = shutdown_signal_rpc_api.clone(); 311 | let shutdown_signal_http = shutdown_signal_rpc_api.clone(); 312 | let shutdown_signal_cm = shutdown_signal_rpc_api.clone(); 313 | let shutdown_signal_tor = shutdown_signal_rpc_api.clone(); 314 | 315 | // The ordering here actually matters. Listeners are called by order, and we want the gatekeeper to be called 316 | // first so it updates the users' states and both the Watcher and the Responder operate only on registered users. 317 | let listener = &(gatekeeper, &(watcher.clone(), responder)); 318 | let cache = &mut UnboundedCache::new(); 319 | let spv_client = SpvClient::new(tip, poller, cache, listener); 320 | let mut chain_monitor = ChainMonitor::new( 321 | spv_client, 322 | tip, 323 | dbm, 324 | conf.polling_delta, 325 | shutdown_signal_cm, 326 | bitcoind_reachable.clone(), 327 | ) 328 | .await; 329 | 330 | // Get all the components up to date if there's a backlog of blocks 331 | chain_monitor.poll_best_tip().await; 332 | log::info!("Bootstrap completed. Turning on interfaces"); 333 | 334 | // Build interfaces 335 | let http_api_addr = format!("{}:{}", conf.api_bind, conf.api_port) 336 | .parse() 337 | .unwrap(); 338 | let mut addresses = vec![msgs::NetworkAddress::from_ipv4( 339 | conf.api_bind.clone(), 340 | conf.api_port, 341 | )]; 342 | 343 | // Create Tor endpoint if required 344 | let tor_api = if conf.tor_support { 345 | let tor_api = TorAPI::new( 346 | http_api_addr, 347 | conf.onion_hidden_service_port, 348 | conf.tor_control_port, 349 | path_network, 350 | ) 351 | .await; 352 | addresses.push(msgs::NetworkAddress::from_torv3( 353 | tor_api.get_onion_address(), 354 | conf.onion_hidden_service_port, 355 | )); 356 | 357 | Some(tor_api) 358 | } else { 359 | None 360 | }; 361 | 362 | let internal_api = Arc::new(InternalAPI::new( 363 | watcher, 364 | addresses, 365 | bitcoind_reachable.clone(), 366 | shutdown_trigger, 367 | )); 368 | let internal_api_cloned = internal_api.clone(); 369 | 370 | let rpc_api_addr = format!("{}:{}", conf.rpc_bind, conf.rpc_port) 371 | .parse() 372 | .unwrap(); 373 | let internal_api_addr = format!("{}:{}", conf.internal_api_bind, conf.internal_api_port) 374 | .parse() 375 | .unwrap(); 376 | 377 | // Generate mtls certificates to data directory so the admin can securely connect 378 | // to the server to perform administrative tasks. 379 | let (identity, ca_cert) = tls_init(&path).unwrap_or_else(|e| { 380 | eprintln!("Couldn't generate tls certificates: {e:?}"); 381 | std::process::exit(1); 382 | }); 383 | 384 | let tls = ServerTlsConfig::new() 385 | .identity(identity) 386 | .client_ca_root(Certificate::from_pem(ca_cert)); 387 | 388 | // Start tasks 389 | let private_api_task = task::spawn(async move { 390 | Server::builder() 391 | .tls_config(tls) 392 | .expect("couldn't configure tls") 393 | .add_service(PrivateTowerServicesServer::new(internal_api)) 394 | .serve_with_shutdown(rpc_api_addr, shutdown_signal_rpc_api) 395 | .await 396 | .unwrap(); 397 | }); 398 | 399 | let public_api_task = task::spawn(async move { 400 | Server::builder() 401 | .add_service(PublicTowerServicesServer::new(internal_api_cloned)) 402 | .serve_with_shutdown(internal_api_addr, shutdown_signal_internal_api) 403 | .await 404 | .unwrap(); 405 | }); 406 | 407 | let (http_service_ready, ready_signal_http) = triggered::trigger(); 408 | let http_api_task = task::spawn(http::serve( 409 | http_api_addr, 410 | internal_api_addr, 411 | http_service_ready, 412 | shutdown_signal_http, 413 | )); 414 | ready_signal_http.await; 415 | 416 | // Add Tor Onion Service for public API 417 | let mut tor_task = Option::None; 418 | let (tor_service_ready, ready_signal_tor) = triggered::trigger(); 419 | if let Some(tor_api) = tor_api { 420 | log::info!("Starting up Tor hidden service"); 421 | 422 | tor_task = Some(task::spawn(async move { 423 | if let Err(e) = tor_api 424 | .expose_onion_service(tor_service_ready, shutdown_signal_tor) 425 | .await 426 | { 427 | eprintln!("Cannot connect to the Tor backend: {e}"); 428 | std::process::exit(1); 429 | } 430 | })); 431 | 432 | ready_signal_tor.await 433 | } 434 | 435 | log::info!("Tower ready"); 436 | chain_monitor.monitor_chain().await; 437 | 438 | // Wait until shutdown 439 | http_api_task.await.unwrap(); 440 | private_api_task.await.unwrap(); 441 | public_api_task.await.unwrap(); 442 | if let Some(tor_task) = tor_task { 443 | tor_task.await.unwrap(); 444 | } 445 | 446 | log::info!("Shutting down tower"); 447 | } 448 | -------------------------------------------------------------------------------- /teos/src/rpc_errors.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | // Ported from https://github.com/bitcoin/bitcoin/blob/0.18/src/rpc/protocol.h 3 | // TODO: Check if we can get rid of this whole module once `bitcoincore-rpc` is fully integrated. 4 | 5 | // General application defined errors 6 | pub const RPC_MISC_ERROR: i32 = -1; // std::exception thrown in command handling 7 | pub const RPC_TYPE_ERROR: i32 = -3; // Unexpected type was passed as parameter 8 | pub const RPC_INVALID_ADDRESS_OR_KEY: i32 = -5; // Invalid address or key 9 | pub const RPC_OUT_OF_MEMORY: i32 = -7; // Ran out of memory during operation 10 | pub const RPC_INVALID_PARAMETER: i32 = -8; // Invalid missing or duplicate parameter 11 | pub const RPC_DATABASE_ERROR: i32 = -20; // Database error 12 | pub const RPC_DESERIALIZATION_ERROR: i32 = -22; // Error parsing or validating structure in raw format 13 | pub const RPC_VERIFY_ERROR: i32 = -25; // General error during transaction or block submission 14 | pub const RPC_VERIFY_REJECTED: i32 = -26; // Transaction or block was rejected by network rules 15 | pub const RPC_VERIFY_ALREADY_IN_CHAIN: i32 = -27; // Transaction already in chain 16 | pub const RPC_IN_WARMUP: i32 = -28; // Client still warming up 17 | pub const RPC_METHOD_DEPRECATED: i32 = -32; // RPC method is deprecated 18 | 19 | // Aliases for backward compatibility 20 | pub const RPC_TRANSACTION_ERROR: i32 = RPC_VERIFY_ERROR; 21 | pub const RPC_TRANSACTION_REJECTED: i32 = RPC_VERIFY_REJECTED; 22 | pub const RPC_TRANSACTION_ALREADY_IN_CHAIN: i32 = RPC_VERIFY_ALREADY_IN_CHAIN; 23 | 24 | // P2P client errors 25 | pub const RPC_CLIENT_NOT_CONNECTED: i32 = -9; // Bitcoin is not connected 26 | pub const RPC_CLIENT_IN_INITIAL_DOWNLOAD: i32 = -10; // Still downloading initial blocks 27 | pub const RPC_CLIENT_NODE_ALREADY_ADDED: i32 = -23; // Node is already added 28 | pub const RPC_CLIENT_NODE_NOT_ADDED: i32 = -24; // Node has not been added before 29 | pub const RPC_CLIENT_NODE_NOT_CONNECTED: i32 = -29; // Node to disconnect not found in connected nodes 30 | pub const RPC_CLIENT_INVALID_IP_OR_SUBNET: i32 = -30; // Invalid IP/Subnet 31 | pub const RPC_CLIENT_P2P_DISABLED: i32 = -31; // No valid connection manager instance found 32 | 33 | // Wallet errors 34 | pub const RPC_WALLET_ERROR: i32 = -4; // Unspecified problem with wallet (key not found etc.) 35 | pub const RPC_WALLET_INSUFFICIENT_FUNDS: i32 = -6; // Not enough funds in wallet or account 36 | pub const RPC_WALLET_INVALID_LABEL_NAME: i32 = -11; // Invalid label name 37 | pub const RPC_WALLET_KEYPOOL_RAN_OUT: i32 = -12; // Keypool ran out call keypoolrefill first 38 | pub const RPC_WALLET_UNLOCK_NEEDED: i32 = -13; // Enter the wallet passphrase with walletpassphrase first 39 | pub const RPC_WALLET_PASSPHRASE_INCORRECT: i32 = -14; // The wallet passphrase entered was incorrect 40 | pub const RPC_WALLET_WRONG_ENC_STATE: i32 = -15; // Command given in wrong wallet encryption state (encrypting an encrypted wallet etc.) 41 | pub const RPC_WALLET_ENCRYPTION_FAILED: i32 = -16; // Failed to encrypt the wallet 42 | pub const RPC_WALLET_ALREADY_UNLOCKED: i32 = -17; // Wallet is already unlocked 43 | pub const RPC_WALLET_NOT_FOUND: i32 = -18; // Invalid wallet specified 44 | pub const RPC_WALLET_NOT_SPECIFIED: i32 = -19; // No wallet specified (error when there are multiple wallets loaded) 45 | -------------------------------------------------------------------------------- /teos/src/tls.rs: -------------------------------------------------------------------------------- 1 | /* The following code for generating mTLS certificates is adapted from: 2 | * https://github.com/ElementsProject/lightning/blob/master/plugins/grpc-plugin/src/tls.rs 3 | * 4 | * This file is licensed under the BSD-MIT license, as described here: 5 | * https://github.com/ElementsProject/lightning/blob/master/LICENSE 6 | */ 7 | 8 | use rcgen::{Certificate, Error as RcgenError, KeyPair}; 9 | use std::convert::TryFrom; 10 | use std::path::Path; 11 | 12 | /// Packs the reasons why generating mtls certificates may fail. 13 | #[derive(Debug)] 14 | pub enum GenCertificateFailure { 15 | RcgenError(RcgenError), 16 | IoError(std::io::Error), 17 | } 18 | 19 | impl From for GenCertificateFailure { 20 | fn from(e: RcgenError) -> Self { 21 | GenCertificateFailure::RcgenError(e) 22 | } 23 | } 24 | 25 | impl From for GenCertificateFailure { 26 | fn from(e: std::io::Error) -> Self { 27 | GenCertificateFailure::IoError(e) 28 | } 29 | } 30 | 31 | /// Just a wrapper around a certificate and an associated keypair. 32 | #[derive(Clone, Debug)] 33 | struct Identity { 34 | pub key: Vec, 35 | pub certificate: Vec, 36 | } 37 | 38 | impl TryFrom<&Identity> for (Certificate, KeyPair) { 39 | type Error = RcgenError; 40 | 41 | fn try_from(id: &Identity) -> Result<(Certificate, KeyPair), RcgenError> { 42 | let key = KeyPair::from_pem(&String::from_utf8_lossy(&id.key))?; 43 | let params = 44 | rcgen::CertificateParams::from_ca_cert_pem(&String::from_utf8_lossy(&id.certificate))?; 45 | let cert = params.self_signed(&key)?; 46 | Ok((cert, key)) 47 | } 48 | } 49 | 50 | pub fn tls_init( 51 | directory: &Path, 52 | ) -> Result<(tonic::transport::Identity, Vec), GenCertificateFailure> { 53 | let ca = generate_or_load_identity("teos Root CA", directory, "ca", None)?; 54 | let server = generate_or_load_identity("teos grpc Server", directory, "server", Some(&ca))?; 55 | let _client = generate_or_load_identity("teos grpc Client", directory, "client", Some(&ca))?; 56 | let server_id = tonic::transport::Identity::from_pem(&server.certificate, &server.key); 57 | 58 | Ok((server_id, ca.certificate)) 59 | } 60 | 61 | /// Generate a given identity 62 | fn generate_or_load_identity( 63 | name: &str, 64 | directory: &Path, 65 | filename: &str, 66 | parent: Option<&Identity>, 67 | ) -> Result { 68 | // Just our naming convention here. 69 | let cert_path = directory.join(format!("{filename}.pem")); 70 | let key_path = directory.join(format!("{filename}-key.pem")); 71 | // Did we have to generate a new key? In that case we also need to regenerate the certificate. 72 | if !key_path.exists() || !cert_path.exists() { 73 | log::debug!("Generating a new keypair in {key_path:?}, it didn't exist",); 74 | let keypair = KeyPair::generate()?; 75 | std::fs::write(&key_path, keypair.serialize_pem())?; 76 | log::debug!("Generating a new certificate for key {key_path:?} at {cert_path:?}",); 77 | 78 | // Configure the certificate we want. 79 | let subject_alt_names = vec!["teos".to_string(), "localhost".to_string()]; 80 | let mut params = rcgen::CertificateParams::new(subject_alt_names)?; 81 | if parent.is_none() { 82 | params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained); 83 | } else { 84 | params.is_ca = rcgen::IsCa::NoCa; 85 | } 86 | params 87 | .distinguished_name 88 | .push(rcgen::DnType::CommonName, name); 89 | 90 | std::fs::write( 91 | &cert_path, 92 | match parent { 93 | None => params.self_signed(&keypair)?.pem(), 94 | Some(ca) => { 95 | let (ca_cert, ca_key) = <(Certificate, KeyPair)>::try_from(ca)?; 96 | params.signed_by(&keypair, &ca_cert, &ca_key)?.pem() 97 | } 98 | }, 99 | )?; 100 | } 101 | 102 | let key = std::fs::read(&key_path)?; 103 | let certificate = std::fs::read(cert_path)?; 104 | Ok(Identity { certificate, key }) 105 | } 106 | -------------------------------------------------------------------------------- /teos/src/tx_index.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, VecDeque}; 2 | use std::fmt; 3 | use std::hash::Hash; 4 | use std::ops::Deref; 5 | 6 | use bitcoin::block::Header; 7 | use bitcoin::hash_types::BlockHash; 8 | use bitcoin::{Transaction, Txid}; 9 | use lightning_block_sync::poll::ValidatedBlock; 10 | 11 | use teos_common::appointment::Locator; 12 | 13 | /// A trait implemented by types that can be used as key in a [TxIndex]. 14 | pub trait Key: Hash + Eq { 15 | fn from_txid(txid: Txid) -> Self; 16 | } 17 | 18 | impl Key for Txid { 19 | fn from_txid(txid: Txid) -> Self { 20 | txid 21 | } 22 | } 23 | 24 | impl Key for Locator { 25 | fn from_txid(txid: Txid) -> Self { 26 | Locator::new(txid) 27 | } 28 | } 29 | 30 | pub enum Type { 31 | Transaction, 32 | BlockHash, 33 | } 34 | 35 | pub enum Data { 36 | Transaction(Transaction), 37 | BlockHash(BlockHash), 38 | } 39 | 40 | impl fmt::Display for Data { 41 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 42 | match self { 43 | Data::Transaction(_) => write!(f, "Transaction"), 44 | Data::BlockHash(_) => write!(f, "BlockHash"), 45 | } 46 | } 47 | } 48 | 49 | /// A trait implemented by types that can be used as value in a [TxIndex]. 50 | pub trait Value { 51 | fn get_type() -> Type; 52 | fn from_data(d: Data) -> Self; 53 | } 54 | 55 | impl Value for BlockHash { 56 | fn get_type() -> Type { 57 | Type::BlockHash 58 | } 59 | 60 | fn from_data(d: Data) -> Self { 61 | match d { 62 | Data::BlockHash(b) => b, 63 | other => panic!("Cannot build a BlockHash from {}", other), 64 | } 65 | } 66 | } 67 | 68 | impl Value for Transaction { 69 | fn get_type() -> Type { 70 | Type::Transaction 71 | } 72 | 73 | fn from_data(d: Data) -> Self { 74 | match d { 75 | Data::Transaction(t) => t, 76 | other => panic!("Cannot build a BlockHash from {}", other), 77 | } 78 | } 79 | } 80 | 81 | /// Data structure used to index locators computed from parsed blocks. 82 | /// 83 | /// Holds up to `size` blocks with their corresponding computed [Locator]s. 84 | #[derive(Debug, PartialEq, Eq)] 85 | pub struct TxIndex { 86 | /// A [K]:[V] map. 87 | index: HashMap, 88 | /// Vector of block hashes covered by the index. 89 | blocks: VecDeque, 90 | /// Map of [BlockHash]:[Vec]. Used to remove data from the index. 91 | tx_in_block: HashMap>, 92 | /// The height of the last block included in the index. 93 | tip: u32, 94 | /// Maximum size of the index. 95 | size: usize, 96 | } 97 | 98 | impl TxIndex 99 | where 100 | K: Key + Copy, 101 | V: Value + Clone, 102 | Self: Sized, 103 | { 104 | pub fn new(last_n_blocks: &[ValidatedBlock], height: u32) -> Self { 105 | let size = last_n_blocks.len(); 106 | let mut tx_index = Self { 107 | index: HashMap::new(), 108 | blocks: VecDeque::with_capacity(size), 109 | tx_in_block: HashMap::new(), 110 | tip: height, 111 | size, 112 | }; 113 | 114 | for block in last_n_blocks.iter().rev() { 115 | match block.deref() { 116 | lightning_block_sync::BlockData::HeaderOnly(_) => { 117 | panic!("Expected FullBlock") 118 | } 119 | lightning_block_sync::BlockData::FullBlock(block) => { 120 | if let Some(prev_block_hash) = tx_index.blocks.back() { 121 | if block.header.prev_blockhash != *prev_block_hash { 122 | panic!("last_n_blocks contains unchained blocks"); 123 | } 124 | }; 125 | 126 | let map = block 127 | .txdata 128 | .iter() 129 | .map(|tx| { 130 | ( 131 | K::from_txid(tx.compute_txid()), 132 | match V::get_type() { 133 | Type::Transaction => { 134 | V::from_data(Data::Transaction(tx.clone())) 135 | } 136 | Type::BlockHash => { 137 | V::from_data(Data::BlockHash(block.header.block_hash())) 138 | } 139 | }, 140 | ) 141 | }) 142 | .collect(); 143 | 144 | tx_index.update(block.header, &map); 145 | } 146 | } 147 | } 148 | 149 | tx_index 150 | } 151 | 152 | /// Gets an item from the index if present. [None] otherwise. 153 | pub fn get<'a>(&'a self, k: &'a K) -> Option<&V> { 154 | self.index.get(k) 155 | } 156 | 157 | /// Checks if the index if full. 158 | pub fn is_full(&self) -> bool { 159 | self.blocks.len() > self.size 160 | } 161 | 162 | /// Get's the height of a given block based on its position in the block queue. 163 | pub fn get_height(&self, block_hash: &BlockHash) -> Option { 164 | let pos = self.blocks.iter().position(|x| x == block_hash)?; 165 | Some(self.tip as usize + pos + 1 - self.blocks.len()) 166 | } 167 | 168 | /// Updates the index by adding data from a new block. Removes the oldest block if the index is full afterwards. 169 | pub fn update(&mut self, block_header: Header, data: &HashMap) { 170 | self.blocks.push_back(block_header.block_hash()); 171 | 172 | let ks = data 173 | .iter() 174 | .map(|(k, v)| { 175 | self.index.insert(*k, v.clone()); 176 | *k 177 | }) 178 | .collect(); 179 | 180 | self.tx_in_block.insert(block_header.block_hash(), ks); 181 | 182 | if self.is_full() { 183 | // Avoid logging during bootstrap 184 | log::debug!("New block added to index: {}", block_header.block_hash()); 185 | self.tip += 1; 186 | self.remove_oldest_block(); 187 | } 188 | } 189 | 190 | /// Fixes the index by removing disconnected data. 191 | pub fn remove_disconnected_block(&mut self, block_hash: &BlockHash) { 192 | if let Some(ks) = self.tx_in_block.remove(block_hash) { 193 | self.index.retain(|k, _| !ks.contains(k)); 194 | 195 | // Blocks should be disconnected from last backwards. Log if that's not the case so we can revisit this and fix it. 196 | if let Some(ref h) = self.blocks.pop_back() { 197 | if h != block_hash { 198 | log::error!("Disconnected block does not match the oldest block stored in the TxIndex ({block_hash} != {h})"); 199 | } 200 | } 201 | } else { 202 | log::warn!("The index is already empty"); 203 | } 204 | } 205 | 206 | /// Removes the oldest block from the index. 207 | /// This removes data from `self.blocks`, `self.tx_in_block` and `self.index`. 208 | pub fn remove_oldest_block(&mut self) { 209 | let h = self.blocks.pop_front().unwrap(); 210 | let ks = self.tx_in_block.remove(&h).unwrap(); 211 | self.index.retain(|k, _| !ks.contains(k)); 212 | 213 | log::debug!("Oldest block removed from index: {h}"); 214 | } 215 | } 216 | 217 | impl fmt::Display for TxIndex { 218 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 219 | write!( 220 | f, 221 | "index: {:?}\n\nblocks: {:?}\n\ntx_in_block: {:?}\n\nsize: {}", 222 | self.index, self.blocks, self.tx_in_block, self.size 223 | ) 224 | } 225 | } 226 | 227 | #[cfg(test)] 228 | mod tests { 229 | use super::*; 230 | use std::ops::Deref; 231 | 232 | use crate::test_utils::{get_full_block, get_full_blocks, get_last_n_blocks, Blockchain}; 233 | 234 | use bitcoin::hashes::serde_macros::serde_details::SerdeHash; 235 | use bitcoin::Block; 236 | 237 | impl TxIndex 238 | where 239 | K: Key + std::cmp::Eq + Copy, 240 | V: Value + Clone, 241 | Self: Sized, 242 | { 243 | pub fn index_mut(&mut self) -> &mut HashMap { 244 | &mut self.index 245 | } 246 | 247 | pub fn blocks(&self) -> &VecDeque { 248 | &self.blocks 249 | } 250 | 251 | pub fn contains_key(&self, k: &K) -> bool { 252 | self.index.contains_key(k) 253 | } 254 | } 255 | 256 | #[tokio::test] 257 | async fn test_new() { 258 | let height = 10; 259 | let mut chain = Blockchain::default().with_height(height as usize); 260 | let last_six_blocks = get_last_n_blocks(&mut chain, 6).await; 261 | let blocks: Vec = get_full_blocks(&last_six_blocks); 262 | 263 | let cache: TxIndex = TxIndex::new(&last_six_blocks, height); 264 | assert_eq!(blocks.len(), cache.size); 265 | for block in blocks.iter() { 266 | assert!(cache.blocks().contains(&block.block_hash())); 267 | 268 | let mut locators = Vec::new(); 269 | for tx in block.txdata.iter() { 270 | let locator = Locator::new(tx.compute_txid()); 271 | assert!(cache.contains_key(&locator)); 272 | locators.push(locator); 273 | } 274 | 275 | assert_eq!(cache.tx_in_block[&block.block_hash()], locators); 276 | } 277 | } 278 | 279 | #[tokio::test] 280 | async fn test_get_height() { 281 | let cache_size = 10; 282 | let height = 50; 283 | let mut chain = Blockchain::default().with_height_and_txs(height, 42); 284 | let last_n_blocks = get_last_n_blocks(&mut chain, cache_size).await; 285 | 286 | // last_n_blocks is ordered from latest to earliest 287 | let first_block = get_full_block(last_n_blocks.get(cache_size - 1).unwrap()); 288 | let last_block = get_full_block(last_n_blocks.first().unwrap()); 289 | let mid_block = get_full_block(last_n_blocks.get(cache_size / 2).unwrap()); 290 | 291 | let cache: TxIndex = TxIndex::new(&last_n_blocks, height as u32); 292 | 293 | assert_eq!( 294 | cache.get_height(&first_block.header.block_hash()).unwrap(), 295 | height - cache_size + 1 296 | ); 297 | assert_eq!( 298 | cache.get_height(&last_block.header.block_hash()).unwrap(), 299 | height 300 | ); 301 | assert_eq!( 302 | cache.get_height(&mid_block.header.block_hash()).unwrap(), 303 | height - cache_size / 2 304 | ); 305 | } 306 | 307 | #[tokio::test] 308 | async fn test_get_height_not_found() { 309 | let cache_size = 10; 310 | let height = 50; 311 | let mut chain = Blockchain::default().with_height_and_txs(height, 42); 312 | let cache: TxIndex = TxIndex::new( 313 | &get_last_n_blocks(&mut chain, cache_size).await, 314 | height as u32, 315 | ); 316 | 317 | let fake_hash = &BlockHash::from_slice_delegated(&[0; 32]).unwrap(); 318 | assert!(cache.get_height(fake_hash).is_none()); 319 | } 320 | 321 | #[tokio::test] 322 | async fn test_update() { 323 | let height = 10; 324 | let mut chain = Blockchain::default().with_height(height as usize); 325 | let mut last_n_blocks = get_last_n_blocks(&mut chain, 7).await; 326 | 327 | // Store the last block to use it for an update and the first to check eviction 328 | // Notice that the list of blocks is ordered from last to first. 329 | let last_block = last_n_blocks.remove(0); 330 | let first_block = last_n_blocks.last().unwrap(); 331 | 332 | // Init the cache with the 6 block before the last 333 | let mut cache = TxIndex::new(&last_n_blocks, height); 334 | 335 | // Update the cache with the last block 336 | let full_block = get_full_block(&last_block); 337 | let locator_tx_map = full_block 338 | .txdata 339 | .iter() 340 | .map(|tx| (Locator::new(tx.compute_txid()), tx.clone())) 341 | .collect(); 342 | 343 | let header = full_block.header; 344 | cache.update(header, &locator_tx_map); 345 | 346 | // Check that the new data is in the cache 347 | assert!(cache.blocks().contains(&header.block_hash())); 348 | 349 | for (locator, _) in locator_tx_map.iter() { 350 | assert!(cache.contains_key(locator)); 351 | } 352 | 353 | let block_hash = full_block.header.block_hash(); 354 | assert_eq!( 355 | cache.tx_in_block[&block_hash], 356 | locator_tx_map.keys().cloned().collect::>() 357 | ); 358 | 359 | // Check that the data from the first block has been evicted 360 | let first_full_block = get_full_block(first_block); 361 | let tx = first_full_block.txdata[0].clone(); 362 | assert!(!cache.contains_key(&Locator::new(tx.compute_txid()))); 363 | 364 | let block_hash = first_full_block.header.block_hash(); 365 | assert!(!cache.tx_in_block.contains_key(&block_hash)); 366 | } 367 | 368 | #[tokio::test] 369 | async fn test_remove_disconnected_block() { 370 | let cache_size = 6; 371 | let height = cache_size * 2; 372 | let mut chain = Blockchain::default().with_height_and_txs(height, 42); 373 | let mut cache: TxIndex = TxIndex::new( 374 | &get_last_n_blocks(&mut chain, cache_size).await, 375 | height as u32, 376 | ); 377 | 378 | // TxIndex::fix removes the last connected block and removes all the associated data 379 | for i in 0..cache_size { 380 | let header = chain 381 | .at_height(chain.get_block_count() as usize - i) 382 | .deref() 383 | .header; 384 | let locators = cache.tx_in_block.get(&header.block_hash()).unwrap().clone(); 385 | 386 | // Make sure there's data regarding the target block in the cache before fixing it 387 | assert_eq!(cache.blocks().len(), cache.size - i); 388 | assert!(cache.blocks().contains(&header.block_hash())); 389 | assert!(!locators.is_empty()); 390 | for locator in locators.iter() { 391 | assert!(cache.contains_key(locator)); 392 | } 393 | 394 | cache.remove_disconnected_block(&header.block_hash()); 395 | 396 | // Check that the block data is not in the cache anymore 397 | assert_eq!(cache.blocks().len(), cache.size - i - 1); 398 | assert!(!cache.blocks().contains(&header.block_hash())); 399 | assert!(!cache.tx_in_block.contains_key(&header.block_hash())); 400 | for locator in locators.iter() { 401 | assert!(!cache.contains_key(locator)); 402 | } 403 | } 404 | 405 | // At this point the cache should be empty, fixing it further shouldn't do anything 406 | for i in cache_size..cache_size * 2 { 407 | assert!(cache.index.is_empty()); 408 | assert!(cache.blocks().is_empty()); 409 | assert!(cache.tx_in_block.is_empty()); 410 | 411 | let header = chain 412 | .at_height(chain.get_block_count() as usize - i) 413 | .deref() 414 | .header; 415 | cache.remove_disconnected_block(&header.block_hash()); 416 | } 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /watchtower-plugin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "watchtower-plugin" 3 | version = "0.2.0" 4 | authors = ["Sergi Delgado Segura "] 5 | license = "MIT" 6 | edition = "2021" 7 | 8 | [[bin]] 9 | name = "watchtower-client" 10 | path = "src/main.rs" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | # General 16 | backoff = { version = "0.4.0", features = ["tokio"] } 17 | hex = { version = "0.4.3", features = [ "serde" ] } 18 | home = "0.5.3" 19 | reqwest = { version = "0.11", features = [ "blocking", "json", "socks" ] } 20 | log = "0.4.16" 21 | rusqlite = { version = "0.26.0", features = [ "bundled", "limits" ] } 22 | serde = "1.0.130" 23 | serde_json = { version = "1.0", features = [ "preserve_order" ] } 24 | tonic = { version = "0.11", features = [ "tls", "transport" ] } 25 | tokio = { version = "1.5", features = [ "rt-multi-thread", "fs" ] } 26 | 27 | # Bitcoin and Lightning 28 | bitcoin = "0.32.0" 29 | cln-plugin = "0.3.0" 30 | 31 | # Local 32 | teos-common = { path = "../teos-common" } 33 | 34 | [dev-dependencies] 35 | mockito = "0.32.4" 36 | tempdir = "0.3.7" 37 | -------------------------------------------------------------------------------- /watchtower-plugin/README.md: -------------------------------------------------------------------------------- 1 | # Watchtower client 2 | 3 | This is a watchtower client plugin to interact with an [Eye of Satoshi tower](https://github.com/talaia-labs/rust-teos), 4 | and eventually with any [BOLT13](https://github.com/sr-gi/bolt13/blob/master/13-watchtowers.md) compliant watchtower. 5 | 6 | The plugin manages all the client-side logic to send appointment to a number of registered towers every time a new 7 | commitment transaction is generated. It also keeps a summary of the messages sent to the towers and their responses. 8 | 9 | The plugin has the following methods: 10 | 11 | - `registertower `: registers the user id (compressed public key) with a given tower. 12 | - `gettowerinfo `: gets all the locally stored data about a given tower. 13 | - `retrytower `: tries to send pending appointment to a (previously) unreachable tower. 14 | - `abandontower `: deletes all data associated with a given tower. 15 | - `pingtower `: Polls the tower to check if it is online. 16 | - `listtowers`: lists all registered towers. 17 | - `getappointment `: queries a given tower about an appointment. 18 | - `getsubscriptioninfo `: gets the subscription information by querying the tower. 19 | - `getappointmentreceipt `: pulls a given appointment receipt from the local database. 20 | - `getregistrationreceipt `: pulls the latest registration receipt from the local database. 21 | 22 | The plugin also has an implicit method to send appointments to the registered towers for every new commitment transaction. 23 | 24 | # Installing the plugin and linking it to CLN 25 | 26 | The first step to add the plugin to CLN is installing it. To do so you need to run (from the `rust-teos` folder): 27 | 28 | ``` 29 | cargo install --locked --path watchtower-plugin 30 | ``` 31 | 32 | That will generate a binary called `watchtower-client`. That's the binary we need to link to CLN. 33 | 34 | You can link the plugin either via cmd or by placing it in the plugins folder. 35 | 36 | ### Linking the plugin via cmd 37 | 38 | To link the plugin via cmd simply run `lightningd` specifying the plugin to link, that is: 39 | 40 | ``` 41 | lightningd --plugin=watchtower-client 42 | ``` 43 | 44 | Notice that you'll need to do this every time you restart your node. 45 | 46 | ### Linking the plugin via the plugins folder 47 | 48 | You can also add a plugin by adding it to the plugins folder so you don't have to link it every single time. In order to do so you need to go to the CLN user directory in your machine (that's usually `~/.lightning`). Once there, you need to create a folder called `plugins` if it does not exist: 49 | 50 | ``` 51 | cd ~/.lightning 52 | mkdir plugins && cd plugins 53 | ``` 54 | 55 | Now you need to place the watchtower-client into this folder. To do so we will create a symbolic link to it. First, check where the binary is placed (this is usually placed in your user's home). 56 | 57 | ``` 58 | whereis watchtower-client 59 | > watchtower-client: /.cargo/bin/watchtower-client 60 | ``` 61 | 62 | Notice that here `` will be the path to your user's home directory, for instance `/home/sergi/`. 63 | 64 | Now create a symbolic link to it (make sure to replace the path for your's!): 65 | 66 | ``` 67 | ln -s /.cargo/bin/watchtower-client . 68 | ``` 69 | 70 | If you check the folder you'll see that now there's a link called `watchtower-client`: 71 | 72 | ``` 73 | ls 74 | > watchtower-client 75 | ``` 76 | 77 | You can now turn on your lightning node and the plugin will be automatically linked. 78 | 79 | ### Check that the client is properly linked 80 | 81 | We can check the plugin was properly linked by running: 82 | 83 | ``` 84 | lightning-cli plugin list 85 | ``` 86 | 87 | That will return a list of all linked plugins, which should include the `watchtower-client`: 88 | 89 | ``` 90 | [ 91 | ... 92 | { 93 | "name": "~/.lightning/plugins/watchtower-client", 94 | "active": true 95 | } 96 | ] 97 | ``` 98 | 99 | # Config file, data folder and first bootstrap 100 | 101 | The plugin, by default, creates a data folder under the user's home folder (`~/.watchtower`), where all the plugin's data is stored. The data folder can be modified by setting the ENV variable `TOWERS_DATA_DIR`. 102 | 103 | On first bootstrap, the plugin generates a key pair that is used as the user identifier. All requests from the user are signed using the secret key, so the tower can authenticate the user after the registration process (`registertower`). 104 | 105 | All the appointments generated by the tower, as well as all the registered towers' data, are stored on a `SQLite3` database under the data dir (that's `~/.watchtower/watchtowers_db.sql3` for the default data dir). 106 | 107 | # Core Lightning (CLN) config 108 | 109 | Config options can be setup directly in the [CLN config file](https://github.com/ElementsProject/lightning#configuration-file). The currently available options are: 110 | 111 | - `watchtower-port`: default tower API port. 112 | - `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). 113 | - `watchtower-auto-retry-delay`: how long (in seconds) the client will wait before auto-retrying a failed tower (default: 8 hours). 114 | - `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). 115 | - `always-use-proxy`: Use the proxy always (default: false). 116 | 117 | 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. 118 | 119 | # Getting started 120 | 121 | ## Registering with a tower 122 | 123 | Once the plugin is loaded in your node, the first step is to register your node with an active tower. You can do so by running: 124 | 125 | ``` 126 | lightning-cli registertower tower_id [host, port] 127 | ``` 128 | 129 | Where `tower_id` represents the target tower public key. As a convenience, `tower_id` may be of the form `tower_id@host` or `id@host:port`. In this case, the host and port parameters must be omitted. Port defaults to `9814` and can be changed in the config file. 130 | 131 | ### Example 132 | 133 | ``` 134 | lightning-cli registertower 02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4 135 | ``` 136 | 137 | If the tower is online, you should get back a response similar to this: 138 | 139 | ``` 140 | { 141 | "user_id": "032fd79e4052531955cf3782b09b495a75919317573ba2fb4dca199652595ced2a", 142 | "available_slots": 10000, 143 | "subscription_expiry": 4712 144 | } 145 | ``` 146 | 147 | Where `available_slots` is the amount of free slots the user has available in the tower, `user_id` is the user's public key and `subscription_expiry` is the block height when the subscription expires. Generally speaking, a slot fits an appointment, so in this example the user can send **10000** appointments in roughly **one month**. 148 | 149 | Notice that, ideally, the client and the tower have to agree on the **subscription details** (`available_slots` and `subscription_expiry`). Currently, those depend only on the tower, since it is offering the service for free. However, in the current state, hitting `registertower` again will add another `10000` slots and reset the time to `current_height + roughtly_one_mont_in_blocks`. 150 | 151 | ## Sending data to the tower 152 | Once your node is registered with at least one tower it will start sending appointments to the tower for every commitment transaction update on any of your channels. In the current version of the plugin, everything is sent to every registered tower (**full replication**). There is nothing to be done here, under normal conditions, the plugin takes care of it. 153 | 154 | ## Checking the state of the towers 155 | 156 | To find out more information about registered towers, you can use `list_towers` and `gettowerinfo`: 157 | 158 | ``` 159 | lightning-cli listtowers 160 | ``` 161 | ``` 162 | { 163 | "02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4": { 164 | "net_addr": "http://localhost:9814", 165 | "available_slots": 9996, 166 | "subscription_expiry": 4712, 167 | "status": "reachable", 168 | "pending_appointments": [], 169 | "invalid_appointments": [] 170 | } 171 | } 172 | ``` 173 | 174 | The overview contains the `id` and network address of the tower (`netaddr`), as well as the current `status` and two list of appointments: **pending** and **invalid**. 175 | 176 | The tower has 5 different states: 177 | 178 | - `reachable`: the tower is reachable at the given network address. 179 | - `temporarily unreachable`: the tower is temporarily unreachable, meaning that one of the last requests sent to it has failed. 180 | - `unreachable`: the tower has been unreachable for a while. 181 | - `misbehaving`: the tower has sent us incorrect data. 182 | - `subscription error`: the subscription with the tower has expired or run out of slots. 183 | 184 | The main difference between `temporarily unreachable` and `unreachable` is the amount of time that has passed since we last received a response. If a tower is temporarily unreachable, a backoff strategy is triggered and all the appointments that cannot be delivered are stored under `pending_appointments`. If the tower comes back online within the retry strategy, every pending appointment is sent through and the tower is flagged back as `reachable`. However, if the backoff strategy ends up giving up, the tower is flagged as `unreachable`. 185 | 186 | If the client receives data from a tower that is not properly signed, the tower is flagged as `misbehaving` and it is abandoned, meaning that no more appointments are sent to it. This state should never be reached by honest towers. 187 | 188 | A `subscription error` means that the subscription needs to be renewed (hit `registertower` again). 189 | 190 | Regarding `pending_appointments` and `invalid_appointments` they store the data that is pending to be sent to the tower (for unreachable towers) and the appointments that have been rejected by the tower for being invalid, respectively. The latter should never get populated for honest clients. 191 | 192 | `gettowerinfo` provides more detailed information about the tower: 193 | 194 | **Usage** 195 | 196 | ``` 197 | lightning-cli gettowerinfo tower_id 198 | ``` 199 | 200 | **Call** 201 | 202 | ``` 203 | lightning-cli gettowerinfo 02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4 204 | ``` 205 | 206 | **Return** 207 | 208 | ``` 209 | { 210 | "net_addr": "http://localhost:9814", 211 | "available_slots": 9996, 212 | "subscription_expiry": 4712, 213 | "status": "reachable", 214 | "appointments": { 215 | "b851b8ec05f5809b9a710f7d9d24db6c": "rbxrs8ncqgzyrxkw5h95a64tbeyhmx6wopdtqndktkko3mq8q3tkczjyk19epd713it8warbpnxgk8py6utq87dt16f3qk6ehkjw5c7q", 216 | "10c6f7787fc33d6298fa89fc41f6a0eb": "dhrtt91bbswmmu41nu4quszt7bsxzpfyx84ycfc1yjt73rs8eqpqg3fwqq8q9tff8aqorohueo3bcgqrww1ocef38hdfuhna44ikjife", 217 | "52dc9bd565bdfe227111927e3964d70b": "d9495n3giiof4rq5aqzh4a6fezftnhofwdi1gb7q5mciyq9besdh4xixczitpgo5dxzdnyzzdy4b9i7hd1zcojdgw833975dn8azfc7x", 218 | "e2824d355f711806d38671c19b91110d": "rbxhpeztw74dspxsr3tk7jdekw7cbkt88kfmda4guf5xkmh1tcmeauqf3s15168y8eo438nbpath58qrxsh9usskzmxk8suf1h19meae" 219 | }, 220 | "pending_appointments": { 221 | "062dc0f28ce5b31e6902c87ff1de15ee": { 222 | "encrypted_blob": "e91bf1a1ab097f71976f240fb2d0c036f5b2188f14089dd1960e041b0a4d31a2bcbf9d6bec064a1d81471bdacf1f4d3b7c8d5df280d86a44504a5ee2ebf309adadc4976cc48cef7b94c9a8f17a16f0dcddfd6d0d105621bc519c0f20b46a8335a3a091bf6bfcc813bd4e34e644822bddda81b2a829d8a3b522b4c9b3f4465a6e416ae9ca8c808637cbc51e8d73dfe80cad3a6cc8c5ca018dd8a4cf2edbc02fd5f6cee0aef5ed5411731ef89061272712180c04150652f5bbb1b540ccc72547fe4ca5e92819c3bbb2feeccd7ce8f7b6568dd7f725fefdd64684f63d59e5d719b24a11272c64818b6319c19a261ee9c8a1674eb2e7c7367797893ac8", 223 | "to_self_delay": 42 224 | }, 225 | "ad229060698d4bc2b910a30933b1b50a": { 226 | "encrypted_blob": "5881dce52efc18b698adc4f93b4ba275eb73271645b471227680ddb889ab60870972c4d44278dc55da4502021d9af67fd4e40803a2c9a6b4d2fe1d1f89b93373407302b67d12bb6c90b6e72b073f1c6bb3c69d57e635bfd5ff2b9648812364821b30bd95b3e8b3b2a888da8225e3d4d5cd2cd1cf2705d022b908b6b2d71c155ea50c38e2b3fb45a615c7bc1d61607a9240999c1bf174d6153b4ca7d086586614c99a45d7195c589fda8101ee8801e28b7ccad7c2b5fbda38cfe7b5e8ec13c23b8fa3cc3e6791ea9675f312cd59278ea0434538d15600b7fc905cf5a8371fc93d1e834e16d5c6399127b71c8f5c9ebfc11c5c8d3f72ce92e278f163", 227 | "to_self_delay": 42 228 | } 229 | }, 230 | "invalid_appointments": {} 231 | } 232 | ``` 233 | 234 | Notice that there are is a new field in this report: `appointments`. 235 | 236 | `appointments` contains a collection of `locator:tower_signature` pairs of all the appointments sent and accepted by the tower. 237 | 238 | The report may also contain a `misbehaving_proof` field if the tower has misbehaved (this is not the case for this example). The proof would look as follows: 239 | 240 | ``` 241 | "misbehaving_proof": { 242 | "locator": "3ebd6c5a4d5ec18c815ad9fcda9aac75", 243 | "appointment_receipt": { 244 | "user_signature": "d7efykp63dy69jrtc3r65pssbdhp4335etq3jap1zqk135qmrtyhr8ghbdhw8y8f7nsjgmm9eoyhsfj6yugzq1bu657frmwwrudr9gpt", 245 | "start_block": 391, 246 | "signature": "rd41nsmhtjsawhc9pta1p5na7kmsyk48xttjy4bt3tbkbajboyzfq6mpamkjixs1w7qotocwjg3sxnbzg6uduec4cnahhkmctgddjn8w" 247 | }, 248 | "recovered_id": "02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4" 249 | } 250 | ``` 251 | 252 | Finally, notice how `pending_appointments` now contains all the data about the pending appointments (**the full appointment**). The same applies to `invalid_appointments`. 253 | 254 | ## Manually retrying a tower 255 | If a tower has been flagged as **unreachable** (after the default backoff has failed) or there has been a **subscription error**, the tower won't be tried again until the user manually requests so. This can be managed with the `retrytower` command: 256 | 257 | **Usage** 258 | 259 | ``` 260 | lightning-cli retrytower tower_id 261 | ``` 262 | **Call** 263 | 264 | ``` 265 | lightning-cli retrytower 02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4 266 | ``` 267 | **Return** 268 | 269 | ``` 270 | "Retrying 02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4" 271 | ``` 272 | 273 | Notice that this only works if the tower is **unreachable**. A tower cannot be retried if it is already being retried (**temporarily unreachable**). 274 | 275 | ## Query data from a tower 276 | Data can be queried from a tower to check, for instance, that the tower is keeping it or that it is correct. This can be done using the `getappointment` command: 277 | 278 | **Usage** 279 | 280 | ``` 281 | lightning-cli getappointment tower_id locator 282 | ``` 283 | **Call** 284 | 285 | ``` 286 | lightning-cli getappointment 02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4 b851b8ec05f5809b9a710f7d9d24db6c 287 | ``` 288 | 289 | **Return** 290 | 291 | ``` 292 | { 293 | "appointment": { 294 | "locator": "b851b8ec05f5809b9a710f7d9d24db6c", 295 | "encrypted_blob": "017044dd0686e89bd3cf69777f1fdcb63d13eafa35e1946a0ac1324247ed793f11e27b3ee599bb1676cc98862c1f07d8e5bd29ed51c94c4ea2721a2b6f205f11cbdb1478da413ced585fe5069c6f438e977d325499bdedb985c055eaff00466209007587f20d09d153b537b0b1b6f5b8151384a1ad9f94dfffd5d5f6c2d484bad7d007976fdcaff173b18dbc4e1e24ca2ae29f8ab7e6933468c179f3857c813441e303b2e9e9b7625b19d8460d368f66cf5a7a2f54139ae0a0c9f0ef0c56183734e5dd51289ecb4f046d97e02895373c97e242c71f910c3ed1fc1b32eda4a3c28c73ad7e5fef624094fadb0753c03f8c9a4189a427e721f3ddfc0a", 296 | "to_self_delay": 42 297 | }, 298 | "status": "being_watched" 299 | } 300 | ``` -------------------------------------------------------------------------------- /watchtower-plugin/src/constants.rs: -------------------------------------------------------------------------------- 1 | // Collection of ENV variable names and values 2 | pub const TOWERS_DATA_DIR: &str = "TOWERS_DATA_DIR"; 3 | pub const DEFAULT_TOWERS_DATA_DIR: &str = ".watchtower"; 4 | 5 | /// Collections of plugin option names, default values and descriptions 6 | 7 | pub const WT_PORT: &str = "watchtower-port"; 8 | pub const DEFAULT_WT_PORT: i64 = 9814; 9 | pub const WT_PORT_DESC: &str = "tower API port"; 10 | pub const WT_MAX_RETRY_TIME: &str = "watchtower-max-retry-time"; 11 | pub const DEFAULT_WT_MAX_RETRY_TIME: i64 = 3600; 12 | 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"; 13 | pub const WT_AUTO_RETRY_DELAY: &str = "watchtower-auto-retry-delay"; 14 | pub const DEFAULT_WT_AUTO_RETRY_DELAY: i64 = 28800; 15 | 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"; 16 | pub const DEV_WT_MAX_RETRY_INTERVAL: &str = "dev-watchtower-max-retry-interval"; 17 | pub const DEFAULT_DEV_WT_MAX_RETRY_INTERVAL: i64 = 900; 18 | pub const DEV_WT_MAX_RETRY_INTERVAL_DESC: &str = 19 | "maximum length (in seconds) for a retry interval. Defaults to 15 min"; 20 | 21 | /// Collections of rpc method names and descriptions 22 | 23 | pub const RPC_REGISTER_TOWER: &str = "registertower"; 24 | pub const RPC_REGISTER_TOWER_DESC: &str = 25 | "Registers the client public key (user id) with the tower"; 26 | pub const RPC_GET_REGISTRATION_RECEIPT: &str = "getregistrationreceipt"; 27 | pub const RPC_GET_REGISTRATION_RECEIPT_DESC: &str = 28 | "Gets the latest registration receipt given a tower id"; 29 | pub const RPC_GET_APPOINTMENT: &str = "getappointment"; 30 | pub const RPC_GET_APPOINTMENT_DESC: &str = 31 | "Gets appointment data from the tower given a tower id and a locator"; 32 | pub const RPC_GET_APPOINTMENT_RECEIPT: &str = "getappointmentreceipt"; 33 | pub const RPC_GET_APPOINTMENT_RECEIPT_DESC: &str = 34 | "Gets a (local) appointment receipt given a tower id and a locator"; 35 | pub const RPC_GET_SUBSCRIPTION_INFO: &str = "getsubscriptioninfo"; 36 | pub const RPC_GET_SUBSCRIPTION_INFO_DESC: &str = 37 | "Gets the subscription information directly from the tower"; 38 | pub const RPC_LIST_TOWERS: &str = "listtowers"; 39 | pub const RPC_LIST_TOWERS_DESC: &str = "Lists all registered towers"; 40 | pub const RPC_GET_TOWER_INFO: &str = "gettowerinfo"; 41 | pub const RPC_GET_TOWER_INFO_DESC: &str = "Shows the info about a tower given a tower id"; 42 | pub const RPC_RETRY_TOWER: &str = "retrytower"; 43 | pub const RPC_RETRY_TOWER_DESC: &str = 44 | "Retries to send pending appointment to an unreachable tower"; 45 | pub const RPC_ABANDON_TOWER: &str = "abandontower"; 46 | pub const RPC_ABANDON_TOWER_DESC: &str = "Forgets about a tower and wipes all local data"; 47 | pub const RPC_PING: &str = "pingtower"; 48 | pub const RPC_PING_DESC: &str = "Polls the tower to check if it is online"; 49 | 50 | /// Collections of hook names 51 | 52 | pub const HOOK_COMMITMENT_REVOCATION: &str = "commitment_revocation"; 53 | -------------------------------------------------------------------------------- /watchtower-plugin/src/net/mod.rs: -------------------------------------------------------------------------------- 1 | use cln_plugin::messages; 2 | use serde::Deserialize; 3 | pub mod http; 4 | 5 | #[derive(Clone, Debug, Deserialize)] 6 | pub struct ProxyInfo { 7 | #[serde(flatten)] 8 | /// The proxy data 9 | inner: messages::ProxyInfo, 10 | /// Whether to only send data though Tor or not 11 | pub always_use: bool, 12 | } 13 | 14 | impl ProxyInfo { 15 | pub fn new(proxy: messages::ProxyInfo, always_use: bool) -> Self { 16 | Self { 17 | inner: proxy, 18 | always_use, 19 | } 20 | } 21 | 22 | pub fn get_socks_addr(&self) -> String { 23 | format!("socks5h://{}:{}", self.inner.address, self.inner.port) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /watchtower-plugin/src/ser.rs: -------------------------------------------------------------------------------- 1 | use bitcoin::consensus::encode; 2 | use bitcoin::Transaction; 3 | 4 | use teos_common::appointment::{Appointment, Locator}; 5 | 6 | use hex::FromHex; 7 | use serde::{de, ser::SerializeMap, Deserializer, Serialize, Serializer}; 8 | use std::collections::HashMap; 9 | 10 | pub fn deserialize_tx<'de, D>(deserializer: D) -> Result 11 | where 12 | D: Deserializer<'de>, 13 | { 14 | struct TransactionVisitor; 15 | 16 | impl<'de> de::Visitor<'de> for TransactionVisitor { 17 | type Value = Transaction; 18 | 19 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 20 | formatter.write_str("a hex string containing the transaction") 21 | } 22 | 23 | fn visit_str(self, v: &str) -> Result 24 | where 25 | E: de::Error, 26 | { 27 | let tx = encode::deserialize( 28 | &Vec::from_hex(v).map_err(|_| E::custom("transaction is not hex encoded"))?, 29 | ) 30 | .map_err(|_| E::custom("transaction cannot be deserialized"))?; 31 | Ok(tx) 32 | } 33 | } 34 | 35 | deserializer.deserialize_any(TransactionVisitor) 36 | } 37 | 38 | pub fn serialize_receipts(hm: &HashMap, s: S) -> Result 39 | where 40 | S: Serializer, 41 | { 42 | let mut map = s.serialize_map(Some(hm.len()))?; 43 | for (locator, sig) in hm { 44 | map.serialize_entry(&hex::encode(locator), sig)?; 45 | } 46 | map.end() 47 | } 48 | 49 | #[derive(Serialize)] 50 | struct AppointmentInners { 51 | encrypted_blob: String, 52 | to_self_delay: u32, 53 | } 54 | 55 | pub fn serialize_appointments(v: &Vec, s: S) -> Result 56 | where 57 | S: Serializer, 58 | { 59 | let mut map = s.serialize_map(Some(v.len()))?; 60 | for a in v { 61 | map.serialize_entry( 62 | &hex::encode(a.locator), 63 | &AppointmentInners { 64 | encrypted_blob: hex::encode(&a.encrypted_blob), 65 | to_self_delay: a.to_self_delay, 66 | }, 67 | )?; 68 | } 69 | map.end() 70 | } 71 | -------------------------------------------------------------------------------- /watchtower-plugin/src/test_utils.rs: -------------------------------------------------------------------------------- 1 | use teos_common::appointment::Locator; 2 | use teos_common::protos as common_msgs; 3 | use teos_common::receipts::AppointmentReceipt; 4 | 5 | pub fn get_dummy_add_appointment_response( 6 | locator: Locator, 7 | receipt: &AppointmentReceipt, 8 | ) -> common_msgs::AddAppointmentResponse { 9 | common_msgs::AddAppointmentResponse { 10 | locator: locator.to_vec(), 11 | start_block: receipt.start_block(), 12 | signature: receipt.signature().unwrap(), 13 | available_slots: 21, 14 | subscription_expiry: 1000, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /watchtower-plugin/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import subprocess 3 | 4 | from pyln.testing.fixtures import * # noqa: F401,F403 5 | from pyln.testing.utils import BITCOIND_CONFIG, TailableProc 6 | 7 | WT_PLUGIN = Path("~/.cargo/bin/watchtower-client").expanduser() 8 | TEOSD_CONFIG = { 9 | "btc_network": "regtest", 10 | "polling_delta": 0, 11 | } 12 | 13 | 14 | def write_toml_config(filename, opts): 15 | with open(filename, "w") as f: 16 | for k, v in opts.items(): 17 | if isinstance(v, str): 18 | f.write('{} = "{}"\n'.format(k, v)) 19 | else: 20 | f.write("{} = {}\n".format(k, v)) 21 | 22 | 23 | class TeosCLI: 24 | def __init__(self, directory="/tmp/watchtower-test"): 25 | self.datadir = directory 26 | 27 | def _call(self, method_name, *args): 28 | try: 29 | r = subprocess.run( 30 | ["teos-cli", f"--datadir={self.datadir}/teos", method_name, *args], 31 | capture_output=True, 32 | text=True, 33 | ) 34 | if r.returncode != 0: 35 | result = ValueError(f"Unknown method {method_name}") 36 | else: 37 | result = json.loads(r.stdout) 38 | except json.JSONDecodeError: 39 | result = None 40 | return result 41 | 42 | def __getattr__(self, name): 43 | if name.startswith("__") and name.endswith("__"): 44 | # Prevent RPC calls for non-existing python internal attribute 45 | # access. If someone tries to get an internal attribute 46 | # of RawProxy instance, and the instance does not have this 47 | # attribute, we do not want the bogus RPC call to happen. 48 | raise AttributeError 49 | 50 | # Create a callable to do the actual call 51 | f = lambda *args: self._call(name, *args) # noqa: E731 52 | 53 | # Make debuggers show rather than > 55 | f.__name__ = name 56 | return f 57 | 58 | 59 | class TeosD(TailableProc): 60 | def __init__(self, bitcoind_rpcport, directory="/tmp/watchtower-test"): 61 | self.teos_dir = os.path.join(directory, "teos") 62 | self.prefix = "teosd" 63 | TailableProc.__init__(self, self.teos_dir) 64 | self.cli = TeosCLI(directory) 65 | 66 | if not os.path.exists(self.teos_dir): 67 | os.makedirs(self.teos_dir) 68 | 69 | self.cmd_line = [ 70 | "teosd", 71 | f"--datadir={self.teos_dir}", 72 | f'--btcrpcuser={BITCOIND_CONFIG["rpcuser"]}', 73 | f'--btcrpcpassword={BITCOIND_CONFIG["rpcpassword"]}', 74 | f"--btcrpcport={bitcoind_rpcport}", 75 | ] 76 | 77 | self.conf_file = os.path.join(self.teos_dir, "teos.toml") 78 | write_toml_config(self.conf_file, TEOSD_CONFIG) 79 | 80 | def start(self, overwrite_key=False): 81 | if overwrite_key: 82 | self.cmd_line.append("--overwritekey") 83 | TailableProc.start(self) 84 | self.wait_for_log("Tower ready") 85 | 86 | logging.info("TeosD started") 87 | 88 | def stop(self): 89 | self.cli.stop() 90 | self.wait_for_log("Shutting down tower") 91 | 92 | return TailableProc.stop(self) 93 | 94 | 95 | @pytest.fixture 96 | def teosd(bitcoind, directory): 97 | # Set the user data dir for the watchtower-plugin so it uses a unique one per test. 98 | os.environ["TOWERS_DATA_DIR"] = os.path.join(directory, "watchtower") 99 | 100 | teosd = TeosD(directory=directory, bitcoind_rpcport=bitcoind.rpcport) 101 | teosd.start() 102 | yield teosd 103 | 104 | teosd.stop() 105 | 106 | 107 | @pytest.hookimpl(tryfirst=True, hookwrapper=True) 108 | def pytest_runtest_makereport(item, call): 109 | # execute all other hooks to obtain the report object 110 | outcome = yield 111 | rep = outcome.get_result() 112 | 113 | # set a report attribute for each phase of a call, which can 114 | # be "setup", "call", "teardown" 115 | 116 | setattr(item, "rep_" + rep.when, rep) 117 | 118 | 119 | @pytest.fixture(scope="function", autouse=True) 120 | def log_name(request): 121 | # Here logging is used, you can use whatever you want to use for logs 122 | logging.info("Starting '{}'".format(request.node.name)) 123 | -------------------------------------------------------------------------------- /watchtower-plugin/tests/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "tests" 3 | version = "0.1.2" 4 | description = "watchtower-plugin tests" 5 | authors = ["Sergi Delgado Segura "] 6 | license = "MIT" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.9" 10 | black = "^22.6.0" 11 | 12 | [tool.poetry.dev-dependencies] 13 | pytest = "^7.1.2" 14 | pytest-timeout = "^2.1.0" 15 | pyln-testing = "^24.2.1" 16 | pyln-client = "^24.2.1" 17 | 18 | 19 | [build-system] 20 | requires = ["poetry-core>=1.0.0"] 21 | build-backend = "poetry.core.masonry.api" 22 | -------------------------------------------------------------------------------- /watchtower-plugin/tests/test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from conftest import WT_PLUGIN 3 | 4 | 5 | def change_endianness(x): 6 | """Changes the endianness (from BE to LE and vice versa) of a given value. 7 | 8 | :param x: Given value which endianness will be changed. 9 | :type x: hex str 10 | :return: The opposite endianness representation of the given value. 11 | :rtype: hex str 12 | """ 13 | 14 | b = bytes.fromhex(x) 15 | return b[::-1].hex() 16 | 17 | 18 | def test_watchtower(node_factory, bitcoind, teosd): 19 | """ 20 | Test watchtower hook. 21 | 22 | l1 and l2 open a channel, make a couple of updates and then l1 cheats on 23 | l2 while that one is offline. The watchtower plugin meanwhile stashes all 24 | the penalty transactions and we release the one matching the offending 25 | commitment transaction. 26 | """ 27 | 28 | l1, l2 = node_factory.line_graph( 29 | 2, 30 | opts=[ 31 | {"broken_log": r"Could not find resolution for output [0-9]?: did \*we\* cheat\?"}, 32 | {"plugin": WT_PLUGIN}, 33 | ], 34 | ) 35 | 36 | # We need to register l2 with the tower 37 | tower_id = teosd.cli.gettowerinfo()["tower_id"] 38 | l2.rpc.registertower(tower_id) 39 | 40 | # Force a new commitment 41 | l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"]) 42 | tx = l1.rpc.dev_sign_last_tx(l2.info["id"])["tx"] 43 | 44 | # Now make sure it is out of date 45 | l1.rpc.pay(l2.rpc.invoice(25000000, "lbl2", "desc2")["bolt11"]) 46 | 47 | # l2 stops watching the chain, allowing the watchtower to react 48 | l2.stop() 49 | 50 | # Now l1 cheats 51 | dispute_txid = bitcoind.rpc.sendrawtransaction(tx) 52 | locator = change_endianness(dispute_txid[32:]) 53 | 54 | # Make sure l2's normal penalty_tx doesn't reach the network 55 | l2.daemon.rpcproxy.mock_rpc("sendrawtransaction", lambda _: {"result": None, "error": None, "id": "pytest"}) 56 | l2.start() 57 | 58 | # The tower will react once the dispute gets confirmed. For now it is still watching for it 59 | assert l2.rpc.getappointment(tower_id, locator)["status"] == "being_watched" 60 | 61 | # Confirm the dispute so the tower can react with the penalty 62 | bitcoind.generate_block() 63 | l1.daemon.wait_for_log("State changed from FUNDING_SPEND_SEEN to ONCHAIN") 64 | penalty_txid = bitcoind.rpc.getrawmempool()[0] 65 | 66 | # The channel still exists between the two peers, but it's on chain 67 | assert l1.rpc.listpeerchannels()["channels"][0]["state"] == "ONCHAIN" 68 | assert l2.rpc.getappointment(tower_id, locator)["status"] == "dispute_responded" 69 | 70 | # Generate blocks until the penalty gets irrevocably resolved 71 | for i in range(101): 72 | bitcoind.generate_block() 73 | if i < 100: 74 | assert l2.rpc.getappointment(tower_id, locator)["status"] == "dispute_responded" 75 | else: 76 | # Once the channel gets irrevocably resolved the tower will forget about it 77 | assert l2.rpc.getappointment(tower_id, locator) == { 78 | "error": "Appointment not found", 79 | "error_code": 36, 80 | } 81 | 82 | # Make sure the penalty outputs are in l2's wallet 83 | fund_txids = [o["txid"] for o in l2.rpc.listfunds()["outputs"]] 84 | assert penalty_txid in fund_txids 85 | 86 | 87 | @pytest.mark.timeout(60) 88 | def test_unreachable_watchtower(node_factory, bitcoind, teosd): 89 | # Set the max retry interval to 1 sec so we know how much to wait for the next retry attempt 90 | max_interval_time = 1 91 | l1, l2 = node_factory.line_graph( 92 | 2, 93 | opts=[ 94 | {}, 95 | { 96 | "plugin": WT_PLUGIN, 97 | "dev-watchtower-max-retry-interval": max_interval_time, 98 | }, 99 | ], 100 | ) 101 | 102 | # We need to register l2 with the tower 103 | tower_id = teosd.cli.gettowerinfo()["tower_id"] 104 | l2.rpc.registertower(tower_id) 105 | 106 | # Stop the tower 107 | teosd.stop() 108 | 109 | # Make a new payment with an unreachable tower 110 | l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"]) 111 | assert l2.rpc.gettowerinfo(tower_id)["status"] == "temporary_unreachable" 112 | assert l2.rpc.gettowerinfo(tower_id)["pending_appointments"] 113 | 114 | # Start the tower and check the automatic backoff works 115 | teosd.start() 116 | l2.daemon.wait_for_log(f"Retry strategy succeeded for {tower_id}") 117 | 118 | assert l2.rpc.gettowerinfo(tower_id)["status"] == "reachable" 119 | 120 | 121 | def test_auto_retry_watchtower(node_factory, bitcoind, teosd): 122 | # The plugin is set to give up on retrying straight-away so we can test this fast. 123 | l1, l2 = node_factory.line_graph( 124 | 2, 125 | opts=[ 126 | {}, 127 | { 128 | "plugin": WT_PLUGIN, 129 | "broken_log": r"plugin-watchtower-client: Data was send to an idle retrier. This should have never happened. Please report!.*", 130 | "watchtower-max-retry-time": 1, 131 | "watchtower-auto-retry-delay": 1, 132 | }, 133 | ], 134 | ) 135 | 136 | # We need to register l2 with the tower 137 | tower_id = teosd.cli.gettowerinfo()["tower_id"] 138 | l2.rpc.registertower(tower_id) 139 | 140 | # Stop the tower 141 | teosd.stop() 142 | 143 | # Make a new payment with an unreachable tower 144 | l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"]) 145 | 146 | # Wait until the tower has been flagged as unreachable 147 | l2.daemon.wait_for_log("Starting to idle") 148 | assert l2.rpc.gettowerinfo(tower_id)["status"] == "unreachable" 149 | assert l2.rpc.gettowerinfo(tower_id)["pending_appointments"] 150 | 151 | # Start the tower and retry it 152 | teosd.start() 153 | 154 | l2.daemon.wait_for_log(f"Finished idling. Flagging {tower_id} for retry") 155 | l2.daemon.wait_for_log(f"Retry strategy succeeded for {tower_id}") 156 | assert l2.rpc.gettowerinfo(tower_id)["status"] == "reachable" 157 | 158 | 159 | def test_manually_retry_watchtower(node_factory, bitcoind, teosd): 160 | # The plugin is set to give up on retrying straight-away so we can test this fast. 161 | l1, l2 = node_factory.line_graph( 162 | 2, 163 | opts=[ 164 | {}, 165 | { 166 | "plugin": WT_PLUGIN, 167 | "watchtower-max-retry-time": 0, 168 | }, 169 | ], 170 | ) 171 | 172 | # We need to register l2 with the tower 173 | tower_id = teosd.cli.gettowerinfo()["tower_id"] 174 | l2.rpc.registertower(tower_id) 175 | 176 | # Stop the tower 177 | teosd.stop() 178 | 179 | # Make a new payment with an unreachable tower 180 | l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"]) 181 | 182 | # Wait until the tower has been flagged as unreachable 183 | l2.daemon.wait_for_log("Starting to idle") 184 | assert l2.rpc.gettowerinfo(tower_id)["status"] == "unreachable" 185 | assert l2.rpc.gettowerinfo(tower_id)["pending_appointments"] 186 | 187 | # Start the tower and retry it 188 | teosd.start() 189 | 190 | # Manual retry 191 | l2.rpc.retrytower(tower_id) 192 | l2.daemon.wait_for_log(f"Manually finished idling. Flagging {tower_id} for retry") 193 | l2.daemon.wait_for_log(f"Retry strategy succeeded for {tower_id}") 194 | assert l2.rpc.gettowerinfo(tower_id)["status"] == "reachable" 195 | 196 | 197 | def test_misbehaving_watchtower(node_factory, bitcoind, teosd, directory): 198 | l1, l2 = node_factory.line_graph(2, opts=[{}, {"plugin": WT_PLUGIN}]) 199 | 200 | # We need to register l2 with the tower 201 | tower_id = teosd.cli.gettowerinfo()["tower_id"] 202 | l2.rpc.registertower(tower_id) 203 | 204 | # Restart overwriting the tower private key 205 | teosd.stop() 206 | teosd.start(overwrite_key=True) 207 | 208 | # Make a new payment and check the state 209 | l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"]) 210 | assert l2.rpc.gettowerinfo(tower_id)["status"] == "misbehaving" 211 | assert l2.rpc.gettowerinfo(tower_id)["misbehaving_proof"] 212 | 213 | 214 | def test_get_appointment(node_factory, bitcoind, teosd, directory): 215 | l1, l2 = node_factory.line_graph(2, opts=[{}, {"plugin": WT_PLUGIN}]) 216 | 217 | # We need to register l2 with the tower 218 | tower_id = teosd.cli.gettowerinfo()["tower_id"] 219 | l2.rpc.registertower(tower_id) 220 | 221 | # Force a new commitment 222 | l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"]) 223 | tx = l1.rpc.dev_sign_last_tx(l2.info["id"])["tx"] 224 | 225 | # Now make sure it is out of date 226 | l1.rpc.pay(l2.rpc.invoice(25000000, "lbl2", "desc2")["bolt11"]) 227 | 228 | # Now l1 cheats 229 | dispute_txid = bitcoind.rpc.sendrawtransaction(tx) 230 | locator = change_endianness(dispute_txid[32:]) 231 | 232 | # Check the appointment before mining a block 233 | appointment = l2.rpc.getappointment(tower_id, locator)["appointment"] 234 | assert "locator" in appointment and "encrypted_blob" in appointment and "to_self_delay" in appointment 235 | 236 | # And after. Now this should be a tracker 237 | bitcoind.generate_block() 238 | teosd.wait_for_log("New tracker added") 239 | tracker = l2.rpc.getappointment(tower_id, locator)["appointment"] 240 | assert "dispute_txid" in tracker and "penalty_txid" in tracker and "penalty_rawtx" in tracker 241 | 242 | # Manually stop l2, otherwise the tower may be stopped before the tower client and we may get some BROKEN logs. 243 | l2.stop() 244 | --------------------------------------------------------------------------------