Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
6a3a153
presumably updated common
dzdidi Jan 30, 2025
91469b5
presumably updated teos/src/test_utils
dzdidi Jan 30, 2025
c3024d2
presumably updated teos/src/tx_index
dzdidi Jan 30, 2025
ffaabe4
drop bitcoin to 0.32.0 as is used by bitcoind-rpc
dzdidi Jan 30, 2025
198e191
presumably updated teos/src/tx_carrier
dzdidi Jan 30, 2025
afd2469
presumably getting there!
dzdidi Jan 31, 2025
7e59195
presumably getting even closer
dzdidi Jan 31, 2025
25e8727
build seem to be working
dzdidi Jan 31, 2025
6396d61
cargo fix
dzdidi Jan 31, 2025
fdd24c3
fix cryptography tests
dzdidi Feb 2, 2025
2bc0012
fix tests for dbm
dzdidi Feb 3, 2025
6c127be
fix tests pow header req
dzdidi Feb 3, 2025
af86b0e
increase timeout for fixing test
dzdidi Feb 3, 2025
c006f7d
todo comment
dzdidi Feb 3, 2025
7f57880
fix deprecation warnings
dzdidi Feb 3, 2025
6183904
increase timeout for fixing test
dzdidi Feb 3, 2025
82e0b09
fmt
dzdidi Feb 3, 2025
d13e1ad
fmt, clippy, review
dzdidi Feb 10, 2025
01b4e42
add msrv and tool chain file
dzdidi Feb 10, 2025
de4cf76
revert const to original values, await for idling
dzdidi Feb 10, 2025
9d49657
fmt
dzdidi Feb 10, 2025
73f49ce
ci: cln v24.11.1
dzdidi Feb 11, 2025
7cf1f1e
Add rust toolchain to cln-plugin.yml
Feb 25, 2025
30fc640
import base64 encoder, dont wrap client creation in cli
dzdidi Mar 3, 2025
2ae307e
unfailible sign
dzdidi Mar 3, 2025
49a8c4a
test: txid from_str
dzdidi Mar 3, 2025
dc0191b
nit: rename local variable
dzdidi Mar 3, 2025
1cde45f
remove network mapping in favor of instantiator
dzdidi Mar 3, 2025
48e8b09
fix tx enconding in tracker
dzdidi Mar 3, 2025
fd860a2
test: cleaner target bits
dzdidi Mar 3, 2025
8839a2c
ci: remove sudo in adding bin to path
dzdidi Mar 3, 2025
c9ec4b5
remove leftover todo comment
dzdidi Mar 25, 2025
522414b
block getters test helpers
dzdidi Mar 25, 2025
c84984b
fmt
dzdidi Mar 25, 2025
1deb062
cargo init
dzdidi Feb 5, 2025
3154121
Readme draft. Mix of pseudo code and rust.
dzdidi Feb 5, 2025
9f6972c
lib draft for wt client
dzdidi Feb 6, 2025
9ec88ed
draft based on wt client
dzdidi Feb 6, 2025
dd0a7af
draft based on wt p2
dzdidi Feb 6, 2025
39159c8
Derive deserializaiton for AppointmentReceipt
dzdidi Feb 6, 2025
d4213d7
draft based on wt p3
dzdidi Feb 6, 2025
32133ec
feature flag for storage; sqlite
dzdidi Feb 7, 2025
43beb12
in memory kv store
dzdidi Feb 7, 2025
91e8e0a
in memory kv store p1
dzdidi Feb 9, 2025
71f715e
storage trait draft
dzdidi Feb 11, 2025
90fa5ed
persister trait done
dzdidi Feb 11, 2025
20ad64f
fmt
dzdidi Feb 12, 2025
4d91b92
rename storage to persister
dzdidi Feb 12, 2025
90f119d
fix tests
dzdidi Feb 12, 2025
9c0d75f
rename dbm to sql_storage
dzdidi Feb 12, 2025
962b4f9
storage creator
dzdidi Feb 12, 2025
b0606e2
kv draft
dzdidi Feb 12, 2025
8183c32
mem_store to dyn_store
dzdidi Feb 13, 2025
9474a2b
tower_info serde
dzdidi Feb 13, 2025
9d56f29
kv crud tower_info
dzdidi Feb 13, 2025
8ddc11f
store registration recepies with tower info
dzdidi Feb 13, 2025
a3f0129
update tower record only if registration receip is newer
dzdidi Feb 13, 2025
ca75835
crud appointment locators
dzdidi Feb 13, 2025
b7b960f
store pending and invalid appointments
dzdidi Feb 14, 2025
387377f
crud appointments; tower status based on appointments
dzdidi Feb 14, 2025
977f2da
hack for appointment existance
dzdidi Feb 14, 2025
c8df9a9
new key space
dzdidi Feb 19, 2025
731ccf7
remove sqlite feature
dzdidi Feb 20, 2025
f5acdff
move kv storage creator to mod
dzdidi Feb 20, 2025
e2792cb
remove sqlite
dzdidi Feb 20, 2025
4ccd76b
fix: store appointment with misbehaving proof
dzdidi Feb 20, 2025
3dcf294
fix: cascade delete tower related data upon tower deletion
dzdidi Feb 20, 2025
b43e6ec
store available slots
dzdidi Feb 24, 2025
d4a5b9e
small clean up
dzdidi Feb 24, 2025
81ab116
small clean up
dzdidi Feb 24, 2025
aaebf98
key space manager draft
dzdidi Feb 24, 2025
7f92e93
bincode serde for available slots
dzdidi Feb 24, 2025
b155a57
small cleanups
dzdidi Feb 24, 2025
82044b3
small cleanups
dzdidi Feb 24, 2025
a15b899
key space, name space and kv helpers
dzdidi Feb 24, 2025
079869b
small cleanups
dzdidi Feb 25, 2025
246e297
kv refactoring, encrypt everything
dzdidi Feb 25, 2025
05604a4
clippy fix
dzdidi Feb 25, 2025
1c6998f
rename memory_store to mock_kv
dzdidi Feb 25, 2025
33e57a5
kv cleanup
dzdidi Feb 25, 2025
3afce12
create strorage inside of wt client
dzdidi Feb 25, 2025
c5e128b
add on_commitment_revocation
dzdidi Feb 25, 2025
e930acb
encryptor class
dzdidi Feb 26, 2025
4dbb41c
remove unnecessary unwrap on signatures
dzdidi Mar 20, 2025
f88c9e0
fmt
dzdidi Mar 20, 2025
4860b01
Merge branch 'master' into feat/teos-ldk-client
dzdidi Apr 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
604 changes: 373 additions & 231 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ resolver = "2"
members = [
"teos",
"teos-common",
"teos-ldk-client",
"watchtower-plugin"
]
6 changes: 3 additions & 3 deletions teos-common/src/receipts.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Receipts issued by towers and handed to users as commitment proof.

