From f202fb3553819361d6ef3bc8b825bb82a923d789 Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Tue, 15 Mar 2022 17:47:26 +0100 Subject: [PATCH] bolt2: coop-close channel between two nodes Signed-off-by: Vincenzo Palazzo --- lnprototest/__init__.py | 1 + lnprototest/clightning/clightning.py | 54 +++--- lnprototest/dummyrunner.py | 65 ++++--- lnprototest/event.py | 14 ++ tests/helpers.py | 13 +- tests/test_bolt2-01-close_channel.py | 273 +++++++++++++++++++++++++++ 6 files changed, 359 insertions(+), 61 deletions(-) create mode 100644 tests/test_bolt2-01-close_channel.py diff --git a/lnprototest/__init__.py b/lnprototest/__init__.py index 76cce01..92ad091 100644 --- a/lnprototest/__init__.py +++ b/lnprototest/__init__.py @@ -35,6 +35,7 @@ negotiated, DualFundAccept, Wait, + CloseChannel, ) from .structure import Sequence, OneOf, AnyOrder, TryAll from .runner import ( diff --git a/lnprototest/clightning/clightning.py b/lnprototest/clightning/clightning.py index 6cf1500..c06d8b9 100644 --- a/lnprototest/clightning/clightning.py +++ b/lnprototest/clightning/clightning.py @@ -77,8 +77,8 @@ def __init__(self, config: Any): stdout=subprocess.PIPE, check=True, ) - .stdout.decode("utf-8") - .splitlines() + .stdout.decode("utf-8") + .splitlines() ) self.options: Dict[str, str] = {} for o in opts: @@ -232,12 +232,12 @@ def recv(self, event: Event, conn: Conn, outbuf: bytes) -> None: raise EventError(event, "Connection closed") def fundchannel( - self, - event: Event, - conn: Conn, - amount: int, - feerate: int = 253, - expect_fail: bool = False, + self, + event: Event, + conn: Conn, + amount: int, + feerate: int = 253, + expect_fail: bool = False, ) -> None: """ event - the event which cause this, for error logging @@ -257,11 +257,11 @@ def fundchannel( self.fundchannel_future = None def _fundchannel( - runner: Runner, - conn: Conn, - amount: int, - feerate: int, - expect_fail: bool = False, + runner: Runner, + conn: Conn, + amount: int, + feerate: int, + expect_fail: bool = False, ) -> str: peer_id = conn.pubkey.format().hex() # Need to supply feerate here, since regtest cannot estimate fees @@ -285,14 +285,14 @@ def _done(fut: Any) -> None: self.cleanup_callbacks.append(self.kill_fundchannel) def init_rbf( - self, - event: Event, - conn: Conn, - channel_id: str, - amount: int, - utxo_txid: str, - utxo_outnum: int, - feerate: int, + self, + event: Event, + conn: Conn, + channel_id: str, + amount: int, + utxo_txid: str, + utxo_outnum: int, + feerate: int, ) -> None: if self.fundchannel_future: @@ -365,7 +365,7 @@ def addhtlc(self, event: Event, conn: Conn, amount: int, preimage: str) -> None: self.rpc.sendpay([routestep], payhash) def get_output_message( - self, conn: Conn, event: Event, timeout: int = TIMEOUT + self, conn: Conn, event: Event, timeout: int = TIMEOUT ) -> Optional[bytes]: fut = self.executor.submit(cast(CLightningConn, conn).connection.read_message) try: @@ -382,11 +382,11 @@ def check_error(self, event: Event, conn: Conn) -> Optional[str]: return msg.hex() def check_final_error( - self, - event: Event, - conn: Conn, - expected: bool, - must_not_events: List[MustNotMsg], + self, + event: Event, + conn: Conn, + expected: bool, + must_not_events: List[MustNotMsg], ) -> None: if not expected: # Inject raw packet to ensure it hangs up *after* processing all previous ones. diff --git a/lnprototest/dummyrunner.py b/lnprototest/dummyrunner.py index 41324cd..9b88d89 100644 --- a/lnprototest/dummyrunner.py +++ b/lnprototest/dummyrunner.py @@ -16,7 +16,6 @@ class DummyRunner(Runner): - def __init__(self, config: Any): super().__init__(config) @@ -87,12 +86,12 @@ def recv(self, event: Event, conn: Conn, outbuf: bytes) -> None: print("[RECV {} {}]".format(event, outbuf.hex())) def fundchannel( - self, - event: Event, - conn: Conn, - amount: int, - feerate: int = 253, - expect_fail: bool = False, + self, + event: Event, + conn: Conn, + amount: int, + feerate: int = 253, + expect_fail: bool = False, ) -> None: if self.config.getoption("verbose"): print( @@ -102,14 +101,14 @@ def fundchannel( ) def init_rbf( - self, - event: Event, - conn: Conn, - channel_id: str, - amount: int, - utxo_txid: str, - utxo_outnum: int, - feerate: int, + self, + event: Event, + conn: Conn, + channel_id: str, + amount: int, + utxo_txid: str, + utxo_outnum: int, + feerate: int, ) -> None: if self.config.getoption("verbose"): print( @@ -144,21 +143,21 @@ def fake_field(ftype: FieldType) -> str: if ftype.elemtype.name == "byte": return "00" * ftype.arraysize return ( - "[" - + ",".join([DummyRunner.fake_field(ftype.elemtype)] * ftype.arraysize) - + "]" + "[" + + ",".join([DummyRunner.fake_field(ftype.elemtype)] * ftype.arraysize) + + "]" ) elif ftype.name in ( - "byte", - "u8", - "u16", - "u32", - "u64", - "tu16", - "tu32", - "tu64", - "bigsize", - "varint", + "byte", + "u8", + "u16", + "u32", + "u64", + "tu16", + "tu32", + "tu64", + "bigsize", + "varint", ): return "0" elif ftype.name in ("chain_hash", "channel_id", "sha256"): @@ -201,11 +200,11 @@ def check_error(self, event: Event, conn: Conn) -> Optional[str]: return "Dummy error" def check_final_error( - self, - event: Event, - conn: Conn, - expected: bool, - must_not_events: List[MustNotMsg], + self, + event: Event, + conn: Conn, + expected: bool, + must_not_events: List[MustNotMsg], ) -> None: pass diff --git a/lnprototest/event.py b/lnprototest/event.py index 14f5965..dc0c766 100644 --- a/lnprototest/event.py +++ b/lnprototest/event.py @@ -532,6 +532,20 @@ def action(self, runner: "Runner") -> bool: return True +class CloseChannel(Event): + """Implementing the lnprototest event related to the + close channel operation. + BOLT 2""" + + def __init__(self, channel_id: str): + super(CloseChannel, self).__init__() + self.channel_id = channel_id + + def action(self, runner: "Runner") -> bool: + super().action(runner) + return runner.close_channel(self.channel_id) + + def msg_to_stash(runner: "Runner", event: Event, msg: Message) -> None: """ExpectMsg and Msg save every field to the stash, in order""" fields = msg.to_py() diff --git a/tests/helpers.py b/tests/helpers.py index 227a338..46f5d5c 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,7 +1,7 @@ import bitcoin.core import coincurve from typing import Tuple -from lnprototest import privkey_expand +from lnprototest import privkey_expand, KeySet # Here are the keys to spend funds, derived from BIP32 seed # `0000000000000000000000000000000000000000000000000000000000000001`: @@ -134,3 +134,14 @@ def pubkey_of(privkey: str) -> str: return ( coincurve.PublicKey.from_secret(privkey_expand(privkey).secret).format().hex() ) + + +def gen_random_keyset(counter: int = 20) -> KeySet: + """Helper function to generate a random keyset with the possibility.""" + return KeySet( + revocation_base_secret=f"{counter + 1}", + payment_base_secret=f"{counter + 2}", + htlc_base_secret=f"{counter + 3}", + delayed_payment_base_secret=f"{counter + 4}", + shachain_seed="00" * 32, + ) diff --git a/tests/test_bolt2-01-close_channel.py b/tests/test_bolt2-01-close_channel.py new file mode 100644 index 0000000..b079add --- /dev/null +++ b/tests/test_bolt2-01-close_channel.py @@ -0,0 +1,273 @@ +#! /usr/bin/env python3 +# Variations on open_channel, accepter + opener perspectives + +from lnprototest import ( + TryAll, + Connect, + Block, + FundChannel, + ExpectMsg, + ExpectTx, + Msg, + RawMsg, + AcceptFunding, + CreateFunding, + Commit, + Runner, + remote_funding_pubkey, + remote_revocation_basepoint, + remote_payment_basepoint, + remote_htlc_basepoint, + remote_per_commitment_point, + remote_delayed_payment_basepoint, + Side, + CheckEq, + msat, + remote_funding_privkey, + regtest_hash, + bitfield, + CloseChannel, + Wait, + OneOf, +) +from lnprototest.stash import ( + sent, + rcvd, + commitsig_to_send, + commitsig_to_recv, + channel_id, + funding_txid, + funding_tx, + funding, +) +from helpers import ( + utxo, + tx_spendable, + funding_amount_for_utxo, + pubkey_of, + gen_random_keyset, +) + + +def test_close_channel_shutdown_msg(runner: Runner) -> None: + """Close the channel with the other peer, and check if the + shutdown message works in the expected way.""" + local_funding_privkey = "20" + + local_keyset = gen_random_keyset() + + test = [ + Block(blockheight=102, txs=[tx_spendable]), + Connect(connprivkey="02"), + ExpectMsg("init"), + TryAll( + # BOLT-a12da24dd0102c170365124782b46d9710950ac1 #9: + # | 20/21 | `option_anchor_outputs` | Anchor outputs + Msg("init", globalfeatures="", features=bitfield(13, 21)), + # BOLT #9: + # | 12/13 | `option_static_remotekey` | Static key for remote output + Msg("init", globalfeatures="", features=bitfield(13)), + # And not. + Msg("init", globalfeatures="", features=""), + ), + TryAll( + # Accepter side: we initiate a new channel. + [ + Msg( + "open_channel", + chain_hash=regtest_hash, + temporary_channel_id="00" * 32, + funding_satoshis=funding_amount_for_utxo(0), + push_msat=0, + dust_limit_satoshis=546, + max_htlc_value_in_flight_msat=4294967295, + channel_reserve_satoshis=9998, + htlc_minimum_msat=0, + feerate_per_kw=253, + # We use 5, because c-lightning runner uses 6, so this is different. + to_self_delay=5, + max_accepted_htlcs=483, + funding_pubkey=pubkey_of(local_funding_privkey), + revocation_basepoint=local_keyset.revocation_basepoint(), + payment_basepoint=local_keyset.payment_basepoint(), + delayed_payment_basepoint=local_keyset.delayed_payment_basepoint(), + htlc_basepoint=local_keyset.htlc_basepoint(), + first_per_commitment_point=local_keyset.per_commit_point(0), + channel_flags=1, + ), + # Ignore unknown odd messages + TryAll([], RawMsg(bytes.fromhex("270F"))), + ExpectMsg( + "accept_channel", + temporary_channel_id=sent(), + funding_pubkey=remote_funding_pubkey(), + revocation_basepoint=remote_revocation_basepoint(), + payment_basepoint=remote_payment_basepoint(), + delayed_payment_basepoint=remote_delayed_payment_basepoint(), + htlc_basepoint=remote_htlc_basepoint(), + first_per_commitment_point=remote_per_commitment_point(0), + minimum_depth=3, + channel_reserve_satoshis=9998, + ), + # Ignore unknown odd messages + TryAll([], RawMsg(bytes.fromhex("270F"))), + # Create and stash Funding object and FundingTx + CreateFunding( + *utxo(0), + local_node_privkey="02", + local_funding_privkey=local_funding_privkey, + remote_node_privkey=runner.get_node_privkey(), + remote_funding_privkey=remote_funding_privkey() + ), + Commit( + funding=funding(), + opener=Side.local, + local_keyset=local_keyset, + local_to_self_delay=rcvd("to_self_delay", int), + remote_to_self_delay=sent("to_self_delay", int), + local_amount=msat(sent("funding_satoshis", int)), + remote_amount=0, + local_dust_limit=546, + remote_dust_limit=546, + feerate=253, + local_features=sent("init.features"), + remote_features=rcvd("init.features"), + ), + Msg( + "funding_created", + temporary_channel_id=rcvd(), + funding_txid=funding_txid(), + funding_output_index=0, + signature=commitsig_to_send(), + ), + ExpectMsg( + "funding_signed", + channel_id=channel_id(), + signature=commitsig_to_recv(), + ), + # Mine it and get it deep enough to confirm channel. + Block(blockheight=103, number=3, txs=[funding_tx()]), + ExpectMsg( + "funding_locked", + channel_id=channel_id(), + next_per_commitment_point="032405cbd0f41225d5f203fe4adac8401321a9e05767c5f8af97d51d2e81fbb206", + ), + Msg( + "funding_locked", + channel_id=channel_id(), + next_per_commitment_point="027eed8389cf8eb715d73111b73d94d2c2d04bf96dc43dfd5b0970d80b3617009d", + ), + # Ignore unknown odd messages + TryAll([], RawMsg(bytes.fromhex("270F"))), + ], + # Now we test the 'opener' side of an open_channel (node initiates) + [ + FundChannel(amount=999877), + # This gives a channel of 999877sat + ExpectMsg( + "open_channel", + chain_hash=regtest_hash, + funding_satoshis=999877, + push_msat=0, + dust_limit_satoshis=546, + htlc_minimum_msat=0, + channel_reserve_satoshis=9998, + to_self_delay=6, + funding_pubkey=remote_funding_pubkey(), + revocation_basepoint=remote_revocation_basepoint(), + payment_basepoint=remote_payment_basepoint(), + delayed_payment_basepoint=remote_delayed_payment_basepoint(), + htlc_basepoint=remote_htlc_basepoint(), + first_per_commitment_point=remote_per_commitment_point(0), + # FIXME: Check more fields! + channel_flags="01", + ), + Msg( + "accept_channel", + temporary_channel_id=rcvd(), + dust_limit_satoshis=546, + max_htlc_value_in_flight_msat=4294967295, + channel_reserve_satoshis=9998, + htlc_minimum_msat=0, + minimum_depth=3, + max_accepted_htlcs=483, + # We use 5, because c-lightning runner uses 6, so this is different. + to_self_delay=5, + funding_pubkey=pubkey_of(local_funding_privkey), + revocation_basepoint=local_keyset.revocation_basepoint(), + payment_basepoint=local_keyset.payment_basepoint(), + delayed_payment_basepoint=local_keyset.delayed_payment_basepoint(), + htlc_basepoint=local_keyset.htlc_basepoint(), + first_per_commitment_point=local_keyset.per_commit_point(0), + ), + # Ignore unknown odd messages + TryAll([], RawMsg(bytes.fromhex("270F"))), + ExpectMsg( + "funding_created", temporary_channel_id=rcvd("temporary_channel_id") + ), + # Now we can finally stash the funding information. + AcceptFunding( + rcvd("funding_created.funding_txid"), + funding_output_index=rcvd( + "funding_created.funding_output_index", int + ), + funding_amount=rcvd("open_channel.funding_satoshis", int), + local_node_privkey="02", + local_funding_privkey=local_funding_privkey, + remote_node_privkey=runner.get_node_privkey(), + remote_funding_privkey=remote_funding_privkey(), + ), + Commit( + funding=funding(), + opener=Side.remote, + local_keyset=local_keyset, + local_to_self_delay=rcvd("open_channel.to_self_delay", int), + remote_to_self_delay=sent("accept_channel.to_self_delay", int), + local_amount=0, + remote_amount=msat(rcvd("open_channel.funding_satoshis", int)), + local_dust_limit=sent("accept_channel.dust_limit_satoshis", int), + remote_dust_limit=rcvd("open_channel.dust_limit_satoshis", int), + feerate=rcvd("open_channel.feerate_per_kw", int), + local_features=sent("init.features"), + remote_features=rcvd("init.features"), + ), + # Now we've created commit, we can check sig is valid! + CheckEq(rcvd("funding_created.signature"), commitsig_to_recv()), + Msg( + "funding_signed", + channel_id=channel_id(), + signature=commitsig_to_send(), + ), + # It will broadcast tx + ExpectTx(rcvd("funding_created.funding_txid")), + # Mine three blocks to confirm channel. + Block(blockheight=103, number=3), + Msg( + "funding_locked", + channel_id=sent(), + next_per_commitment_point=local_keyset.per_commit_point(1), + ), + ExpectMsg( + "funding_locked", + channel_id=sent(), + next_per_commitment_point=remote_per_commitment_point(1), + ), + # Ignore unknown odd messages + TryAll([], RawMsg(bytes.fromhex("270F"))), + # After the funding locked the node update with the + # channel data the other side with a channel update. + ExpectMsg("channel_update"), + Block(blockheight=106, number=6), + # TODO this should be optional + ExpectMsg("announcement_signatures"), + CloseChannel(channel_id=sent()), + ExpectMsg( + "shutdown", + # TODO: injects the scriptpubkey here + channel_id=sent(), + ), + ], + ), + ] + + runner.run(test)