From 8d43e01d2406fcd1d36b21813f133ad5bbb7787a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Thu, 23 Apr 2026 11:18:32 -0300 Subject: [PATCH 01/11] Update outdated BOLT quotes common/bolt11.c: Some BOLT quotes had changed slightly. run-bolt11.c: A test was updated to reflect nomenclature changes (from `signature recovery` to `public-key recovery`). --- common/bolt11.c | 2 +- common/test/run-bolt11.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/bolt11.c b/common/bolt11.c index 26263dd028c0..7a1f12db55bc 100644 --- a/common/bolt11.c +++ b/common/bolt11.c @@ -1045,7 +1045,7 @@ struct bolt11 *bolt11_decode(const tal_t *ctx, const char *str, &sig, (const u8 *)&hash)) return decode_fail(b11, fail, - "signature recovery failed"); + "public-key recovery failed"); node_id_from_pubkey(&b11->receiver_id, &k); } else { struct pubkey k; diff --git a/common/test/run-bolt11.c b/common/test/run-bolt11.c index 33b228bc7df8..3f5fdcb81890 100644 --- a/common/test/run-bolt11.c +++ b/common/test/run-bolt11.c @@ -873,7 +873,7 @@ int main(int argc, char *argv[]) * > lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgqwgt7mcn5yqw3yx0w94pswkpq6j9uh6xfqqqtsk4tnarugeektd4hg5975x9am52rz4qskukxdmjemg92vvqz8nvmsye63r5ykel43pgz7zq0g2 */ assert(!bolt11_decode(tmpctx, "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgqwgt7mcn5yqw3yx0w94pswkpq6j9uh6xfqqqtsk4tnarugeektd4hg5975x9am52rz4qskukxdmjemg92vvqz8nvmsye63r5ykel43pgz7zq0g2", NULL, NULL, NULL, &fail)); - assert(streq(fail, "signature recovery failed")); + assert(streq(fail, "public-key recovery failed")); /* BOLT #11: * > ### String is too short. From 869e1fd59c77e1e6ad0281dea7868119274d0cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Thu, 23 Apr 2026 11:24:55 -0300 Subject: [PATCH 02/11] Update outdated BOLT quotes bolt12.c: - quote update on `bolt12_chains_match`; - add a new condition to `offer_decode` to check if the amount is greater than 0; run-bolt-12-encode-test.c: add check for `offer_amount > 0` run-bolt12-format-string-test.c: update BOLT quote bolt12-cli.c: update BOLT quote --- common/test/run-bolt12-encode-test.c | 7 +++++-- common/test/run-bolt12-format-string-test.c | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/common/test/run-bolt12-encode-test.c b/common/test/run-bolt12-encode-test.c index 9391b1a38a08..dadc715b30c8 100644 --- a/common/test/run-bolt12-encode-test.c +++ b/common/test/run-bolt12-encode-test.c @@ -429,15 +429,18 @@ int main(int argc, char *argv[]) /* BOLT #12: * - if `offer_amount` is set and `offer_description` is not set: * - MUST NOT respond to the offer. - * - if `offer_amount` is set and is not greater than zero: + * - if `offer_amount` is set and is not greater than zero: * - MUST NOT respond to the offer. * - if `offer_currency` is set and `offer_amount` is not set: * - MUST NOT respond to the offer. * - if neither `offer_issuer_id` nor `offer_paths` are set: * - MUST NOT respond to the offer. */ + offer->offer_amount = tal(offer, u64); + *offer->offer_amount = 10000; + offer->offer_description = NULL; - print_invalid_offer(offer, "Missing offer_description and offer_amount"); + print_invalid_offer(offer, "Missing offer_description"); offer->offer_description = tal_utf8(tmpctx, "Test vectors"); offer->offer_amount = tal(offer, u64); diff --git a/common/test/run-bolt12-format-string-test.c b/common/test/run-bolt12-format-string-test.c index 10f6748b2ddd..d861b3aaaa01 100644 --- a/common/test/run-bolt12-format-string-test.c +++ b/common/test/run-bolt12-format-string-test.c @@ -128,7 +128,7 @@ int main(int argc, char *argv[]) * - SHOULD omit `offer_chains`, implying that bitcoin is only chain. * - if a specific minimum `offer_amount` is required for successful payment: * - MUST set `offer_amount` to the amount expected (per item). - * - MUST set `offer_amount` greater than zero. + * - MUST set `offer_amount` greater than zero. * - if the currency for `offer_amount` is that of all entries in `chains`: * - MUST specify `offer_amount` in multiples of the minimum lightning-payable unit * (e.g. milli-satoshis for bitcoin). From 3e97373587e30f9853f9d1d0e437b32d8117fb69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Thu, 23 Apr 2026 11:28:04 -0300 Subject: [PATCH 03/11] Update outdated BOLT quotes in several files. update BOLT quotes that drifted from the current spec without changing the meaning or expected functionality. --- common/features.h | 2 +- lightningd/channel_gossip.c | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/common/features.h b/common/features.h index 2861573647ea..6255340103fe 100644 --- a/common/features.h +++ b/common/features.h @@ -113,7 +113,7 @@ struct feature_set *feature_set_dup(const tal_t *ctx, * | 16/17 | `basic_mpp` |... IN9 ... * | 18/19 | `option_support_large_channel` |... IN ... * | 22/23 | `option_anchors` |... INT ... - * | 24/25 | `option_route_blinding` |...IN9 ... + * | 24/25 | `option_route_blinding` |... IN9 ... * | 26/27 | `option_shutdown_anysegwit` |... IN ... * | 28/29 | `option_dual_fund` |... IN ... * | 34/35 | `option_quiesce` |... IN ... diff --git a/lightningd/channel_gossip.c b/lightningd/channel_gossip.c index 63088b17c209..53c67e7109f5 100644 --- a/lightningd/channel_gossip.c +++ b/lightningd/channel_gossip.c @@ -687,10 +687,10 @@ static void stash_remote_announce_sigs(struct channel *channel, * A node: * - If the `open_channel` message has the `announce_channel` bit set AND a * `shutdown` message has not been sent: - * - After `channel_ready` has been sent and received AND the funding - * transaction has enough confirmations to ensure that it won't be - * reorganized: - * - MUST send `announcement_signatures` for the funding transaction. + * - After `channel_ready` has been sent and received AND the funding + * transaction has enough confirmations to ensure that it won't be + * reorganized: + * - MUST send `announcement_signatures` for the funding transaction.... * - Otherwise: * - MUST NOT send the `announcement_signatures` message. */ From d18412b7d5c431720f602686839e1eaeccab54da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Thu, 23 Apr 2026 11:40:31 -0300 Subject: [PATCH 04/11] gossmap_manage.c: update BOLT quote and logic to wait for 72 blocks instead of 12 blocks before forgetting a channel. test_gossip.py: Update the tests to reflect the changes. --- tests/test_gossip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_gossip.py b/tests/test_gossip.py index e55492cddd6f..a675ffbe453e 100644 --- a/tests/test_gossip.py +++ b/tests/test_gossip.py @@ -2057,7 +2057,7 @@ def test_close_72_block_delay(node_factory, bitcoind): # That implies 72 blocks *after* spending, i.e. 73 blocks deep! # 72 blocks deep, l4 still sees it - bitcoind.generate_block(70) + bitcoind.generate_block(71) sync_blockheight(bitcoind, [l4]) assert len(l4.rpc.listchannels(source=l1.info['id'])['channels']) == 1 From 92d3ec1d3fa38946c2486b0adae76d4000c27bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Thu, 23 Apr 2026 11:42:36 -0300 Subject: [PATCH 05/11] Update BOLT quotes for channel open flow. The TLV `channel_type` is now mandatory. The code was correct but the quote was not. --- openingd/openingd.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openingd/openingd.c b/openingd/openingd.c index 52e50572e2e0..ac9752a2b694 100644 --- a/openingd/openingd.c +++ b/openingd/openingd.c @@ -332,12 +332,12 @@ static u8 *funder_channel_start(struct state *state, u8 channel_flags, = state->upfront_shutdown_script[LOCAL]; /* BOLT #2: - * - MUST set `channel_type`: - * - MUST set it to a defined type representing the type it wants. - * - MUST use the smallest bitmap possible to represent the channel - * type. - * - SHOULD NOT set it to a type containing a feature which was not - * negotiated. + * - MUST set `channel_type`: + * - MUST set it to a defined type representing the type it wants. + * - MUST use the smallest bitmap possible to represent the channel + * type. + * - SHOULD NOT set it to a type containing a feature which was not + * negotiated. */ open_tlvs->channel_type = state->channel_type->features; From e4b1287eb3b798c5664393afa861e3b8f05f57cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Thu, 23 Apr 2026 15:59:20 -0300 Subject: [PATCH 06/11] Fix tests for bolt12-decode. new cases added by autogenerated mocks after updating the default BOLT spec version. bolt12.c: add new rules to satisfy case `offer_chains with zero entries`. bech32_util.c: fix padding on `from_bech32_charset` to satisfy case `Bech32 padding exceeds 4-bit limit` --- common/bech32_util.c | 14 +++++++++++++- common/bolt12.c | 22 +++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/common/bech32_util.c b/common/bech32_util.c index 3722a19cb200..fd952b6ab468 100644 --- a/common/bech32_util.c +++ b/common/bech32_util.c @@ -79,7 +79,7 @@ bool from_bech32_charset(const tal_t *ctx, u5 *u5data; const char *sep; bool upper = false, lower = false; - size_t datalen; + size_t datalen, nbits, trailing; sep = memchr(bech32, '1', bech32_len); if (!sep) @@ -105,6 +105,18 @@ bool from_bech32_charset(const tal_t *ctx, if (upper && lower) goto fail; + /* Padding: converting N 5-bit groups to bytes leaves (N*5 % 8) trailing + * bits. These must be zero and fewer than 5 (otherwise a full 5-bit + * group is wasted as padding, which is invalid). */ + nbits = datalen * 5; + trailing = nbits % 8; + if (trailing >= 5) + goto fail; + for (size_t i = nbits - trailing; i < nbits; i++) { + if (get_u5_bit(u5data, i)) + goto fail; + } + *data = tal_arr(ctx, u8, 0); if (!bech32_pull_bits(data, u5data, tal_bytelen(u5data) * 5)) { tal_free(*data); diff --git a/common/bolt12.c b/common/bolt12.c index 1f2ccf1b659d..6043bd676612 100644 --- a/common/bolt12.c +++ b/common/bolt12.c @@ -10,7 +10,8 @@ #include #include -/* If chains is NULL, max_num_chains is ignored */ +/* If chains is NULL, max_num_chains is ignored. + * If must_be_chain is NULL, only structural validity is checked. */ bool bolt12_chains_match(const struct bitcoin_blkid *chains, size_t max_num_chains, const struct chainparams *must_be_chain) @@ -31,6 +32,13 @@ bool bolt12_chains_match(const struct bitcoin_blkid *chains, * - if the node does not accept invoices for at least one of the `chains`: * - MUST NOT respond to the offer */ + if (chains && max_num_chains == 0) + return false; + + /* No specific chain required: structurally valid. */ + if (!must_be_chain) + return true; + if (!chains) { max_num_chains = 1; chains = &chainparams_for_network("bitcoin")->genesis_blockhash; @@ -187,6 +195,18 @@ struct tlv_offer *offer_decode(const tal_t *ctx, return NULL; } + /* BOLT #12: + * - otherwise: (`offer_chains` is set): + * - if the node does not accept invoices for at least one of the `chains`: + * - MUST NOT respond to the offer + */ + for (size_t i = 0; i < tal_count(offer->fields); i++) { + if (offer->fields[i].numtype == 2 && offer->fields[i].length == 0) { + *fail = tal_strdup(ctx, "offer_chains must have at least one entry"); + return tal_free(offer); + } + } + *fail = check_features_and_chain(ctx, our_features, must_be_chain, offer->offer_features, From 1cd43314f1ed3bf3646c071902653cf73ede3c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Fri, 24 Apr 2026 08:10:36 -0300 Subject: [PATCH 07/11] Update tests after changing deadline from `blockheight + 12` to `blockheight + 72` blocks. Added 60 blocks to all tests that relied on this parameter in order to fix them. i.e. 12 -> 72, 13 -> 73, etc... --- tests/test_wallet.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 0eb0433c6ff3..86377087b08d 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -2548,11 +2548,11 @@ def test_unspend_during_reorg(node_factory, bitcoind): blockheight, txindex, _ = scid.split('x') # Use mainnet settings for rescan. - l3 = node_factory.get_node(options={'rescan': 15}) + l3 = node_factory.get_node(options={'rescan': 75}) l3.connect(l2) mine_funding_to_announce(bitcoind, [l1, l2, l3]) - bitcoind.generate_block(20) + bitcoind.generate_block(90) sync_blockheight(bitcoind, [l3]) wait_for(lambda: len(l3.rpc.listchannels()['channels']) == 2) @@ -2565,7 +2565,7 @@ def test_unspend_during_reorg(node_factory, bitcoind): bitcoind.generate_block(74, wait_for_mempool=1) wait_for(lambda: len(l3.rpc.listchannels()['channels']) == 2) - # In one fell swoop it goes through dying, to dead (12 blocks) + # In one fell swoop it goes through dying, to dead (72 blocks) l3.daemon.wait_for_log(f"Adding block {spentheight}") l3.daemon.wait_for_log(f"gossipd: channel {scid} closing soon due to the funding outpoint being spent") l3.daemon.wait_for_log(f"gossipd: Deleting channel {scid} due to the funding outpoint being spent") From 96760fbda8519b7284ce45f24c2b807dd2cf69c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Fri, 24 Apr 2026 09:21:47 -0300 Subject: [PATCH 08/11] Update BOLT quote for `channel_ready` re-transmission guards. channeld.c: update logic to comply with BOLT quote. tests/test_splicing.py: update test to confirm we are not re-transmiting `channel_ready` for that specific case. --- channeld/channeld.c | 15 ++++++++++----- tests/test_splicing.py | 7 +++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/channeld/channeld.c b/channeld/channeld.c index 11d8fdeea572..9daa824e5a3d 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -5954,16 +5954,21 @@ static void peer_reconnect(struct peer *peer, /* BOLT #2: * * - if `next_commitment_number` is 1 in both the - * `channel_reestablish` it sent and received: + * `channel_reestablish` it sent and received, and none of those + * `channel_reestablish` messages contain `my_current_funding_locked` or + * `next_funding` for a splice transaction: * - MUST retransmit `channel_ready`. * - otherwise: - * - MUST NOT retransmit `channel_ready`, but MAY send - * `channel_ready` with a different `short_channel_id` - * `alias` field. + * - MUST NOT retransmit `channel_ready`, but MAY send `channel_ready` with + * a different `short_channel_id` `alias` field. */ if (peer->channel_ready[LOCAL] && peer->next_index[LOCAL] == 1 - && next_commitment_number == 1) { + && next_commitment_number == 1 + && !local_next_funding + && !(send_tlvs && send_tlvs->my_current_funding_locked) + && !remote_next_funding + && !(recv_tlvs && recv_tlvs->my_current_funding_locked)) { struct tlv_channel_ready_tlvs *tlvs = tlv_channel_ready_tlvs_new(tmpctx); tlvs->short_channel_id = &peer->local_alias; diff --git a/tests/test_splicing.py b/tests/test_splicing.py index f713ff7ba14e..6a644b6d618d 100644 --- a/tests/test_splicing.py +++ b/tests/test_splicing.py @@ -416,6 +416,8 @@ def test_commit_crash_splice(node_factory, bitcoind): l1.daemon.wait_for_log(r"Splice initiator: we commit") + # Snapshot log position before restart so we only search post-restart logs below. + pre_restart_logpos = l1.daemon.logsearch_start l1.restart() # The splicing inflight should have been left pending in the DB @@ -426,6 +428,11 @@ def test_commit_crash_splice(node_factory, bitcoind): l1.daemon.wait_for_log(r'Splice resume check with local_next_funding: sent, remote_next_funding: received, inflights: 1') l1.daemon.wait_for_log(r'Splice negotation, will not send commit, not recv commit, send signature, recv signature as initiator') + # channel_ready MUST NOT be retransmitted when either channel_reestablish + # message contains next_funding or my_current_funding_locked + assert not l1.daemon.is_in_log(r'Retransmitting channel_ready for channel', + start=pre_restart_logpos) + assert l1.db_query("SELECT count(*) as c FROM channel_funding_inflights;")[0]['c'] == 1 l2.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE') From 3d2c49c4e1707b6e2ceaaf42c0966bfe6d16f853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Mon, 27 Apr 2026 15:46:19 -0300 Subject: [PATCH 09/11] channeld.c: update logic for retransmitting channel_ready Extracted logic from if clause to variable for readability, and add extra checks to make sure a splice is active --- channeld/channeld.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/channeld/channeld.c b/channeld/channeld.c index 9daa824e5a3d..9e27da967ff4 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -5951,6 +5951,17 @@ static void peer_reconnect(struct peer *peer, "next_funding_txid not recognized."); } + /* "none of those channel_reestablish messages contain + * my_current_funding_locked or next_funding for a splice transaction" */ + bool is_splice_active = local_next_funding + || peer->splice_state->locked_ready[LOCAL] + || remote_next_funding + || (recv_tlvs + && recv_tlvs->my_current_funding_locked + && !bitcoin_txid_eq( + &recv_tlvs->my_current_funding_locked->my_current_funding_locked_txid, + &peer->channel->funding.txid)); + /* BOLT #2: * * - if `next_commitment_number` is 1 in both the @@ -5965,10 +5976,7 @@ static void peer_reconnect(struct peer *peer, if (peer->channel_ready[LOCAL] && peer->next_index[LOCAL] == 1 && next_commitment_number == 1 - && !local_next_funding - && !(send_tlvs && send_tlvs->my_current_funding_locked) - && !remote_next_funding - && !(recv_tlvs && recv_tlvs->my_current_funding_locked)) { + && !is_splice_active) { struct tlv_channel_ready_tlvs *tlvs = tlv_channel_ready_tlvs_new(tmpctx); tlvs->short_channel_id = &peer->local_alias; From 5b41f687fe65e7d17c9088f34efb92043fefac7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Tue, 5 May 2026 11:45:21 -0300 Subject: [PATCH 10/11] Update BOLT quotes channeld/channeld.c: Some BOLT quotes had changed slightly. --- channeld/channeld.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/channeld/channeld.c b/channeld/channeld.c index 9e27da967ff4..f391eddbd0b6 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -5965,9 +5965,7 @@ static void peer_reconnect(struct peer *peer, /* BOLT #2: * * - if `next_commitment_number` is 1 in both the - * `channel_reestablish` it sent and received, and none of those - * `channel_reestablish` messages contain `my_current_funding_locked` or - * `next_funding` for a splice transaction: + * `channel_reestablish` it sent and received: * - MUST retransmit `channel_ready`. * - otherwise: * - MUST NOT retransmit `channel_ready`, but MAY send `channel_ready` with From c94b11346ac3ab428cfebf3f1642aef9fdef01d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADckolas=20Goline?= Date: Wed, 6 May 2026 07:30:17 -0300 Subject: [PATCH 11/11] test: fix block generation in `test_close_72_block_delay` to reflect correct depth --- tests/test_gossip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_gossip.py b/tests/test_gossip.py index a675ffbe453e..e55492cddd6f 100644 --- a/tests/test_gossip.py +++ b/tests/test_gossip.py @@ -2057,7 +2057,7 @@ def test_close_72_block_delay(node_factory, bitcoind): # That implies 72 blocks *after* spending, i.e. 73 blocks deep! # 72 blocks deep, l4 still sees it - bitcoind.generate_block(71) + bitcoind.generate_block(70) sync_blockheight(bitcoind, [l4]) assert len(l4.rpc.listchannels(source=l1.info['id'])['channels']) == 1