forked from ElementsProject/lightning
/
test_eltoo.py
276 lines (204 loc) · 11.5 KB
/
test_eltoo.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
from fixtures import * # noqa: F401,F403
from pyln.client import RpcError, Millisatoshi
from shutil import copyfile
from pyln.testing.utils import SLOW_MACHINE
from utils import (
only_one, sync_blockheight, wait_for, TIMEOUT,
account_balance, first_channel_id, closing_fee, TEST_NETWORK,
scriptpubkey_addr, calc_lease_fee, EXPERIMENTAL_FEATURES,
check_utxos_channel, anchor_expected, check_coin_moves,
check_balance_snaps, mine_funding_to_announce
)
import os
import queue
import pytest
import re
import subprocess
import threading
import time
import unittest
# In msats
SAT = 1000
def test_uncommitted_removal_reestablishment(node_factory, bitcoind):
# Want offering node to disconnect right afer sending off update_xxx_htlc
disconnects = ['+WIRE_UPDATE_FULFILL_HTLC']
l1, l2 = node_factory.line_graph(2,
opts=[{'may_reconnect': True}, {'may_reconnect': True, 'disconnect': disconnects}])
# Pay comment will cause disconnect, but should recover
l1.pay(l2, 100000*1000)
wait_for(lambda: l2.rpc.listpeers()['peers'][0]['channels'][0]['in_fulfilled_msat'] == Millisatoshi(100000000))
def test_uncommitted_addition_reestablishment(node_factory, bitcoind):
# Want offering node to disconnect right afer sending off update_xxx_htlc
disconnects = ['+WIRE_UPDATE_ADD_HTLC']
l1, l2 = node_factory.line_graph(2,
opts=[{'may_reconnect': True, 'disconnect': disconnects}, {'may_reconnect': True}])
# Pay comment will cause disconnect, and payment should fail hard
try:
l1.pay(l2, 100000*1000)
raise Exception('Should have raised RPCError')
except RpcError:
# FIXME better way of waiting for channel to be ready?
time.sleep(5)
pass
# But otherwise be ok on follow-up attempts
l1.pay(l2, 150000*1000)
wait_for(lambda: l2.rpc.listpeers()['peers'][0]['channels'][0]['in_fulfilled_msat'] == Millisatoshi(150000000))
def test_eltoo_offerer_ack_reestablishment(node_factory, bitcoind):
"""Test that channel reestablishment does the expected thing when
update signed ack didn't make it back to offerer. Reestablishment
flow is essentially the offerer getting the ACK back on reconnect """
# Want receiving node to disconnect right before sending off update_signed_ack
disconnects = ['-WIRE_UPDATE_SIGNED_ACK']
l1, l2 = node_factory.line_graph(2,
opts=[{'may_reconnect': True}, {'may_reconnect': True, 'disconnect': disconnects}])
# Pay comment will cause disconnect, but should recover
l1.pay(l2, 100000*SAT)
# Offerer gets new partial sig on reestablishment
l1.daemon.wait_for_log("partial signature reestablish combine our_psig")
wait_for(lambda: l2.rpc.listpeers()['peers'][0]['channels'][0]['in_fulfilled_msat'] == Millisatoshi(100000000))
def test_eltoo_uneven_reestablishment(node_factory, bitcoind):
"""Test that channel reestablishment does the expected thing when
an update signed message was "sent" but not received by the recipient
before disconnect """
# Want offering node to disconnect right before sending off update_signed
# So on reconnect offerer must replay all updates.
disconnects = ['-WIRE_UPDATE_SIGNED']
l1, l2 = node_factory.line_graph(2,
opts=[{'may_reconnect': True, 'disconnect': disconnects}, {'may_reconnect': True}])
# Pay comment will cause disconnect, but should recover
l1.pay(l2, 100000*SAT)
# Offerer sends whole update again
l1.daemon.wait_for_log('Retransmitting update')
l2.daemon.wait_for_log('Received update_sig')
wait_for(lambda: l2.rpc.listpeers()['peers'][0]['channels'][0]['in_fulfilled_msat'] == Millisatoshi(100000000))
def test_eltoo_base_reestablishment(node_factory, bitcoind):
"""Test that channel reestablishment does the expected thing when all prior messages completed """
l1, l2 = node_factory.line_graph(2,
opts=[{'may_reconnect': True}, {'may_reconnect': True}])
# Simple reestblishment where funding is locked
l1.rpc.disconnect(l2.info['id'], force=True)
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
# We should see funding_locked messages be passed around, then
# normal operation
l1.daemon.wait_for_log('Reconnected, and reestablished')
l2.daemon.wait_for_log('Reconnected, and reestablished')
l1_update_tx = l1.rpc.listpeers(l2.info['id'])["peers"][0]["channels"][0]['last_update_tx']
l1_settle_tx = l1.rpc.listpeers(l2.info['id'])["peers"][0]["channels"][0]['last_settle_tx']
l2_update_tx = l2.rpc.listpeers(l1.info['id'])["peers"][0]["channels"][0]['last_update_tx']
l2_settle_tx = l2.rpc.listpeers(l1.info['id'])["peers"][0]["channels"][0]['last_settle_tx']
assert l1_update_tx == l2_update_tx
assert l1_settle_tx == l2_settle_tx
l1_update_details = bitcoind.rpc.decoderawtransaction(l1_update_tx)
l1_settle_details = bitcoind.rpc.decoderawtransaction(l1_settle_tx)
# First update recovered
assert l1_update_details["locktime"] == 500000000
assert l1_settle_details["locktime"] == 500000000
# l1 can pay l2
l1.pay(l2, 100000*SAT)
wait_for(lambda: l2.rpc.listpeers()['peers'][0]['channels'][0]['in_fulfilled_msat'] == Millisatoshi(100000000))
def test_eltoo_unannounced_hop(node_factory, bitcoind):
"""Test eltoo payments work over hops"""
# Make three nodes, two private channels
l1, l2, l3 = node_factory.line_graph(3,
opts=[{}, {}, {}], announce_channels=False) # Channel announcement unsupported, doing private hops)
# l1 can pay l2
l1.pay(l2, 100000*SAT)
# l2 can pay back l1
l1.pay(l2, 5000*SAT)
# l2 can pay l3
l2.pay(l3, 200000*SAT)
# With proper hints exposed,
# l1 can pay l3
scid = l3.rpc.listchannels()['channels'][0]['short_channel_id']
invoice = l3.rpc.invoice(msatoshi=10000, label='hop', description='test', exposeprivatechannels=scid)
l1.rpc.pay(invoice['bolt11'])
wait_for(lambda: l3.rpc.listpeers()['peers'][0]['channels'][0]['in_fulfilled_msat'] == Millisatoshi(200010000))
# Example flags to run test
# DEBUG_SUBD=eltoo_onchaind VALGRIND=0 BITCOIND_TEST_PATH=/home/greg/bitcoin-dev/lightning/eltoo_bitcoind pytest -s tests/test_eltoo.py -k test_eltoo_htlc
@pytest.mark.developer("needs dev-disable-commit-after")
def test_eltoo_htlc(node_factory, bitcoind, executor, chainparams):
"""Test HTLC resolution via eltoo_onchaind after a single successful payment"""
# We track channel balances, to verify that accounting is ok.
coin_mvt_plugin = os.path.join(os.getcwd(), 'tests/plugins/coin_movements.py')
# First we need to get funds to l2, so suppress after second.
# Feerates identical so we don't get gratuitous commit to update them
l1, l2 = node_factory.line_graph(2,
opts=[{'dev-disable-commit-after': 1, # add HTLC once
'may_fail': True,
'feerates': (7500, 7500, 7500, 7500),
'allow_broken_log': True,
'plugin': coin_mvt_plugin},
{'dev-disable-commit-after': 2, # remove HTLC, then later add
'plugin': coin_mvt_plugin}])
channel_id = first_channel_id(l1, l2)
# Move some across to l2. This will cause *2* updates to be sent for
# addition and removal of HTLC
l1.pay(l2, 200000*SAT)
# l1 won't be able to remove next HTLC after offering first addition
l1.daemon.wait_for_log('dev-disable-commit-after: disabling')
assert not l2.daemon.is_in_log('dev-disable-commit-after: disabling')
# Now, this will get stuck due to l1 commit being disabled due to one more update..
t = executor.submit(l2.pay, l1, 100000*SAT)
# Make sure we get partial signature
l1.daemon.wait_for_log('peer_in WIRE_UPDATE_ADD_HTLC')
l1.daemon.wait_for_log('peer_in WIRE_UPDATE_SIGNED')
# They should both have commitments blocked now.
l2.daemon.wait_for_log('dev-disable-commit-after: disabling')
# Both peers have partial sigs for the latest update transaction
l1.daemon.wait_for_log('WIRE_UPDATE_SIGNED_ACK')
l2.daemon.wait_for_log('WIRE_UPDATE_SIGNED_ACK')
# Take our snapshot of complete tx with HTLC.
l1_update_tx = l1.rpc.listpeers(l2.info['id'])["peers"][0]["channels"][0]['last_update_tx']
l1_settle_tx = l1.rpc.listpeers(l2.info['id'])["peers"][0]["channels"][0]['last_settle_tx']
l2_update_tx = l2.rpc.listpeers(l1.info['id'])["peers"][0]["channels"][0]['last_update_tx']
l2_settle_tx = l2.rpc.listpeers(l1.info['id'])["peers"][0]["channels"][0]['last_settle_tx']
assert l1_update_tx == l2_update_tx
assert l1_settle_tx == l2_settle_tx
# Now we really mess things up!
# FIXME we need real anchor CPFP + package relay to pay fees
l1_update_details = bitcoind.rpc.decoderawtransaction(l1_update_tx)
l1_settle_details = bitcoind.rpc.decoderawtransaction(l1_settle_tx)
# N.B. We rely on bitcoin-inquisition imputing 1 sat/vbyte on txs with EAs
bitcoind.rpc.sendrawtransaction(l1_update_tx)
# Mine and mature the update tx
bitcoind.generate_block(6)
# Symmetrical transactions(!), symmetrical state, mostly
l1.daemon.wait_for_log(' to ONCHAIN')
l2.daemon.wait_for_log(' to ONCHAIN')
needle_1 = l1.daemon.logsearch_start
needle_2 = l2.daemon.logsearch_start
# The settle transaction should hit the mempool for both!
l1.wait_for_onchaind_broadcast('ELTOO_SETTLE',
'ELTOO_UPDATE/DELAYED_OUTPUT_TO_US')
l2.wait_for_onchaind_broadcast('ELTOO_SETTLE',
'ELTOO_UPDATE/DELAYED_OUTPUT_TO_US')
assert len(bitcoind.rpc.getrawmempool()) == 1
# We're going to disable transaction relay for the SUCCESS transaction
# To allow us to test broadcast of one transaction at a time
def censoring_sendrawtx(r):
return {'id': r['id'], 'result': {}}
l1.daemon.rpcproxy.mock_rpc('sendrawtransaction', censoring_sendrawtx)
# Mine settle tx, then we should see HTLC timeout resolution hit the mempool by the receiver
bitcoind.generate_block(1)
wait_for(lambda: len(bitcoind.rpc.getrawmempool()) == 1)
timeout_tx = bitcoind.rpc.getrawtransaction(bitcoind.rpc.getrawmempool()[0], 1)
assert len(timeout_tx['vin'][0]['txinwitness']) == 3
l2.wait_for_onchaind_broadcast('ELTOO_HTLC_TIMEOUT',
'ELTOO_SETTLE/OUR_HTLC')
# Stop mining of tx for this next block
bitcoind.rpc.prioritisetransaction(timeout_tx['txid'], 0, -100000000)
# Allow SUCCESS tx to hit mempool next block
l1.daemon.rpcproxy.mock_rpc('sendrawtransaction', None)
bitcoind.generate_block(1)
# Should hit mempool; do the log/pool check
l1.wait_for_onchaind_broadcast('ELTOO_HTLC_SUCCESS',
'ELTOO_SETTLE/THEIR_HTLC')
success_tx = bitcoind.rpc.getrawtransaction(bitcoind.rpc.getrawmempool()[0], 1)
assert len(success_tx['vin'][0]['txinwitness']) == 4
bitcoind.generate_block(1)
# FIXME Check wallet related things, balances
# FIXME The mounds of memleaks
# Mine enough blocks to closed out onchaind
bitcoind.generate_block(99)
l1.daemon.wait_for_log('onchaind complete, forgetting peer')
l2.daemon.wait_for_log('onchaind complete, forgetting peer')