|
|
@ -35,7 +35,7 @@ from electrum import lnmsg |
|
|
|
from electrum.logging import console_stderr_handler, Logger |
|
|
|
from electrum.lnworker import PaymentInfo, RECEIVED |
|
|
|
from electrum.lnonion import OnionFailureCode |
|
|
|
from electrum.lnutil import derive_payment_secret_from_payment_preimage |
|
|
|
from electrum.lnutil import derive_payment_secret_from_payment_preimage, UpdateAddHtlc |
|
|
|
from electrum.lnutil import LOCAL, REMOTE |
|
|
|
from electrum.invoices import PR_PAID, PR_UNPAID |
|
|
|
from electrum.interface import GracefulDisconnect |
|
|
@ -256,7 +256,7 @@ class MockLNWallet(Logger, NetworkRetryManager[LNPeerAddr]): |
|
|
|
|
|
|
|
class MockTransport: |
|
|
|
def __init__(self, name): |
|
|
|
self.queue = asyncio.Queue() |
|
|
|
self.queue = asyncio.Queue() # incoming messages |
|
|
|
self._name = name |
|
|
|
self.peer_addr = None |
|
|
|
|
|
|
@ -265,7 +265,11 @@ class MockTransport: |
|
|
|
|
|
|
|
async def read_messages(self): |
|
|
|
while True: |
|
|
|
yield await self.queue.get() |
|
|
|
data = await self.queue.get() |
|
|
|
if isinstance(data, asyncio.Event): # to artificially delay messages |
|
|
|
await data.wait() |
|
|
|
continue |
|
|
|
yield data |
|
|
|
|
|
|
|
class NoFeaturesTransport(MockTransport): |
|
|
|
""" |
|
|
@ -382,8 +386,14 @@ class TestPeer(TestCaseForTestnet): |
|
|
|
|
|
|
|
super().tearDown() |
|
|
|
|
|
|
|
def prepare_peers(self, alice_channel: Channel, bob_channel: Channel): |
|
|
|
k1, k2 = keypair(), keypair() |
|
|
|
def prepare_peers( |
|
|
|
self, alice_channel: Channel, bob_channel: Channel, |
|
|
|
*, k1: Keypair = None, k2: Keypair = None, |
|
|
|
): |
|
|
|
if k1 is None: |
|
|
|
k1 = keypair() |
|
|
|
if k2 is None: |
|
|
|
k2 = keypair() |
|
|
|
alice_channel.node_id = k2.pubkey |
|
|
|
bob_channel.node_id = k1.pubkey |
|
|
|
t1, t2 = transport_pair(k1, k2, alice_channel.name, bob_channel.name) |
|
|
@ -557,6 +567,130 @@ class TestPeer(TestCaseForTestnet): |
|
|
|
self.assertEqual(alice_channel_0.peer_state, PeerState.BAD) |
|
|
|
self.assertEqual(bob_channel._state, ChannelState.FORCE_CLOSING) |
|
|
|
|
|
|
|
@staticmethod |
|
|
|
def _send_fake_htlc(peer: Peer, chan: Channel) -> UpdateAddHtlc: |
|
|
|
htlc = UpdateAddHtlc(amount_msat=10000, payment_hash=os.urandom(32), cltv_expiry=999, timestamp=1) |
|
|
|
htlc = chan.add_htlc(htlc) |
|
|
|
peer.send_message( |
|
|
|
"update_add_htlc", |
|
|
|
channel_id=chan.channel_id, |
|
|
|
id=htlc.htlc_id, |
|
|
|
cltv_expiry=htlc.cltv_expiry, |
|
|
|
amount_msat=htlc.amount_msat, |
|
|
|
payment_hash=htlc.payment_hash, |
|
|
|
onion_routing_packet=1366 * b"0", |
|
|
|
) |
|
|
|
return htlc |
|
|
|
|
|
|
|
def test_reestablish_replay_messages_rev_then_sig(self): |
|
|
|
""" |
|
|
|
See https://github.com/lightning/bolts/pull/810#issue-728299277 |
|
|
|
|
|
|
|
Rev then Sig |
|
|
|
A B |
|
|
|
<---add----- |
|
|
|
----add----> |
|
|
|
<---sig----- |
|
|
|
----rev----x |
|
|
|
----sig----x |
|
|
|
|
|
|
|
A needs to retransmit: |
|
|
|
----rev--> (note that 'add' can be first too) |
|
|
|
----add--> |
|
|
|
----sig--> |
|
|
|
""" |
|
|
|
chan_AB, chan_BA = create_test_channels() |
|
|
|
k1, k2 = keypair(), keypair() |
|
|
|
# note: we don't start peer.htlc_switch() so that the fake htlcs are left alone. |
|
|
|
async def f(): |
|
|
|
p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(chan_AB, chan_BA, k1=k1, k2=k2) |
|
|
|
async with OldTaskGroup() as group: |
|
|
|
await group.spawn(p1._message_loop()) |
|
|
|
await group.spawn(p2._message_loop()) |
|
|
|
await p1.initialized |
|
|
|
await p2.initialized |
|
|
|
self._send_fake_htlc(p2, chan_BA) |
|
|
|
self._send_fake_htlc(p1, chan_AB) |
|
|
|
p2.transport.queue.put_nowait(asyncio.Event()) # break Bob's incoming pipe |
|
|
|
self.assertTrue(p2.maybe_send_commitment(chan_BA)) |
|
|
|
await p1.received_commitsig_event.wait() |
|
|
|
await group.cancel_remaining() |
|
|
|
# simulating disconnection. recreate transports. |
|
|
|
p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(chan_AB, chan_BA, k1=k1, k2=k2) |
|
|
|
for chan in (chan_AB, chan_BA): |
|
|
|
chan.peer_state = PeerState.DISCONNECTED |
|
|
|
async with OldTaskGroup() as group: |
|
|
|
await group.spawn(p1._message_loop()) |
|
|
|
await group.spawn(p2._message_loop()) |
|
|
|
with self.assertLogs('electrum', level='INFO') as logs: |
|
|
|
async with OldTaskGroup() as group2: |
|
|
|
await group2.spawn(p1.reestablish_channel(chan_AB)) |
|
|
|
await group2.spawn(p2.reestablish_channel(chan_BA)) |
|
|
|
self.assertTrue(any(("alice->bob" in msg and |
|
|
|
"replaying a revoke_and_ack first" in msg) for msg in logs.output)) |
|
|
|
self.assertTrue(any(("alice->bob" in msg and |
|
|
|
"replayed 2 unacked messages. ['update_add_htlc', 'commitment_signed']" in msg) for msg in logs.output)) |
|
|
|
self.assertEqual(chan_AB.peer_state, PeerState.GOOD) |
|
|
|
self.assertEqual(chan_BA.peer_state, PeerState.GOOD) |
|
|
|
raise SuccessfulTest() |
|
|
|
with self.assertRaises(SuccessfulTest): |
|
|
|
run(f()) |
|
|
|
|
|
|
|
def test_reestablish_replay_messages_sig_then_rev(self): |
|
|
|
""" |
|
|
|
See https://github.com/lightning/bolts/pull/810#issue-728299277 |
|
|
|
|
|
|
|
Sig then Rev |
|
|
|
A B |
|
|
|
<---add----- |
|
|
|
----add----> |
|
|
|
----sig----x |
|
|
|
<---sig----- |
|
|
|
----rev----x |
|
|
|
|
|
|
|
A needs to retransmit: |
|
|
|
----add--> |
|
|
|
----sig--> |
|
|
|
----rev--> |
|
|
|
""" |
|
|
|
chan_AB, chan_BA = create_test_channels() |
|
|
|
k1, k2 = keypair(), keypair() |
|
|
|
# note: we don't start peer.htlc_switch() so that the fake htlcs are left alone. |
|
|
|
async def f(): |
|
|
|
p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(chan_AB, chan_BA, k1=k1, k2=k2) |
|
|
|
async with OldTaskGroup() as group: |
|
|
|
await group.spawn(p1._message_loop()) |
|
|
|
await group.spawn(p2._message_loop()) |
|
|
|
await p1.initialized |
|
|
|
await p2.initialized |
|
|
|
self._send_fake_htlc(p2, chan_BA) |
|
|
|
self._send_fake_htlc(p1, chan_AB) |
|
|
|
p2.transport.queue.put_nowait(asyncio.Event()) # break Bob's incoming pipe |
|
|
|
self.assertTrue(p1.maybe_send_commitment(chan_AB)) |
|
|
|
self.assertTrue(p2.maybe_send_commitment(chan_BA)) |
|
|
|
await p1.received_commitsig_event.wait() |
|
|
|
await group.cancel_remaining() |
|
|
|
# simulating disconnection. recreate transports. |
|
|
|
p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(chan_AB, chan_BA, k1=k1, k2=k2) |
|
|
|
for chan in (chan_AB, chan_BA): |
|
|
|
chan.peer_state = PeerState.DISCONNECTED |
|
|
|
async with OldTaskGroup() as group: |
|
|
|
await group.spawn(p1._message_loop()) |
|
|
|
await group.spawn(p2._message_loop()) |
|
|
|
with self.assertLogs('electrum', level='INFO') as logs: |
|
|
|
async with OldTaskGroup() as group2: |
|
|
|
await group2.spawn(p1.reestablish_channel(chan_AB)) |
|
|
|
await group2.spawn(p2.reestablish_channel(chan_BA)) |
|
|
|
self.assertTrue(any(("alice->bob" in msg and |
|
|
|
"replaying a revoke_and_ack last" in msg) for msg in logs.output)) |
|
|
|
self.assertTrue(any(("alice->bob" in msg and |
|
|
|
"replayed 2 unacked messages. ['update_add_htlc', 'commitment_signed']" in msg) for msg in logs.output)) |
|
|
|
self.assertEqual(chan_AB.peer_state, PeerState.GOOD) |
|
|
|
self.assertEqual(chan_BA.peer_state, PeerState.GOOD) |
|
|
|
raise SuccessfulTest() |
|
|
|
with self.assertRaises(SuccessfulTest): |
|
|
|
run(f()) |
|
|
|
|
|
|
|
@needs_test_with_all_chacha20_implementations |
|
|
|
def test_payment(self): |
|
|
|
"""Alice pays Bob a single HTLC via direct channel.""" |
|
|
|