mirror of
https://github.com/talaia-labs/rust-teos.git
synced 2026-08-20 13:28:17 +02:00
CLN logs plugin entries as: plugin-<plugin-name> so by calling the binary `watchtower-plugin` the log lines read plugin-watchtower-plugin, which is quite redundant. plugin-watchtower-client seems more adequate.
155 lines
5.5 KiB
Python
155 lines
5.5 KiB
Python
import time
|
|
import pytest
|
|
|
|
|
|
def change_endianness(x):
|
|
"""Changes the endianness (from BE to LE and vice versa) of a given value.
|
|
|
|
:param x: Given value which endianness will be changed.
|
|
:type x: hex str
|
|
:return: The opposite endianness representation of the given value.
|
|
:rtype: hex str
|
|
"""
|
|
|
|
b = bytes.fromhex(x)
|
|
return b[::-1].hex()
|
|
|
|
|
|
@pytest.mark.developer("Requires dev_sign_last_tx")
|
|
def test_watchtower(node_factory, bitcoind, teosd):
|
|
"""
|
|
Test watchtower hook.
|
|
|
|
l1 and l2 open a channel, make a couple of updates and then l1 cheats on
|
|
l2 while that one is offline. The watchtower plugin meanwhile stashes all
|
|
the penalty transactions and we release the one matching the offending
|
|
commitment transaction.
|
|
"""
|
|
|
|
l1, l2 = node_factory.line_graph(2, opts=[{"allow_broken_log": True}, {"plugin": "watchtower-client"}])
|
|
|
|
# We need to register l2 with the tower
|
|
tower_id = teosd.cli.get_tower_info()["tower_id"]
|
|
l2.rpc.registertower(tower_id)
|
|
|
|
# Force a new commitment
|
|
l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"])
|
|
tx = l1.rpc.dev_sign_last_tx(l2.info["id"])["tx"]
|
|
|
|
# Now make sure it is out of date
|
|
l1.rpc.pay(l2.rpc.invoice(25000000, "lbl2", "desc2")["bolt11"])
|
|
|
|
# l2 stops watching the chain, allowing the watchtower to react
|
|
l2.stop()
|
|
|
|
# Now l1 cheats
|
|
dispute_txid = bitcoind.rpc.sendrawtransaction(tx)
|
|
locator = change_endianness(dispute_txid[32:])
|
|
|
|
# Make sure l2's normal penalty_tx doesn't reach the network
|
|
l2.daemon.rpcproxy.mock_rpc("sendrawtransaction", lambda: None)
|
|
l2.start()
|
|
|
|
# The tower will react once the dispute gets confirmed. For now it is still watching for it
|
|
assert l2.rpc.getappointment(tower_id, locator)["status"] == "being_watched"
|
|
|
|
# Confirm the dispute so the tower can react with the penalty
|
|
bitcoind.generate_block(1)
|
|
time.sleep(1)
|
|
penalty_txid = bitcoind.rpc.getrawmempool()[0]
|
|
|
|
# The channel still exists between the two peers, but it's on chain
|
|
assert l1.rpc.listpeers()["peers"][0]["channels"][0]["state"] == "ONCHAIN"
|
|
assert l2.rpc.getappointment(tower_id, locator)["status"] == "dispute_responded"
|
|
|
|
# Generate blocks until the penalty gets irrevocably resolved
|
|
for i in range(100):
|
|
bitcoind.generate_block(1)
|
|
time.sleep(0.1)
|
|
if i < 100:
|
|
assert l2.rpc.getappointment(tower_id, locator)["status"] == "dispute_responded"
|
|
else:
|
|
# Once the channel gets irrevocably resolved the tower will forget about it
|
|
assert l2.rpc.getappointment(tower_id, locator)["status"] == "not_found"
|
|
|
|
# Make sure the penalty outputs are in l2's wallet
|
|
fund_txids = [o["txid"] for o in l2.rpc.listfunds()["outputs"]]
|
|
assert penalty_txid in fund_txids
|
|
|
|
|
|
def test_unreachable_watchtower(node_factory, bitcoind, teosd):
|
|
# Set the max retry interval to 1 sec so we know how much to wait for the next retry attempt
|
|
max_interval_time = 1
|
|
l1, l2 = node_factory.line_graph(
|
|
2,
|
|
opts=[
|
|
{},
|
|
{
|
|
"plugin": "watchtower-client",
|
|
"allow_broken_log": True,
|
|
"dev-watchtower-max-retry-interval": max_interval_time,
|
|
},
|
|
],
|
|
)
|
|
|
|
# We need to register l2 with the tower
|
|
tower_id = teosd.cli.get_tower_info()["tower_id"]
|
|
l2.rpc.registertower(tower_id)
|
|
|
|
# Stop the tower
|
|
teosd.stop()
|
|
|
|
# Make a new payment with an unreachable tower
|
|
l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"])
|
|
assert l2.rpc.gettowerinfo(tower_id)["status"] == "temporary_unreachable"
|
|
assert l2.rpc.gettowerinfo(tower_id)["pending_appointments"]
|
|
|
|
# Start the tower and check the automatic backoff works
|
|
teosd.start()
|
|
time.sleep(max_interval_time * 2)
|
|
assert l2.rpc.gettowerinfo(tower_id)["status"] == "reachable"
|
|
assert not l2.rpc.gettowerinfo(tower_id)["pending_appointments"]
|
|
|
|
|
|
def test_retry_watchtower(node_factory, bitcoind, teosd):
|
|
# The plugin is set to give up on retrying straight-away so we can test this fast.
|
|
l1, l2 = node_factory.line_graph(
|
|
2, opts=[{}, {"plugin": "watchtower-client", "allow_broken_log": True, "watchtower-max-retry-time": 0}]
|
|
)
|
|
|
|
# We need to register l2 with the tower
|
|
tower_id = teosd.cli.get_tower_info()["tower_id"]
|
|
l2.rpc.registertower(tower_id)
|
|
|
|
# Stop the tower
|
|
teosd.stop()
|
|
|
|
# Make a new payment with an unreachable tower
|
|
l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"])
|
|
assert l2.rpc.gettowerinfo(tower_id)["status"] == "unreachable"
|
|
assert l2.rpc.gettowerinfo(tower_id)["pending_appointments"]
|
|
|
|
# Start the tower and retry it
|
|
teosd.start()
|
|
l2.rpc.retrytower(tower_id)
|
|
time.sleep(2)
|
|
|
|
assert l2.rpc.gettowerinfo(tower_id)["status"] == "reachable"
|
|
assert not l2.rpc.gettowerinfo(tower_id)["pending_appointments"]
|
|
|
|
|
|
def test_misbehaving_watchtower(node_factory, bitcoind, teosd, directory):
|
|
l1, l2 = node_factory.line_graph(2, opts=[{}, {"plugin": "watchtower-client", "allow_broken_log": True}])
|
|
|
|
# We need to register l2 with the tower
|
|
tower_id = teosd.cli.get_tower_info()["tower_id"]
|
|
l2.rpc.registertower(tower_id)
|
|
|
|
# Restart overwriting the tower private key
|
|
teosd.stop()
|
|
teosd.start(overwrite_key=True)
|
|
|
|
# Make a new payment and check the state
|
|
l1.rpc.pay(l2.rpc.invoice(25000000, "lbl1", "desc1")["bolt11"])
|
|
assert l2.rpc.gettowerinfo(tower_id)["status"] == "misbehaving"
|
|
assert l2.rpc.gettowerinfo(tower_id)["misbehaving_proof"]
|