Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for core lightning watchtower TEoS (The Eye of Satoshi) - server and client #543

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b901f4e
rust-teos: init at 0.1.2
seberm Aug 28, 2022
c5356af
rust-teos: add module
seberm Aug 30, 2022
1541cc8
clightning: add TEoS watchtower plugin
seberm Oct 18, 2022
174124d
teos: add new options and defaults for teos v0.2.0
seberm Mar 3, 2023
fd7e178
teos: fix onion ports to be handled by the nix-bitcoin onion-services…
seberm Jun 28, 2023
e466074
teos: change btc network name to mainnet
seberm Jun 28, 2023
c47eab6
teos: improve the docs in example configuration
seberm Oct 9, 2023
be06246
teos: enable tor in teos conditionally
seberm Oct 9, 2023
d5ae5a4
teos: rename the teos-watchtower-plugin to just teos-watchtower
seberm Oct 9, 2023
e0c5afc
teos: fix the onionServices condition
seberm Oct 9, 2023
9e95d94
teos: just enable onion service for teos, no need to set it public
seberm Oct 9, 2023
a90ced3
teos: fix the onion services port mapping
seberm Oct 9, 2023
aa51b19
teos: add documentation watchtower plugin usage in lightning-cli
seberm Oct 9, 2023
c2bbc7f
teos: tests: improve tests for teos service and teos-cli
seberm Oct 10, 2023
f56346c
teos: tests: add tests for cln watchtower plugin
seberm Oct 10, 2023
2ec0b4f
teos: netns: add possibility of clightning to connect to local teos s…
seberm Oct 12, 2023
25f4db3
teos: final code cleanup
seberm Oct 12, 2023
92113dd
nodeinfo: add teos tower ID
seberm Oct 17, 2023
b1802c3
clightning: watchtower: do not hide the tower data directory
seberm Nov 22, 2023
bd3f0d9
backups: add teos and teos-watchtower to regular backups
seberm Nov 23, 2023
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -77,6 +77,7 @@ NixOS modules ([src](modules/modules.nix))
* [monitor](https://github.com/lightningd/plugins/tree/master/monitor): helps you analyze the health of your peers and channels
* [rebalance](https://github.com/lightningd/plugins/tree/master/rebalance): keeps your channels balanced
* [trustedcoin](https://github.com/nbd-wtf/trustedcoin) ([experimental](docs/services.md#trustedcoin)): replaces bitcoind with trusted public explorers
* [teos-watchtower](https://github.com/talaia-labs/rust-teos/tree/master/watchtower-plugin): watchtower client plugin [to interact](docs/services.md#the-eye-of-satoshi-watchtower-plugin) with an Eye of Satoshi tower
* [zmq](https://github.com/lightningd/plugins/tree/master/zmq): publishes notifications via ZeroMQ to configured endpoints
* [clightning-rest](https://github.com/Ride-The-Lightning/c-lightning-REST): REST server for clightning
* [lnd](https://github.com/lightningnetwork/lnd) with support for announcing an onion service and [static channel backups](https://github.com/lightningnetwork/lnd/blob/master/docs/recovery.md)
@@ -90,6 +91,7 @@ NixOS modules ([src](modules/modules.nix))
* [mempool](https://github.com/mempool/mempool): Bitcoin visualizer, explorer, and API service
* [electrs](https://github.com/romanz/electrs): Electrum server
* [fulcrum](https://github.com/cculianu/Fulcrum): Electrum server (see [the module](modules/fulcrum.nix) for a comparison with electrs)
* [teos](https://github.com/talaia-labs/rust-teos) a Lightning watchtower
* [btcpayserver](https://github.com/btcpayserver/btcpayserver)
* [liquid](https://github.com/elementsproject/elements): federated sidechain
* [JoinMarket](https://github.com/joinmarket-org/joinmarket-clientserver)
35 changes: 35 additions & 0 deletions docs/services.md
Original file line number Diff line number Diff line change
@@ -594,3 +594,38 @@ To work around this and connect via clearnet instead, set this option:
```nix
services.clightning.plugins.trustedcoin.tor.proxy = false;
```

### The Eye of Satoshi watchtower plugin
The basic usage of this plugin you can find in [its documentation](https://github.com/talaia-labs/rust-teos/blob/master/watchtower-plugin/README.md).

The registration of a new tower can be done by calling the command in the
following forms:

```
lightning-cli registertower <tower_id>[@<host>[:<port>]]

or

lightning-cli registertower <tower_id> [<host> [<port>]]
```

The `tower_id` represents the target tower public key. If the target watchtower
server uses TEoS, you can get this id by calling the `teos-cli gettowerinfo`
command.

```
lightning-cli registertower 032f7fa1823ab3c611833f2d61ed7d7e513886f5a6862d33dff71857b9cacb8b45@example.com:9814

or via onion:

lightning-cli registertower 032f7fa1823ab3c611833f2d61ed7d7e513886f5a6862d33dff71857b9cacb8b45 xryayjsnc7j2jhqihd7je7sd2dsi4l33hj5v5xbfnvjjiapdxc4el5ad.onion 9814
```

Please note that the [plugin proxy configuration](https://github.com/talaia-labs/rust-teos/blob/master/watchtower-plugin/README.md#core-lightning-cln-config)
is loaded directly from the clightning. This means you must have a
`services.clightning.tor.proxy` enabled to be able to connect to the tower
behind the Tor. Otherwise you get the following error:

```
Cannot connect to an onion address without a proxy
```
8 changes: 8 additions & 0 deletions examples/configuration.nix
Original file line number Diff line number Diff line change
@@ -236,6 +236,14 @@
# services.charge-lnd.policies = ''
# '';

### THE EYE OF SATOSHI
# Set this to enable TEoS, a Lightning watchtower compliant with BOLT13.
# services.teos.enable = true;
#
# Set this to create an onion service by which teos can accept incoming
# connections via Tor.
# nix-bitcoin.onionServices.teos.enable = true;

seberm marked this conversation as resolved.
Show resolved Hide resolved
### JOINMARKET
# Set this to enable the JoinMarket service, including its command-line scripts.
# These scripts have prefix 'jm-', like 'jm-tumbler'.
2 changes: 2 additions & 0 deletions modules/backups.nix
Original file line number Diff line number Diff line change
@@ -62,6 +62,7 @@ let
''}
${config.services.bitcoind.dataDir}
${config.services.clightning.dataDir}
${config.services.clightning.plugins.teos-watchtower.dataDir}
${config.services.clightning-rest.dataDir}
${config.services.lnd.dataDir}
${optionalString (!cfg.with-bulk-data) ''
@@ -71,6 +72,7 @@ let
''}
${config.services.liquidd.dataDir}
${optionalString cfg.with-bulk-data "${config.services.electrs.dataDir}"}
${optionalString cfg.with-bulk-data "${config.services.teos.dataDir}"}
${config.services.nbxplorer.dataDir}
${config.services.btcpayserver.dataDir}
${config.services.joinmarket.dataDir}
1 change: 1 addition & 0 deletions modules/clightning-plugins/default.nix
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ in {
./clboss.nix
./feeadjuster.nix
./trustedcoin.nix
./teos-watchtower.nix
./zmq.nix
];

51 changes: 51 additions & 0 deletions modules/clightning-plugins/teos-watchtower.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{ config, lib, ... }:

with lib;
let cfg = config.services.clightning.plugins.teos-watchtower; in
{
# Ref.: https://github.com/talaia-labs/rust-teos/tree/master/watchtower-plugin
options.services.clightning.plugins.teos-watchtower = {
enable = mkEnableOption "TEoS watchtower (clightning plugin)";
package = mkOption {
type = types.package;
default = config.nix-bitcoin.pkgs.teos-watchtower-plugin;
defaultText = "config.nix-bitcoin.pkgs.teos-watchtower-plugin";
description = mdDoc "The package providing TEoS watchtower plugin binaries.";
};
port = mkOption {
type = types.port;
default = config.services.teos.port;
description = mdDoc "Tower API port.";
};
dataDir = mkOption {
type = types.path;
default = "${config.services.clightning.dataDir}/watchtower";
description = mdDoc "The data directory for teos-watchtower.";
};
maxRetryTime = mkOption {
type = types.int;
default = 3600;
description = mdDoc "For how long (in seconds) a retry strategy will try to reach a temporary unreachable tower before giving up.";
};
autoRetryDelay = mkOption {
type = types.int;
default = 28800;
description = mdDoc "For how long (in seconds) the client will wait before auto-retrying a failed tower.";
};
};

config = mkIf cfg.enable {
services.clightning.extraConfig = ''
plugin=${cfg.package}/bin/watchtower-client
watchtower-port=${toString cfg.port}
watchtower-max-retry-time=${toString cfg.maxRetryTime}
watchtower-auto-retry-delay=${toString cfg.autoRetryDelay}
'';

# The data directory of teos-watchtower must be specified and must
# be writeable. Otherwise the plugin fails to load.
systemd.services.clightning.environment = {
TOWERS_DATA_DIR = cfg.dataDir;
};
};
}
1 change: 1 addition & 0 deletions modules/modules.nix
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@
./rtl.nix
./mempool.nix
./electrs.nix
./teos.nix
./fulcrum.nix
./liquid.nix
./btcpayserver.nix
13 changes: 12 additions & 1 deletion modules/netns-isolation.nix
Original file line number Diff line number Diff line change
@@ -230,7 +230,8 @@ in {
};
clightning = {
id = 13;
connections = [ "bitcoind" ];
connections = [ "bitcoind" ]
++ optional config.services.teos.enable "teos";
};
lnd = {
id = 14;
@@ -303,6 +304,10 @@ in {
(if (config.services.mempool.electrumServer == "electrs") then "electrs" else "fulcrum")
];
};
teos = {
id = 33;
connections = [ "bitcoind" ];
};
};

services.bitcoind = {
@@ -335,6 +340,12 @@ in {

services.fulcrum.address = netns.fulcrum.address;

services.teos = {
address = netns.teos.address;
rpc.address = netns.teos.address;
internalApi.address = netns.teos.address;
};

services.lightning-loop.rpcAddress = netns.lightning-loop.address;

services.nbxplorer.address = netns.nbxplorer.address;
5 changes: 5 additions & 0 deletions modules/nodeinfo.nix
Original file line number Diff line number Diff line change
@@ -145,6 +145,11 @@ in {
clightning-rest = mkInfo "";
electrs = mkInfo "";
fulcrum = mkInfo "";
teos = mkInfo ''
info["tower_id"] = shell("teos-cli gettowerinfo | jq -r '.tower_id'")
if 'onion_address' in info:
info["id"] = f"{info['tower_id']}@{info['onion_address']}"
'';
btcpayserver = mkInfo "";
liquidd = mkInfo "";
joinmarket-ob-watcher = mkInfo "";
2 changes: 2 additions & 0 deletions modules/presets/enable-tor.nix
Original file line number Diff line number Diff line change
@@ -35,6 +35,7 @@ in {
#
electrs = defaultEnforceTor;
fulcrum = defaultEnforceTor;
teos = defaultEnforceTor;
nbxplorer = defaultEnforceTor;
rtl = defaultEnforceTor;
joinmarket = defaultEnforceTor;
@@ -48,6 +49,7 @@ in {
liquidd.enable = defaultTrue;
electrs.enable = defaultTrue;
fulcrum.enable = defaultTrue;
teos.enable = defaultTrue;
joinmarket-ob-watcher.enable = defaultTrue;
rtl.enable = defaultTrue;
};
180 changes: 180 additions & 0 deletions modules/teos.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
{ config, lib, pkgs, ... }:

with lib;
let
options.services.teos = {
enable = mkEnableOption "Lightning watchtower compliant with BOLT13, written in Rust";
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = mdDoc "Address to listen for API connections.";
};
port = mkOption {
type = types.port;
default = 9814;
description = mdDoc "Port to listen for API connections.";
};

rpc = {
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = mdDoc "Address to listen for RPC connections.";
};
port = mkOption {
type = types.port;
default = 8814;
description = mdDoc "Port to listen for RPC connections.";
};
};

internalApi = {
address = mkOption {
type = types.str;
default = "127.0.0.1";
description = mdDoc "Address to listen for internal API connections.";
};
port = mkOption {
type = types.port;
default = 50051;
description = mdDoc "Port to listen for internal API connections.";
};
};

dataDir = mkOption {
type = types.path;
default = "/var/lib/teos";
description = mdDoc "The data directory for teos.";
};
extraArgs = mkOption {
type = types.separatedString " ";
default = "";
description = mdDoc "Extra command line arguments passed to teosd.";
};
user = mkOption {
type = types.str;
default = "teos";
description = mdDoc "The user as which to run teos.";
};
group = mkOption {
type = types.str;
default = cfg.user;
description = mdDoc "The group as which to run teos.";
};
package = mkOption {
type = types.package;
default = nbPkgs.teos;
defaultText = "config.nix-bitcoin.pkgs.teos";
description = mdDoc "The package providing teos binaries.";
};
cli = mkOption {
readOnly = true;
default = pkgs.writeScriptBin "teos-cli" ''
${cfg.package}/bin/teos-cli --datadir='${cfg.dataDir}' "$@"
'';
defaultText = "(See source)";
description = mdDoc "Binary to connect with the teos instance.";
};
tor.enforce = nbLib.tor.enforce;
};

cfg = config.services.teos;
nbLib = config.nix-bitcoin.lib;
nbPkgs = config.nix-bitcoin.pkgs;

secretsDir = config.nix-bitcoin.secretsDir;
bitcoind = config.services.bitcoind;
in {
inherit options;

config = mkIf cfg.enable {
services.bitcoind = {
enable = true;
listenWhitelisted = true;
};

environment.systemPackages = [ cfg.package (hiPrio cfg.cli) ];

systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
];

systemd.services.teos = {
wantedBy = [ "multi-user.target" ];
requires = [ "bitcoind.service" ];
after = [ "bitcoind.service" ];

# Example configuration file:
# Ref.:
# - https://github.com/talaia-labs/rust-teos/blob/master/teos/src/conf_template.toml
#
# Note about tor support:
# We don't want to enable TOR support in teos configuration file, because
# the `tor_support = true` option would create an additional endpoint to
# the clearnet HTTP API. The optional Tor support is configured by
# onionServices.
# Ref.:
# - https://github.com/talaia-labs/rust-teos/issues/174
preStart = ''
install -m 640 /dev/null teos.toml

cat <<EOF > teos.toml
# API
api_bind = "${cfg.address}"
api_port = ${toString cfg.port}

# Tor
tor_support = false

# RPC
rpc_bind = "${cfg.rpc.address}"
rpc_port = ${toString cfg.rpc.port}

# bitcoind
btc_network = "${bitcoind.makeNetworkName "mainnet" "regtest"}"
btc_rpc_user = "${bitcoind.rpc.users.public.name}"
btc_rpc_password = "$(cat ${secretsDir}/bitcoin-rpcpassword-public)"
btc_rpc_connect = "${bitcoind.rpc.address}"
btc_rpc_port = ${toString bitcoind.rpc.port}

# Flags
debug = false
overwrite_key = false

# General
subscription_slots = 10000
subscription_duration = 4320
expiry_delta = 6
min_to_self_delay = 20
polling_delta = 60

# Internal API
internal_api_bind = "${cfg.internalApi.address}"
internal_api_port = ${toString cfg.internalApi.port}
EOF
'';

serviceConfig = nbLib.defaultHardening // {
WorkingDirectory = cfg.dataDir;
ExecStart = ''
${cfg.package}/bin/teosd \
--datadir='${cfg.dataDir}' \
${cfg.extraArgs}
'';
User = cfg.user;
Group = cfg.group;
Restart = "on-failure";
RestartSec = "10s";
ReadWritePaths = [ cfg.dataDir ];
} // nbLib.allowedIPAddresses cfg.tor.enforce;
};

users.users.${cfg.user} = {
isSystemUser = true;
group = cfg.group;
extraGroups = [ "bitcoinrpc-public" ];
};
users.groups.${cfg.group} = {};
nix-bitcoin.operator.groups = [ cfg.group ];
};
}
2 changes: 2 additions & 0 deletions pkgs/pinned.nix
Original file line number Diff line number Diff line change
@@ -11,6 +11,8 @@ pkgs: pkgsUnstable:
hwi
lightning-loop
lightning-pool
teos
teos-watchtower-plugin
lndconnect;

inherit (pkgsUnstable)
10 changes: 10 additions & 0 deletions test/tests.nix
Original file line number Diff line number Diff line change
@@ -65,6 +65,7 @@ let
pluginPkgs = nbPkgs.clightning-plugins // {
clboss.path = "${plugins.clboss.package}/bin/clboss";
trustedcoin.path = "${plugins.trustedcoin.package}/bin/trustedcoin";
teos-watchtower.path = "${nbPkgs.teos-watchtower-plugin}/bin/watchtower-client";
};
in map (plugin: pluginPkgs.${plugin}.path) enabled;

@@ -116,6 +117,8 @@ let
services.fulcrum.port = 50002;
tests.fulcrum = cfg.fulcrum.enable;

tests.teos = cfg.teos.enable;

tests.liquidd = cfg.liquidd.enable;
services.liquidd.extraConfig = mkIf config.test.noConnections "connect=0";

@@ -173,6 +176,7 @@ let
sendpay-success = tcpEndpoint;
sendpay-failure = tcpEndpoint;
};
teos-watchtower.enable = true;
};
})
];
@@ -218,6 +222,11 @@ let
trezor = true;
ledger = true;
};

# The TEoS mainnet test cannot be enabled due to an unreachable network.
# The height of blockchain must not be 0 (genesis block). Please, use
# regtest instead.
#services.teos.enable = true;
};

secureNode = {
@@ -257,6 +266,7 @@ let
services.fulcrum.enable = true;
services.btcpayserver.enable = true;
services.joinmarket.enable = true;
services.teos.enable = true;
};

# netns and regtest, without secure-node.nix
47 changes: 47 additions & 0 deletions test/tests.py
Original file line number Diff line number Diff line change
@@ -21,6 +21,10 @@ def assert_full_match(cmd, regexp):
def log_has_string(unit, str):
return f"journalctl -b --output=cat -u {unit} --grep='{str}'"

def assert_failure(unit):
"""Unit should have failed"""
machine.succeed(log_has_string(unit, "Failed with result"))

def assert_no_failure(unit):
"""Unit should not have failed since the system is running"""
machine.fail(log_has_string(unit, "Failed with result"))
@@ -118,6 +122,40 @@ def _():
assert_running("fulcrum")
machine.wait_until_succeeds(log_has_string("fulcrum", "started ok"))

@test("teos")
def _():
if not "regtest" in enabled_tests:
# When there is no network during the testing on mainnet, the bitcoin block
# download will stop at genesis block. That's why the teos fails.
machine.wait_until_succeeds(log_has_string("teos", "Last known block: 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"))
assert_failure("teos")
machine.fail("runuser -u operator -- teos-cli gettowerinfo")
return

# teos daemon
assert_running("teos")
wait_for_open_port(ip("teos"), 8814)
wait_for_open_port(ip("teos"), 9814)
wait_for_open_port(ip("teos"), 50051)
machine.wait_until_succeeds(log_has_string("teos", "Tower ready"))

# teos-cli
tower_id = succeed("runuser -u operator -- teos-cli gettowerinfo | jq -jMr '.tower_id'")
machine.succeed(log_has_string("teos", f"tower_id: {tower_id}"))

# teos-watchtower CLN plugin
machine.wait_until_succeeds(log_has_string("clightning", "plugin-watchtower-client: Starting retry manager"))

# Note: The registertower arguments must be provided as URI instead of separated arguments due to a bug:
# Ref.: https://github.com/talaia-labs/rust-teos/issues/244
machine.wait_until_succeeds(f"runuser -u operator -- lightning-cli registertower '{tower_id}@{ip('teos')}:9814'")
machine.wait_until_succeeds(log_has_string("clightning", f"plugin-watchtower-client: Registering in the Eye of Satoshi \(tower_id={tower_id}\)"))
machine.wait_until_succeeds(log_has_string("clightning", "plugin-watchtower-client: Registration succeeded."))

cln_tower_info = succeed("runuser -u operator -- lightning-cli listtowers")
assert_matches(f"echo '{cln_tower_info}' | jq -jMr 'keys[0]'", tower_id)
assert_matches(f"echo '{cln_tower_info}' | jq -jMr '.[].status'", "reachable")

# Impure: Stops electrs
# Stop electrs from spamming the test log with 'waiting for 0 blocks to download' messages
@test("stop-electrs")
@@ -401,7 +439,11 @@ def get_block_height(ip, port):
f" | nc {ip} {port} | head -1 | jq -M .result.height"
)

def get_latest_block_hash():
return succeed("bitcoin-cli getblockchaininfo | jq -jr '.bestblockhash'")

num_blocks = test_data["num_blocks"]
latest_block_hash = get_latest_block_hash()

if enabled("electrs"):
machine.wait_until_succeeds(log_has_string("electrs", "serving Electrum RPC"))
@@ -411,6 +453,11 @@ def get_block_height(ip, port):
machine.wait_until_succeeds(log_has_string("fulcrum", "listening for connections"))
assert_full_match(get_block_height(ip('fulcrum'), 50002), f"{num_blocks}\n")

if enabled("teos"):
machine.wait_until_succeeds(log_has_string("teos", f"Last known block: {latest_block_hash}"))
machine.wait_until_succeeds(log_has_string("teos", "Tower ready"))
succeed("runuser -u operator -- teos-cli gettowerinfo")

if enabled("clightning"):
machine.wait_until_succeeds(
f"[[ $(runuser -u operator -- lightning-cli getinfo | jq -M .blockheight) == {num_blocks} ]]"