From 07b6f3fde6c2735a6fa86d956fd21d16787bb4a6 Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Tue, 18 Aug 2026 13:47:04 +0200 Subject: [PATCH 1/2] tests: reproduce zero next_commitment_number on advanced channel BOLT #2 says if next_commitment_number is zero we MUST immediately fail the channel and broadcast the latest commitment. On an advanced channel a reset peer sends 0 for both numbers; we warn about the stale revocation_number and never reach the zero check, so the channel stays up. The test funds, pays so next_index is past 1, then injects that reestablish. It fails until the next commit. Reproduces: #9425 Reported-by: Leo Nash (@tankyleo) Signed-off-by: Vincenzo Palazzo --- tests/test_connection.py | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/test_connection.py b/tests/test_connection.py index dd1210c6569a..bc83c2a9f709 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -3050,6 +3050,48 @@ def test_opener_simple_reconnect(node_factory, bitcoind): l1.pay(l2, 200000000) +def test_reestablish_zero_commitment_number(node_factory, bitcoind): + """BOLT #2: next_commitment_number 0 must fail the channel.""" + l2priv = '12' * 32 + l1, l2 = node_factory.get_nodes(2, opts=[{'may_reconnect': True}, + {'dev-force-privkey': l2priv}]) + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + l1.fundchannel(l2, 10**6) + + # Advance so next_index[REMOTE] > 1; otherwise 0 is the + # "we completed opening" case already handled in-tree. + l1.pay(l2, 10**8) + + channel_id = first_channel_id(l1, l2) + l2_id = l2.info['id'] + l2.stop() + wait_for(lambda: l1.rpc.getpeer(l2_id)['connected'] is False) + + lc = wire.connect(wire.PrivateKey(bytes.fromhex(l2priv)), + wire.PublicKey(bytes.fromhex(l1.info['id'])), + '127.0.0.1', l1.port) + init = lc.read_message() + assert int.from_bytes(init[:2], 'big') == 16 + lc.send_message(init) + + while True: + msg = lc.read_message() + if int.from_bytes(msg[:2], 'big') == 136: + break + + # channel_reestablish: both numbers 0, dummy secret and point. + lc.send_message(bytes.fromhex('0088') + + bytes.fromhex(channel_id) + + (0).to_bytes(8, 'big') + + (0).to_bytes(8, 'big') + + bytes(32) + + bytes.fromhex(l2_id)) + + l1.daemon.wait_for_log('bad reestablish commitment_number: 0') + l1.daemon.wait_for_log('State changed from CHANNELD_NORMAL to AWAITING_UNILATERAL') + l1.wait_for_channel_onchain(l2_id) + + @unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "sqlite3-specific DB rollback") @pytest.mark.openchannel('v1') @pytest.mark.openchannel('v2') From bb2efbc494fabc036971e0dff7d2132ffbb8be89 Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Tue, 18 Aug 2026 13:47:04 +0200 Subject: [PATCH 2/2] channeld: fail channel on zero next_commitment_number The existing BOLT #2 quote already required this, but the first bullet was unimplemented: the only zero check sat under next_commitment_number == next_index[REMOTE] - 1, after the stale-revocation warning. Implement that bullet next to the quote, and drop the nested copy. Fixes: #9425 Changelog-Fixed: Protocol: immediately fail the channel if channel_reestablish next_commitment_number is zero. Reported-by: Leo Nash (@tankyleo) Signed-off-by: Vincenzo Palazzo --- channeld/channeld.c | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/channeld/channeld.c b/channeld/channeld.c index 3848f0d2e837..eaf4efea0a97 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -5990,6 +5990,11 @@ static void peer_reconnect(struct peer *peer, * - MUST NOT retransmit `channel_ready`, but MAY send `channel_ready` with * a different `short_channel_id` `alias` field. */ + if (next_commitment_number == 0) + peer_failed_err(peer->pps, + &peer->channel_id, + "bad reestablish commitment_number: %"PRIu64, + next_commitment_number); if (peer->channel_ready[LOCAL] && peer->next_index[LOCAL] == 1 @@ -6102,14 +6107,6 @@ static void peer_reconnect(struct peer *peer, * `commitment_signed`. */ if (next_commitment_number == peer->next_index[REMOTE] - 1) { - /* We completed opening, we don't re-transmit that one! */ - if (next_commitment_number == 0) - peer_failed_err(peer->pps, - &peer->channel_id, - "bad reestablish commitment_number: %" - PRIu64, - next_commitment_number); - if (!recv_tlvs || !recv_tlvs->next_funding) retransmit_commitment_signed = true; else