use serde::Serialize;
use serde::{Deserialize, Serialize};

use bitcoin::secp256k1::SecretKey;

Expand All @@ -19,7 +19,7 @@ use crate::{cryptography, UserId};
/// as long as the user info is still known. That is, if a user has a subscription with range (S, E) and the user renews the subscription
/// before the tower wipes their data, then the tower can create a new receipt with (S, E') for E' > E instead of a second receipt (E, E').
// Notice this only applies as long as there is no gap between the two subscriptions.
#[derive(Serialize, Debug, Eq, PartialEq, Clone)]
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
pub struct RegistrationReceipt {
user_id: UserId,
available_slots: u32,
Expand Down Expand Up @@ -107,7 +107,7 @@ impl RegistrationReceipt {
/// Proof that a certain state was backed up with the tower.
///
/// Appointment receipts can be used alongside a registration receipt that covers it, and on chain data (a breach not being reacted with a penalty), to prove a tower has not reacted to a channel breach.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[derive(Debug, Deserialize, Clone, PartialEq, Eq, Serialize)]
pub struct AppointmentReceipt {
user_signature: String,
start_block: u32,
Expand Down
31 changes: 31 additions & 0 deletions teos-ldk-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "teos-ldk-client"
version = "0.1.0"
edition = "2021"

[dependencies]
# General
backoff = { version = "0.4.0", features = ["tokio"] }
hex = { version = "0.4.3", features = [ "serde" ] }
home = "0.5.3"
reqwest = { version = "0.11", features = [ "blocking", "json", "socks" ] }
log = "0.4.16"
rusqlite = { version = "0.26.0", features = [ "bundled", "limits" ] }
serde = "1.0.130"
serde_json = { version = "1.0", features = [ "preserve_order" ] }
tonic = { version = "0.11", features = [ "tls", "transport" ] }
tokio = { version = "1.5", features = [ "rt-multi-thread", "fs" ] }
bincode = "1.3.3"

# Bitcoin and Lightning
bitcoin = "0.32.0"
lightning = "0.1.0"

# Local
teos-common = { path = "../teos-common" }
chacha20poly1305 = "0.10.1"
rand = "0.9.0"

[dev-dependencies]
mockito = "0.32.4"
tempdir = "0.3.7"
238 changes: 238 additions & 0 deletions teos-ldk-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
# Watchtower LDK client

This is a watchtower client crate to interact with an [Eye of Satoshi tower](https://github.com/talaia-labs/rust-teos), and eventually with any [BOLT13](https://github.com/sr-gi/bolt13/blob/master/13-watchtowers.md) compliant watchtower. It is designed to be integrated into [LDK-Node](https://github.com/lightningdevkit/ldk-node).

The crate manages all the client-side logic to send appointment to a number of registered towers every time a new commitment transaction is generated. It also keeps a summary of the messages sent to the towers and their responses.

The client instance has the following methods:

- `register_tower <tower_id>`: registers the user id (compressed public key) with a given tower.
- `get_tower_info <tower_id>`: gets all the locally stored data about a given tower.
- `retry_tower <tower_id>`: tries to send pending appointment to a (previously) unreachable tower.
- `abandon_tower <tower_id>`: deletes all data associated with a given tower.
- `ping_tower <tower_id>`: Polls the tower to check if it is online.
- `list_towers`: lists all registered towers.
- `get_appointment <tower_id> <locator>`: queries a given tower about an appointment.
- `get_subscription_info <tower_id>`: gets the subscription information by querying the tower.
- `get_appointment_receipt <tower_id> <locator>`: pulls a given appointment receipt from the local database.
- `get_registration_receipt <tower_id>`: pulls the latest registration receipt from the local database.

The general usage idea
- `on_commitment_revocation <wt_client, tx>`: sends appointments to the registered towers for every new commitment transaction.

# Configuration

## User identification and encryptions

The constructor accepts a key pair that will be 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 (`register_tower`).

All the appointments generated by the tower, as well as all the registered towers' data, are encrytped using aforementioned key are stored in a [KVStore](https://docs.rs/lightning/latest/lightning/util/persist/trait.KVStore.html) persisted storage like [VSS](https://github.com/lightningdevkit/vss-server) or [lightning_persister](https://docs.rs/lightning-persister/latest/lightning_persister/index.html)

## Network configuration

Network configuration options currently available options are:

- `max_retry_count`: how many times a retry strategy will try to reach a temporary unreachable tower before giving up.
- `retry_delay`: how long (in seconds) the client will wait before auto-retrying a failed tower.

# Getting started as a developer

## Creating a client instance
```
let teos_client = TeosClient::new(key_pair, storage)
.set_max_retry_count(10)
.set_retry_delay(1)
.build();
```


## Registering with a tower

Once the crate is instantiated, the first step is to register your node with an active tower. You can do so by calling:

```
async teos_client.register_tower(tower_connector: TowerConnector)) -> Result<RegistrationReceipt, Error>;
```

Where `tower_connector` represents the target tower public key. As a convenience, `tower_connector` may be of the form `tower_public_key@host` or `tower_public_key@host:port`. Port defaults to `9814`.

### Example

```
teos_client = register_tower(TowerConnector::new("02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4@127.0.0.1:9814")).await?
```

If the tower is online, you should get back a response similar to this:

```
{
"user_id": "032fd79e4052531955cf3782b09b495a75919317573ba2fb4dca199652595ced2a",
"available_slots": 10000,
"subscription_expiry": 4712
}
```

Where `available_slots` is the amount of free slots the user has available in the tower, `user_id` is the user's public key and `subscription_expiry` is the block height when the subscription expires. Generally speaking, a slot fits an appointment, so in this example the user can send **10000** appointments in roughly **one month**.

Notice that, ideally, the client and the tower have to agree on the **subscription details** (`available_slots` and `subscription_expiry`). Currently, those depend only on the tower, since it is offering the service for free. However, in the current state, hitting `register_tower` again will add another `10000` slots and reset the time to `current_height + roughtly_one_mont_in_blocks`.

Note that this data will be stored internally inside of client's state and it does not require developer to do anything with it.

## Sending data to the tower
Once node is registered with at least one tower it can start sending appointments to the tower for every commitment transaction update on any of your channels using `on_commitment_revocation(tx: CommitmentRevocation) -> Result<(), Error>`. Where `CommitmentRevocation` has the following structure:
```
{
channel_id: String,
commit_num: u32,
commitment_txid: Txid,
penalty_tx: Transaction,
}
```
In the current version, everything is sent to every registered tower (**full replication**). For the end user there is nothing to be done here, under normal conditions, the crate takes care of it.

## Checking the state of the towers

To find out more information about registered towers, you can use `list_towers` and `get_tower_info`:

```
list_towers() -> Vec<TowerInfo>
```
```
{
"public_key": "02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4",
"net_addr": "http://localhost:9814",
"available_slots": 9996,
"subscription_expiry": 4712,
"status": "reachable",
"pending_appointments": [],
"invalid_appointments": []
}
```

The overview contains the `id` and network address of the tower (`netaddr`), as well as the current `status` and two list of appointments: **pending** and **invalid**.

The tower has 5 different statuses:

- `reachable`: the tower is reachable at the given network address.
- `temporarily unreachable`: the tower is temporarily unreachable, meaning that one of the last requests sent to it has failed.
- `unreachable`: the tower has been unreachable for a while.
- `misbehaving`: the tower has sent us incorrect data.
- `subscription error`: the subscription with the tower has expired or run out of slots.

The main difference between `temporarily unreachable` and `unreachable` is the amount of time that has passed since we last received a response. If a tower is temporarily unreachable, a backoff strategy is triggered and all the appointments that cannot be delivered are stored under `pending_appointments`. If the tower comes back online within the retry strategy, every pending appointment is sent through and the tower is flagged back as `reachable`. However, if the backoff strategy ends up giving up, the tower is flagged as `unreachable`.

If the client receives data from a tower that is not properly signed, the tower is flagged as `misbehaving` and it is abandoned, meaning that no more appointments are sent to it. This state should never be reached by honest towers.

A `subscription error` means that the subscription needs to be renewed (hit `registertower` again).

Regarding `pending_appointments` and `invalid_appointments` they store the data that is pending to be sent to the tower (for unreachable towers) and the appointments that have been rejected by the tower for being invalid, respectively. The latter should never get populated for honest clients.

`gettowerinfo` provides more detailed information about the tower:

**Usage**

```
get_tower_info tower_id
```

**Call**

```
get_tower_info 02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4
```

**Return**

```
{
"net_addr": "http://localhost:9814",
"available_slots": 9996,
"subscription_expiry": 4712,
"status": "reachable",
"appointments": {
"b851b8ec05f5809b9a710f7d9d24db6c": "rbxrs8ncqgzyrxkw5h95a64tbeyhmx6wopdtqndktkko3mq8q3tkczjyk19epd713it8warbpnxgk8py6utq87dt16f3qk6ehkjw5c7q",
"10c6f7787fc33d6298fa89fc41f6a0eb": "dhrtt91bbswmmu41nu4quszt7bsxzpfyx84ycfc1yjt73rs8eqpqg3fwqq8q9tff8aqorohueo3bcgqrww1ocef38hdfuhna44ikjife",
"52dc9bd565bdfe227111927e3964d70b": "d9495n3giiof4rq5aqzh4a6fezftnhofwdi1gb7q5mciyq9besdh4xixczitpgo5dxzdnyzzdy4b9i7hd1zcojdgw833975dn8azfc7x",
"e2824d355f711806d38671c19b91110d": "rbxhpeztw74dspxsr3tk7jdekw7cbkt88kfmda4guf5xkmh1tcmeauqf3s15168y8eo438nbpath58qrxsh9usskzmxk8suf1h19meae"
},
"pending_appointments": {
"062dc0f28ce5b31e6902c87ff1de15ee": {
"encrypted_blob": "e91bf1a1ab097f71976f240fb2d0c036f5b2188f14089dd1960e041b0a4d31a2bcbf9d6bec064a1d81471bdacf1f4d3b7c8d5df280d86a44504a5ee2ebf309adadc4976cc48cef7b94c9a8f17a16f0dcddfd6d0d105621bc519c0f20b46a8335a3a091bf6bfcc813bd4e34e644822bddda81b2a829d8a3b522b4c9b3f4465a6e416ae9ca8c808637cbc51e8d73dfe80cad3a6cc8c5ca018dd8a4cf2edbc02fd5f6cee0aef5ed5411731ef89061272712180c04150652f5bbb1b540ccc72547fe4ca5e92819c3bbb2feeccd7ce8f7b6568dd7f725fefdd64684f63d59e5d719b24a11272c64818b6319c19a261ee9c8a1674eb2e7c7367797893ac8",
"to_self_delay": 42
},
"ad229060698d4bc2b910a30933b1b50a": {
"encrypted_blob": "5881dce52efc18b698adc4f93b4ba275eb73271645b471227680ddb889ab60870972c4d44278dc55da4502021d9af67fd4e40803a2c9a6b4d2fe1d1f89b93373407302b67d12bb6c90b6e72b073f1c6bb3c69d57e635bfd5ff2b9648812364821b30bd95b3e8b3b2a888da8225e3d4d5cd2cd1cf2705d022b908b6b2d71c155ea50c38e2b3fb45a615c7bc1d61607a9240999c1bf174d6153b4ca7d086586614c99a45d7195c589fda8101ee8801e28b7ccad7c2b5fbda38cfe7b5e8ec13c23b8fa3cc3e6791ea9675f312cd59278ea0434538d15600b7fc905cf5a8371fc93d1e834e16d5c6399127b71c8f5c9ebfc11c5c8d3f72ce92e278f163",
"to_self_delay": 42
}
},
"invalid_appointments": {}
}
```

Notice that there are is a new field in this report: `appointments`.

`appointments` contains a collection of `locator:tower_signature` pairs of all the appointments sent and accepted by the tower.

The report may also contain a `misbehaving_proof` field if the tower has misbehaved (this is not the case for this example). The proof would look as follows:

```
"misbehaving_proof": {
"locator": "3ebd6c5a4d5ec18c815ad9fcda9aac75",
"appointment_receipt": {
"user_signature": "d7efykp63dy69jrtc3r65pssbdhp4335etq3jap1zqk135qmrtyhr8ghbdhw8y8f7nsjgmm9eoyhsfj6yugzq1bu657frmwwrudr9gpt",
"start_block": 391,
"signature": "rd41nsmhtjsawhc9pta1p5na7kmsyk48xttjy4bt3tbkbajboyzfq6mpamkjixs1w7qotocwjg3sxnbzg6uduec4cnahhkmctgddjn8w"
},
"recovered_id": "02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4"
}
```

Finally, notice how `pending_appointments` now contains all the data about the pending appointments (**the full appointment**). The same applies to `invalid_appointments`.

## Manually retrying a tower (YAGNI ?)
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 `retry_tower` command:

**Usage**

```
retry_tower tower_id
```
**Call**

```
retry_tower 02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4
```
**Return**

```
"Retrying 02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4"
```

Notice that this only works if the tower is **unreachable**. A tower cannot be retried if it is already being retried (**temporarily unreachable**).

## Query data from a tower
Data can be queried from a tower to check, for instance, that the tower is keeping it or that it is correct. This can be done using the `get_appointment` command:

**Usage**

```
get_appointment <tower_id> <locator>
```
**Call**

```
get_appointment 02bd2b759dd8a4fcef0f7d9692c105da8400d5da7942ee039e869fbfb8738ffde4 b851b8ec05f5809b9a710f7d9d24db6c
```

**Return**

```
{
"appointment": {
"locator": "b851b8ec05f5809b9a710f7d9d24db6c",
"encrypted_blob": "017044dd0686e89bd3cf69777f1fdcb63d13eafa35e1946a0ac1324247ed793f11e27b3ee599bb1676cc98862c1f07d8e5bd29ed51c94c4ea2721a2b6f205f11cbdb1478da413ced585fe5069c6f438e977d325499bdedb985c055eaff00466209007587f20d09d153b537b0b1b6f5b8151384a1ad9f94dfffd5d5f6c2d484bad7d007976fdcaff173b18dbc4e1e24ca2ae29f8ab7e6933468c179f3857c813441e303b2e9e9b7625b19d8460d368f66cf5a7a2f54139ae0a0c9f0ef0c56183734e5dd51289ecb4f046d97e02895373c97e242c71f910c3ed1fc1b32eda4a3c28c73ad7e5fef624094fadb0753c03f8c9a4189a427e721f3ddfc0a",
"to_self_delay": 42
},
"status": "being_watched"
}
```
Loading