From 6c5f02676f59489ea949c797605e2b769cd03171 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 30 Apr 2026 12:18:43 +0930 Subject: [PATCH 01/18] doc: update post-release instructions. Document how to update to the latest bolts, and how to remove deprecated features. Signed-off-by: Rusty Russell --- doc/contribute-to-core-lightning/release-checklist.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/contribute-to-core-lightning/release-checklist.md b/doc/contribute-to-core-lightning/release-checklist.md index 89abf4067acf..42b0ecac5afb 100644 --- a/doc/contribute-to-core-lightning/release-checklist.md +++ b/doc/contribute-to-core-lightning/release-checklist.md @@ -99,10 +99,16 @@ Here's a checklist for the release process. ## Post-release -1. Create a PR to update Makefile's CLN_NEXT_VERSION and important dates for the next release on `.github/PULL_REQUEST_TEMPLATE.md`. +1. Create a PR to update: + * `Makefile`: variables CLN_NEXT_VERSION and CLN_PREV_VERSION (this may break tests as deprecated things are disabled!) + * `tools/lightningd-downgrade.c`: to downgrade to the just-released version. + * `.github/workflows/ci.yaml`: change old-cln to download the just-released version. + * `.github/PULL_REQUEST_TEMPLATE.md` for important dates for the next release. 2. Look through PRs which were delayed for release and merge them. 3. Close out the Milestone for the now-shipped release. 4. Update this file with any missing or changed instructions. +5. Fetch the latest bolt revision in ../bolts. Then run `./devtools/bolt-catchup.sh` to update BOLTVERSION in the Makefile and run `make check-bolt-quotes`. It may get confused by merges in the BOLTs repository, so you may have to do some manual work. Note: this step may involve a significant amount of work for new spec changes! +6. Go through `doc/developers-guide/deprecated-features.md` and remove features and code whose `Last Supported` was the prior version (i.e. now two versions ago: we give one version where the user can use `i-promise-to-fix-broken-api-user=FEATURENAME` to re-enable it). Also remove the features from any schemas and other documentation. ## Performing the Point (hotfix) Release From 20d3c417ac85349d7601b77ecbc975d010f82ba3 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 30 Apr 2026 12:19:43 +0930 Subject: [PATCH 02/18] xpay: don't use MPP for bolt12 unless the invoice explicitly supports it. We actually changed this by default last release, and nobody noticed! Signed-off-by: Rusty Russell --- doc/developers-guide/deprecated-features.md | 1 - plugins/xpay/xpay.c | 11 ++++++++--- tests/test_xpay.py | 15 +++------------ 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/doc/developers-guide/deprecated-features.md b/doc/developers-guide/deprecated-features.md index be44a5837157..acbb3184e8b4 100644 --- a/doc/developers-guide/deprecated-features.md +++ b/doc/developers-guide/deprecated-features.md @@ -9,7 +9,6 @@ privacy: | Name | Type | First Deprecated | Last Supported | Description | |----------------------------------------------------|--------------------|------------------|----------------|---------------------------------------------------------------------------------------------------------------------------| -| xpay.ignore_bolt12_mpp | Field | v25.05 | v25.12 | Try MPP even if the BOLT12 invoice doesn't explicitly allow it (CLN didn't until 25.02) | | listpeerchannels.max_total_htlc_in_msat | Field | v25.02 | v26.04 | Use our_max_total_htlc_out_msat | | wait.details | Field | v25.05 | v26.06 | Use subsystem-specific object instead | | channel_state_changed.old_state.unknown | Notification Field | v25.05 | v26.04 | Value "unknown" is deprecated: field will be omitted instead | diff --git a/plugins/xpay/xpay.c b/plugins/xpay/xpay.c index f3a518c11367..05d001d80323 100644 --- a/plugins/xpay/xpay.c +++ b/plugins/xpay/xpay.c @@ -2033,10 +2033,15 @@ static struct command_result *xpay_core(struct command *cmd, * paths, we just know the cltv we use to enter the * final hop. */ payment->final_cltv = 0; - /* We will start honoring this flag in future */ + /* BOLT #12: + * - if `invoice_features` contains the MPP/compulsory bit: + * - MUST pay the invoice via multiple separate blinded paths. + * - otherwise, if `invoice_features` contains the MPP/optional bit: + * - MAY pay the invoice via multiple separate payments. + * - otherwise: + * - MUST NOT use multiple parts to pay the invoice. + */ payment->disable_mpp = !feature_offered(b12inv->invoice_features, OPT_BASIC_MPP); - if (payment->disable_mpp && command_deprecated_in_ok(cmd, "ignore_bolt12_mpp", "v25.05", "v25.12")) - payment->disable_mpp = false; } else { struct bolt11 *b11 = bolt11_decode(tmpctx, payment->invstring, diff --git a/tests/test_xpay.py b/tests/test_xpay.py index 3ba800667920..db4fde9f18c1 100644 --- a/tests/test_xpay.py +++ b/tests/test_xpay.py @@ -637,15 +637,10 @@ def test_xpay_no_mpp(node_factory, chainparams): assert ret['amount_sent_msat'] == AMOUNT + AMOUNT // 100000 + 1 -@pytest.mark.parametrize("deprecations", [False, True]) -def test_xpay_bolt12_no_mpp(node_factory, chainparams, deprecations): +def test_xpay_bolt12_no_mpp(node_factory, chainparams): """If we force it, we use MPP even if BOLT12 invoice doesn't say we should""" # l4 needs dev-allow-localhost so it considers itself to have an advertized address, and doesn't create a blinded path from l2/l4. opts = [{}, {}, {'dev-force-features': -17, 'dev-allow-localhost': None}, {}] - if deprecations is True: - for o in opts: - o['i-promise-to-fix-broken-api-user'] = 'xpay.ignore_bolt12_mpp' - o['broken_log'] = 'DEPRECATED API USED: xpay.ignore_bolt12_mpp' l1, l2, l3, l4 = node_factory.get_nodes(4, opts=opts) node_factory.join_nodes([l1, l2, l3], wait_for_announce=True) @@ -666,12 +661,8 @@ def test_xpay_bolt12_no_mpp(node_factory, chainparams, deprecations): ret = l1.rpc.xpay(invl3['invoice']) assert ret['failed_parts'] == 0 - if deprecations: - assert ret['successful_parts'] == 2 - assert ret['amount_sent_msat'] == AMOUNT + AMOUNT // 100000 + 2 - else: - assert ret['successful_parts'] == 1 - assert ret['amount_sent_msat'] == AMOUNT + AMOUNT // 100000 + 1 + assert ret['successful_parts'] == 1 + assert ret['amount_sent_msat'] == AMOUNT + AMOUNT // 100000 + 1 assert ret['amount_msat'] == AMOUNT From 288c1ee379ca0a6f562229d52d51b077f10e7195 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 30 Apr 2026 12:20:43 +0930 Subject: [PATCH 03/18] lightning-downgrade: prepare for downgrading to v26.04. Signed-off-by: Rusty Russell --- tools/lightning-downgrade.c | 1 + wallet/migrations.c | 1 + 2 files changed, 2 insertions(+) diff --git a/tools/lightning-downgrade.c b/tools/lightning-downgrade.c index 25a118de04e6..70a8f2206e5b 100644 --- a/tools/lightning-downgrade.c +++ b/tools/lightning-downgrade.c @@ -169,6 +169,7 @@ static const char *downgrade_askrene_layers(const tal_t *ctx, struct db *db) static const struct db_version db_versions[] = { { "v25.09", 276, downgrade_askrene_layers, false }, { "v25.12", 280, NULL, false }, + { "v26.04", 282, NULL, false }, }; static const struct db_version *version_db(const char *version) diff --git a/wallet/migrations.c b/wallet/migrations.c index 83c61c0e9129..1f5d3b51c0f2 100644 --- a/wallet/migrations.c +++ b/wallet/migrations.c @@ -1084,6 +1084,7 @@ static const struct db_migration dbmigrations[] = { NULL, NULL}, {SQL("ALTER TABLE offers ADD COLUMN force_paths INTEGER DEFAULT 0;"), NULL, SQL("ALTER TABLE offers DROP COLUMN force_paths"), NULL}, + /* ^v26.04 */ }; const struct db_migration *get_db_migrations(size_t *num) From e1142a65692cd54ba6fac4d4f4c0f370963ba746 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 30 Apr 2026 12:21:43 +0930 Subject: [PATCH 04/18] Versions: update for next version. Signed-off-by: Rusty Russell --- .github/PULL_REQUEST_TEMPLATE.md | 6 +++--- .github/workflows/ci.yaml | 4 ++-- Makefile | 4 ++-- tests/test_plugin.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index df5969859648..dac258bf2bd9 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,10 +1,10 @@ > [!IMPORTANT] > -> 26.04 FREEZE March 11th: Non-bugfix PRs not ready by this date will wait for 26.06. +> 26.06 FREEZE April 30th: Non-bugfix PRs not ready by this date will wait for 26.09. > -> RC1 is scheduled on _March 23rd_ +> RC1 is scheduled on _May 14th_ > -> The final release is scheduled for April 15th. +> The final release is scheduled for June 1st. ## Checklist diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index eefdca61522f..7ab7618ff538 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -409,8 +409,8 @@ jobs: run: | mkdir old-cln cd old-cln - wget https://github.com/ElementsProject/lightning/releases/download/v25.12/clightning-v25.12-ubuntu-24.04-amd64.tar.xz - tar -xaf clightning-v25.12-ubuntu-24.04-amd64.tar.xz + wget https://github.com/ElementsProject/lightning/releases/download/v26.04/clightning-v26.04-ubuntu-24.04-amd64.tar.xz + tar -xaf clightning-v26.04-ubuntu-24.04-amd64.tar.xz - name: Switch network if: ${{ matrix.TEST_NETWORK == 'liquid-regtest' }} diff --git a/Makefile b/Makefile index 682e3f368bc8..223d383ef12e 100644 --- a/Makefile +++ b/Makefile @@ -7,10 +7,10 @@ VERSION ?= $(shell git describe --tags --always --dirty=-modded --abbrev=7 2>/de $(info Building version $(VERSION)) # Next release. -CLN_NEXT_VERSION := v26.04 +CLN_NEXT_VERSION := v26.06 # Previous release (for downgrade testing) -CLN_PREV_VERSION := v25.12 +CLN_PREV_VERSION := v26.04 # --quiet / -s means quiet, dammit! ifeq ($(findstring s,$(word 1, $(MAKEFLAGS))),s) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 3f4f11cbe90f..10b93255e28f 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -4395,7 +4395,7 @@ def test_sql(node_factory, bitcoind): def test_sql_deprecated(node_factory, bitcoind): - l1, l2 = node_factory.line_graph(2, opts=[{'allow-deprecated-apis': True, "broken_log": "DEPRECATED API USED: listpeerchannels.max_total_htlc_in_msat"}, {}]) + l1, l2 = node_factory.line_graph(2, opts=[{'i-promise-to-fix-broken-api-user': 'listpeerchannels.max_total_htlc_in_msat'}, {}]) # With deprecated APIs, this is there. ret = l1.rpc.sql("SELECT max_total_htlc_in_msat FROM peerchannels;") From 4896baa27888d8f8e56518bbd013e75b91cbd78b Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 30 Apr 2026 12:22:43 +0930 Subject: [PATCH 05/18] CI: check that wire format is correct. Regenerate the CSV files from the specs, apply the wire/extracted*.patch files and make sure they are up-to-date. This catches people editing the files: if you want to change them, you have to create a new patch file. Signed-off-by: Rusty Russell --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 223d383ef12e..c8ddeb8b524c 100644 --- a/Makefile +++ b/Makefile @@ -664,8 +664,11 @@ update-doc-examples: check-doc-examples: update-doc-examples git diff --exit-code HEAD +check-wire-format: extract-bolt-csv + git diff --exit-code HEAD + # This should NOT compile things! -check-source: check-makefile check-whitespace check-spelling check-python-flake8 check-includes check-shellcheck check-setup_locale check-tmpctx check-discouraged-functions check-amount-access check-bad-sprintf +check-source: check-makefile check-whitespace check-spelling check-python-flake8 check-includes check-shellcheck check-setup_locale check-tmpctx check-discouraged-functions check-amount-access check-bad-sprintf check-wire-format full-check: check check-source From cc7a572c90cac6759eb55fdc26a3b1574bc2fce9 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 30 Apr 2026 12:23:43 +0930 Subject: [PATCH 06/18] wire: add shim patch for when we regenerate. The current BOLTVERSION doesn't match our implementation, so when we `make extract-bolt-csv` we get a change. Fold the changes into a single splice patch. Signed-off-by: Rusty Russell --- wire/extracted_peer_11_splice.patch | 48 +++++++++++++++------- wire/extracted_peer_12_splice_update.patch | 33 --------------- 2 files changed, 34 insertions(+), 47 deletions(-) delete mode 100644 wire/extracted_peer_12_splice_update.patch diff --git a/wire/extracted_peer_11_splice.patch b/wire/extracted_peer_11_splice.patch index 13190f572f94..e4df1e2eb953 100644 --- a/wire/extracted_peer_11_splice.patch +++ b/wire/extracted_peer_11_splice.patch @@ -1,8 +1,15 @@ -diff --git a/wire/peer_wire.csv b/wire/peer_wire.csv -index 5b5803afc..41b57e85b 100644 ---- a/wire/peer_wire.csv -+++ b/wire/peer_wire.csv -@@ -206,6 +206,19 @@ subtypedata,lease_rates,channel_fee_max_base_msat,tu32, +diff --git b/wire/peer_wire.csv a/wire/peer_wire.csv +index f3a42e5537..4b01c56836 100644 +--- b/wire/peer_wire.csv ++++ a/wire/peer_wire.csv +@@ -1,3 +1,6 @@ ++msgtype,protocol_batch_element,0 ++msgdata,protocol_batch_element,channel_id,channel_id, ++msgdata,protocol_batch_element,element_size,u16, + msgtype,init,16 + msgdata,init,gflen,u16, + msgdata,init,globalfeatures,byte,gflen +@@ -217,6 +220,19 @@ subtypedata,lease_rates,channel_fee_max_base_msat,tu32, msgtype,stfu,2 msgdata,stfu,channel_id,channel_id, msgdata,stfu,initiator,u8, @@ -22,25 +29,38 @@ index 5b5803afc..41b57e85b 100644 msgtype,shutdown,38 msgdata,shutdown,channel_id,channel_id, msgdata,shutdown,len,u16, -@@ -264,6 +250,10 @@ msgdata,commitment_signed,channel_id,channel_id, +@@ -280,11 +296,20 @@ msgdata,update_fail_malformed_htlc,channel_id,channel_id, + msgdata,update_fail_malformed_htlc,id,u64, + msgdata,update_fail_malformed_htlc,sha256_of_onion,sha256, + msgdata,update_fail_malformed_htlc,failure_code,u16, ++msgtype,start_batch,127 ++msgdata,start_batch,channel_id,channel_id, ++msgdata,start_batch,batch_size,u16, ++msgdata,start_batch,batch_info,start_batch_tlvs, ++tlvtype,start_batch_tlvs,batch_info,1 ++tlvdata,start_batch_tlvs,batch_info,message_type,u16, + msgtype,commitment_signed,132 + msgdata,commitment_signed,channel_id,channel_id, msgdata,commitment_signed,signature,signature, msgdata,commitment_signed,num_htlcs,u16, msgdata,commitment_signed,htlc_signature,signature,num_htlcs +msgdata,commitment_signed,splice_channel_id,commitment_signed_tlvs, -+tlvtype,commitment_signed_tlvs,splice_info,0 -+tlvdata,commitment_signed_tlvs,splice_info,batch_size,u16, ++tlvtype,commitment_signed_tlvs,splice_info,1 +tlvdata,commitment_signed_tlvs,splice_info,funding_txid,sha256, msgtype,revoke_and_ack,133 msgdata,revoke_and_ack,channel_id,channel_id, msgdata,revoke_and_ack,per_commitment_secret,byte,32 -@@ -319,6 +320,10 @@ msgdata,channel_reestablish,your_last_per_commitment_secret,byte,32 +@@ -301,8 +326,12 @@ msgdata,channel_reestablish,next_commitment_number,u64, + msgdata,channel_reestablish,next_revocation_number,u64, + msgdata,channel_reestablish,your_last_per_commitment_secret,byte,32 msgdata,channel_reestablish,my_current_per_commitment_point,point, - tlvtype,channel_reestablish_tlvs,next_funding,0 +-tlvtype,channel_reestablish_tlvs,next_funding,0 ++tlvtype,channel_reestablish_tlvs,next_funding,1 tlvdata,channel_reestablish_tlvs,next_funding,next_funding_txid,sha256, -+tlvtype,channel_reestablish_tlvs,your_last_funding_locked_txid,1 -+tlvdata,channel_reestablish_tlvs,your_last_funding_locked_txid,your_last_funding_locked_txid,sha256, -+tlvtype,channel_reestablish_tlvs,my_current_funding_locked_txid,3 -+tlvdata,channel_reestablish_tlvs,my_current_funding_locked_txid,my_current_funding_locked_txid,sha256, ++tlvdata,channel_reestablish_tlvs,next_funding,retransmit_flags,byte, ++tlvtype,channel_reestablish_tlvs,my_current_funding_locked,5 ++tlvdata,channel_reestablish_tlvs,my_current_funding_locked,my_current_funding_locked_txid,sha256, ++tlvdata,channel_reestablish_tlvs,my_current_funding_locked,retransmit_flags,byte, msgtype,announcement_signatures,259 msgdata,announcement_signatures,channel_id,channel_id, msgdata,announcement_signatures,short_channel_id,short_channel_id, diff --git a/wire/extracted_peer_12_splice_update.patch b/wire/extracted_peer_12_splice_update.patch deleted file mode 100644 index 96a8a8f88ebd..000000000000 --- a/wire/extracted_peer_12_splice_update.patch +++ /dev/null @@ -1,33 +0,0 @@ -diff --git a/wire/peer_wire.csv b/wire/peer_wire.csv -index 9abcb0e64..e2aae8efb 100644 ---- a/wire/peer_wire.csv -+++ b/wire/peer_wire.csv -@@ -1,3 +1,6 @@ -+msgtype,protocol_batch_element,0 -+msgdata,protocol_batch_element,channel_id,channel_id, -+msgdata,protocol_batch_element,element_size,u16, - msgtype,init,16 - msgdata,init,gflen,u16, - msgdata,init,globalfeatures,byte,gflen -@@ -293,6 +296,12 @@ msgdata,update_fail_malformed_htlc,channel_id,channel_id, - msgdata,update_fail_malformed_htlc,id,u64, - msgdata,update_fail_malformed_htlc,sha256_of_onion,sha256, - msgdata,update_fail_malformed_htlc,failure_code,u16, -+msgtype,start_batch,127 -+msgdata,start_batch,channel_id,channel_id, -+msgdata,start_batch,batch_size,u16, -+msgdata,start_batch,batch_info,start_batch_tlvs, -+tlvtype,start_batch_tlvs,batch_info,1 -+tlvdata,start_batch_tlvs,batch_info,message_type,u16, - msgtype,commitment_signed,132 - msgdata,commitment_signed,channel_id,channel_id, - msgdata,commitment_signed,signature,signature, -@@ -309,7 +309,6 @@ msgdata,commitment_signed,num_htlcs,u16, - msgdata,commitment_signed,htlc_signature,signature,num_htlcs - msgdata,commitment_signed,splice_channel_id,commitment_signed_tlvs, --tlvtype,commitment_signed_tlvs,splice_info,0 --tlvdata,commitment_signed_tlvs,splice_info,batch_size,u16, -+tlvtype,commitment_signed_tlvs,splice_info,1 - tlvdata,commitment_signed_tlvs,splice_info,funding_txid,sha256, - msgtype,revoke_and_ack,133 - msgdata,revoke_and_ack,channel_id,channel_id, From cca4d7dc24bd3359c851e84482a771ca2d7ce05a Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 30 Apr 2026 12:24:43 +0930 Subject: [PATCH 07/18] Makefile: check bolt quotes in CI. I accidentally dropped this! Signed-off-by: Rusty Russell --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c8ddeb8b524c..b15fe5c6ff20 100644 --- a/Makefile +++ b/Makefile @@ -668,7 +668,7 @@ check-wire-format: extract-bolt-csv git diff --exit-code HEAD # This should NOT compile things! -check-source: check-makefile check-whitespace check-spelling check-python-flake8 check-includes check-shellcheck check-setup_locale check-tmpctx check-discouraged-functions check-amount-access check-bad-sprintf check-wire-format +check-source: check-makefile check-whitespace check-spelling check-python-flake8 check-includes check-shellcheck check-setup_locale check-tmpctx check-discouraged-functions check-amount-access check-bad-sprintf check-wire-format check-source-bolt full-check: check check-source From 5ccf17dc33dca74f4be4096389ddd46ddad1dca5 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 30 Apr 2026 12:25:43 +0930 Subject: [PATCH 08/18] common: BOLT update which make channel_type assumed. We already required it, so the changes are textual. Signed-off-by: Rusty Russell --- Makefile | 2 +- lightningd/opening_control.c | 3 +-- openingd/dualopend.c | 11 ++++------- openingd/openingd.c | 29 ++++++++++++++--------------- 4 files changed, 20 insertions(+), 25 deletions(-) diff --git a/Makefile b/Makefile index b15fe5c6ff20..eaea6c6fcfff 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ CCANDIR := ccan # Where we keep the BOLT RFCs BOLTDIR := ../bolts/ -DEFAULT_BOLTVERSION := 68881992b97f20aca29edf7a4d673b8e6a70379a +DEFAULT_BOLTVERSION := a678f8452eab8f93e6de802ba4e7e2a4c2be20dc # Can be overridden on cmdline. BOLTVERSION := $(DEFAULT_BOLTVERSION) diff --git a/lightningd/opening_control.c b/lightningd/opening_control.c index 0d482c24e729..820bf4ed6e34 100644 --- a/lightningd/opening_control.c +++ b/lightningd/opening_control.c @@ -160,8 +160,7 @@ wallet_commit_channel(struct lightningd *ld, * #### Requirements * * Both peers: - * ... - * - MUST use that `channel_type` for all commitment transactions. + * - MUST use the negotiated `channel_type` for all commitment transactions. */ /* i.e. We set it now for the channel permanently. */ if (channel_type_has(type, OPT_STATIC_REMOTEKEY)) diff --git a/openingd/dualopend.c b/openingd/dualopend.c index 20ae07c62083..bf70a3182cf3 100644 --- a/openingd/dualopend.c +++ b/openingd/dualopend.c @@ -2430,9 +2430,8 @@ static void accepter_start(struct state *state, const u8 *oc2_msg) /* BOLT #2: * The receiving node MUST fail the channel if: *... - * - It supports `channel_type` and `channel_type` was set: - * - if `type` is not suitable. - * - if `type` includes `option_zeroconf` and it does not trust the sender to open an unconfirmed channel. + * - the `channel_type` is not suitable. + * - the `channel_type` includes `option_zeroconf` and it does not trust the sender to open an unconfirmed channel. */ if (!open_tlv->channel_type) { negotiation_failed(state, @@ -2682,8 +2681,7 @@ static void accepter_start(struct state *state, const u8 *oc2_msg) } /* BOLT #2: - * - if `option_channel_type` was negotiated: - * - MUST set `channel_type` to the `channel_type` from `open_channel` + * - MUST set `channel_type` to the `channel_type` from `open_channel` */ a_tlv->channel_type = state->channel_type->features; @@ -3143,8 +3141,7 @@ static void opener_start(struct state *state, u8 *msg) } /* BOLT #2: - * - if `channel_type` is set, and `channel_type` was set in - * `open_channel`, and they are not equal types: + * if `channel_type` does not match the `channel_type` from `open_channel`: * - MUST fail the channel. */ if (!a_tlv->channel_type) { diff --git a/openingd/openingd.c b/openingd/openingd.c index cc51e1e67c51..52e50572e2e0 100644 --- a/openingd/openingd.c +++ b/openingd/openingd.c @@ -332,7 +332,7 @@ static u8 *funder_channel_start(struct state *state, u8 channel_flags, = state->upfront_shutdown_script[LOCAL]; /* BOLT #2: - * - if it includes `channel_type`: + * - 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. @@ -410,9 +410,8 @@ static u8 *funder_channel_start(struct state *state, u8 channel_flags, their_mindepth); /* BOLT #2: - * - if `option_channel_type` was negotiated but the message doesn't - * include a `channel_type`: - * - MAY fail the channel. + * - if the message doesn't include a `channel_type`: + * - MUST fail the channel. */ if (!accept_tlvs->channel_type) { negotiation_failed(state, @@ -420,8 +419,7 @@ static u8 *funder_channel_start(struct state *state, u8 channel_flags, } /* BOLT #2: - * - if `channel_type` is set, and `channel_type` was set in - * `open_channel`, and they are not equal types: + * - if `channel_type` does not match the `channel_type` from `open_channel`: * - MUST fail the channel. */ /* Simple case: caller specified, don't allow any variants */ @@ -881,18 +879,20 @@ static u8 *fundee_channel(struct state *state, const u8 *open_channel_msg) set_remote_upfront_shutdown(state, open_tlvs->upfront_shutdown_script); /* BOLT #2: - * The receiving node MUST fail the channel if: - *... - * - It supports `channel_type` and `channel_type` was set: - * - if `type` is not suitable. - * - if `type` includes `option_zeroconf` and it does not trust the sender to open an unconfirmed channel. + * - if the message doesn't include a `channel_type`: + * - fail the channel. */ - /* option_channel_type is compulsory. */ if (!open_tlvs->channel_type) { negotiation_failed(state, "Did not set channel_type in open_channel message"); } + /* BOLT #2: + * The receiving node MUST fail the channel if: + *... + * - the `channel_type` is not suitable. + * - the `channel_type` includes `option_zeroconf` and it does not trust the sender to open an unconfirmed channel. + */ state->channel_type = channel_type_accept( state, open_tlvs->channel_type, state->our_features); if (!state->channel_type) { @@ -1050,7 +1050,7 @@ static u8 *fundee_channel(struct state *state, const u8 *open_channel_msg) /* BOLT #2: * The receiving node MUST fail the channel if: *... - * - if `type` includes `option_zeroconf` and it does not trust the + * - the `channel_type` includes `option_zeroconf` and it does not trust the * sender to open an unconfirmed channel. */ if (channel_type_has(state->channel_type, OPT_ZEROCONF) && @@ -1077,8 +1077,7 @@ static u8 *fundee_channel(struct state *state, const u8 *open_channel_msg) accept_tlvs->upfront_shutdown_script = state->upfront_shutdown_script[LOCAL]; /* BOLT #2: - * - if `option_channel_type` was negotiated: - * - MUST set `channel_type` to the `channel_type` from `open_channel` + * - MUST set `channel_type` to the `channel_type` from `open_channel` */ accept_tlvs->channel_type = state->channel_type->features; From f56cff34ee6f8a91ac0f2bf886a3deaaa9eda52c Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 30 Apr 2026 12:26:43 +0930 Subject: [PATCH 09/18] BOLT: update to include `channel_type` feature in BOLT 9. We add it to our code, even though we don't use it (yet?). Signed-off-by: Rusty Russell --- Makefile | 2 +- common/features.c | 10 +++++++--- common/features.h | 9 +++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index eaea6c6fcfff..dd31c360c0b0 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ CCANDIR := ccan # Where we keep the BOLT RFCs BOLTDIR := ../bolts/ -DEFAULT_BOLTVERSION := a678f8452eab8f93e6de802ba4e7e2a4c2be20dc +DEFAULT_BOLTVERSION := 89dfc59529a9ce845b06e7d6a62851a4ea096158 # Can be overridden on cmdline. BOLTVERSION := $(DEFAULT_BOLTVERSION) diff --git a/common/features.c b/common/features.c index 0de221dad443..7b6ed6cb5458 100644 --- a/common/features.c +++ b/common/features.c @@ -29,6 +29,7 @@ const char *feature_place_names[] = { "bolt12_offer", "bolt12_invreq", "bolt12_invoice", + "channel_type", }; static const struct feature_style feature_styles[] = { @@ -81,7 +82,8 @@ static const struct feature_style feature_styles[] = { { OPT_ANCHORS_ZERO_FEE_HTLC_TX, .copy_style = { [INIT_FEATURE] = FEATURE_REPRESENT, [NODE_ANNOUNCE_FEATURE] = FEATURE_REPRESENT, - [CHANNEL_FEATURE] = FEATURE_DONT_REPRESENT } }, + [CHANNEL_FEATURE] = FEATURE_DONT_REPRESENT, + [CHANNEL_TYPE_FEATURE] = FEATURE_REPRESENT } }, { OPT_DUAL_FUND, .copy_style = { [INIT_FEATURE] = FEATURE_REPRESENT, [NODE_ANNOUNCE_FEATURE] = FEATURE_REPRESENT, @@ -93,7 +95,8 @@ static const struct feature_style feature_styles[] = { .copy_style = { [INIT_FEATURE] = FEATURE_REPRESENT, [NODE_ANNOUNCE_FEATURE] = FEATURE_REPRESENT, [BOLT11_FEATURE] = FEATURE_DONT_REPRESENT, - [CHANNEL_FEATURE] = FEATURE_DONT_REPRESENT} }, + [CHANNEL_FEATURE] = FEATURE_DONT_REPRESENT, + [CHANNEL_TYPE_FEATURE] = FEATURE_REPRESENT} }, /* Zeroconf is always signalled in `init`, but we still * negotiate on a per-channel basis when calling `fundchannel` @@ -106,7 +109,8 @@ static const struct feature_style feature_styles[] = { [INIT_FEATURE] = FEATURE_REPRESENT, [NODE_ANNOUNCE_FEATURE] = FEATURE_REPRESENT, [BOLT11_FEATURE] = FEATURE_DONT_REPRESENT, - [CHANNEL_FEATURE] = FEATURE_DONT_REPRESENT} }, + [CHANNEL_FEATURE] = FEATURE_DONT_REPRESENT, + [CHANNEL_TYPE_FEATURE] = FEATURE_REPRESENT} }, { OPT_ROUTE_BLINDING, .copy_style = { [INIT_FEATURE] = FEATURE_REPRESENT, [NODE_ANNOUNCE_FEATURE] = FEATURE_REPRESENT, diff --git a/common/features.h b/common/features.h index 9a9783d92924..5866b105ef67 100644 --- a/common/features.h +++ b/common/features.h @@ -13,8 +13,9 @@ enum feature_place { BOLT12_OFFER_FEATURE, BOLT12_INVREQ_FEATURE, BOLT12_INVOICE_FEATURE, + CHANNEL_TYPE_FEATURE, }; -#define NUM_FEATURE_PLACE (BOLT12_INVOICE_FEATURE+1) +#define NUM_FEATURE_PLACE (CHANNEL_TYPE_FEATURE+1) extern const char *feature_place_names[NUM_FEATURE_PLACE]; /* The complete set of features for all contexts */ @@ -111,7 +112,7 @@ struct feature_set *feature_set_dup(const tal_t *ctx, * | 14/15 | `payment_secret` |... IN9 ... * | 16/17 | `basic_mpp` |... IN9 ... * | 18/19 | `option_support_large_channel` |... IN ... - * | 22/23 | `option_anchors` |... IN ... + * | 22/23 | `option_anchors` |... INT ... * | 24/25 | `option_route_blinding` |...IN9 ... * | 26/27 | `option_shutdown_anysegwit` |... IN ... * | 28/29 | `option_dual_fund` |... IN ... @@ -119,9 +120,9 @@ struct feature_set *feature_set_dup(const tal_t *ctx, * | 38/39 | `option_onion_messages` |... IN ... * | 42/43 | `option_provide_storage` |... IN ... * | 44/45 | `option_channel_type` |... IN ... - * | 46/47 | `option_scid_alias` | ... IN ... + * | 46/47 | `option_scid_alias` | ... INT ... * | 48/49 | `option_payment_metadata` |... 9 ... - * | 50/51 | `option_zeroconf` | ... IN ... + * | 50/51 | `option_zeroconf` | ... INT ... * | 60/61 | `option_simple_close` |... IN ... */ /* BOLT-splice #9: From bab4c369de0d451a53ed71597a87475ab8891c30 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 30 Apr 2026 12:27:43 +0930 Subject: [PATCH 10/18] common: BOLT update which adds taproot fallback. Signed-off-by: Rusty Russell --- Makefile | 2 +- common/test/run-bolt11.c | 202 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index dd31c360c0b0..737bd6055971 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ CCANDIR := ccan # Where we keep the BOLT RFCs BOLTDIR := ../bolts/ -DEFAULT_BOLTVERSION := 89dfc59529a9ce845b06e7d6a62851a4ea096158 +DEFAULT_BOLTVERSION := 40e21b134e76efaa93eb2027ed0bcc3e2afb1d90 # Can be overridden on cmdline. BOLTVERSION := $(DEFAULT_BOLTVERSION) diff --git a/common/test/run-bolt11.c b/common/test/run-bolt11.c index b36ae7966f2a..33b228bc7df8 100644 --- a/common/test/run-bolt11.c +++ b/common/test/run-bolt11.c @@ -3,6 +3,7 @@ #include "../bech32.c" #include "../bech32_util.c" #include "../bolt11.c" +#include "../json_param.c" #include "../features.c" #include "../node_id.c" #include "../hash_u5.c" @@ -13,6 +14,137 @@ #include /* AUTOGENERATED MOCKS START */ +/* Generated stub for command_check_done */ +struct command_result *command_check_done(struct command *cmd) + +{ fprintf(stderr, "command_check_done called!\n"); abort(); } +/* Generated stub for command_check_only */ +bool command_check_only(const struct command *cmd UNNEEDED) +{ fprintf(stderr, "command_check_only called!\n"); abort(); } +/* Generated stub for command_deprecated_in_ok */ +bool command_deprecated_in_ok(struct command *cmd UNNEEDED, + const char *param UNNEEDED, + const char *depr_start UNNEEDED, + const char *depr_end UNNEEDED) +{ fprintf(stderr, "command_deprecated_in_ok called!\n"); abort(); } +/* Generated stub for command_dev_apis */ +bool command_dev_apis(const struct command *cmd UNNEEDED) +{ fprintf(stderr, "command_dev_apis called!\n"); abort(); } +/* Generated stub for command_fail */ +struct command_result *command_fail(struct command *cmd UNNEEDED, enum jsonrpc_errcode code UNNEEDED, + const char *fmt UNNEEDED, ...) + +{ fprintf(stderr, "command_fail called!\n"); abort(); } +/* Generated stub for command_fail_badparam */ +struct command_result *command_fail_badparam(struct command *cmd UNNEEDED, + const char *paramname UNNEEDED, + const char *buffer UNNEEDED, + const jsmntok_t *tok UNNEEDED, + const char *msg UNNEEDED) +{ fprintf(stderr, "command_fail_badparam called!\n"); abort(); } +/* Generated stub for command_set_usage */ +void command_set_usage(struct command *cmd UNNEEDED, const char *usage UNNEEDED) +{ fprintf(stderr, "command_set_usage called!\n"); abort(); } +/* Generated stub for command_usage_only */ +bool command_usage_only(const struct command *cmd UNNEEDED) +{ fprintf(stderr, "command_usage_only called!\n"); abort(); } +/* Generated stub for json_get_membern */ +const jsmntok_t *json_get_membern(const char *buffer UNNEEDED, + const jsmntok_t tok[] UNNEEDED, + const char *label UNNEEDED, size_t len UNNEEDED) +{ fprintf(stderr, "json_get_membern called!\n"); abort(); } +/* Generated stub for json_next */ +const jsmntok_t *json_next(const jsmntok_t *tok UNNEEDED) +{ fprintf(stderr, "json_next called!\n"); abort(); } +/* Generated stub for json_scan */ +const char *json_scan(const tal_t *ctx UNNEEDED, + const char *buffer UNNEEDED, + const jsmntok_t *tok UNNEEDED, + const char *guide UNNEEDED, + ...) +{ fprintf(stderr, "json_scan called!\n"); abort(); } +/* Generated stub for json_strdup */ +char *json_strdup(const tal_t *ctx UNNEEDED, const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED) +{ fprintf(stderr, "json_strdup called!\n"); abort(); } +/* Generated stub for json_to_bool */ +bool json_to_bool(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, bool *b UNNEEDED) +{ fprintf(stderr, "json_to_bool called!\n"); abort(); } +/* Generated stub for json_to_channel_id */ +bool json_to_channel_id(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + struct channel_id *cid UNNEEDED) +{ fprintf(stderr, "json_to_channel_id called!\n"); abort(); } +/* Generated stub for json_to_millionths */ +bool json_to_millionths(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + u64 *millionths UNNEEDED) +{ fprintf(stderr, "json_to_millionths called!\n"); abort(); } +/* Generated stub for json_to_msat */ +bool json_to_msat(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + struct amount_msat *msat UNNEEDED) +{ fprintf(stderr, "json_to_msat called!\n"); abort(); } +/* Generated stub for json_to_node_id */ +bool json_to_node_id(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + struct node_id *id UNNEEDED) +{ fprintf(stderr, "json_to_node_id called!\n"); abort(); } +/* Generated stub for json_to_number */ +bool json_to_number(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + unsigned int *num UNNEEDED) +{ fprintf(stderr, "json_to_number called!\n"); abort(); } +/* Generated stub for json_to_outpoint */ +bool json_to_outpoint(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + struct bitcoin_outpoint *op UNNEEDED) +{ fprintf(stderr, "json_to_outpoint called!\n"); abort(); } +/* Generated stub for json_to_pubkey */ +bool json_to_pubkey(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + struct pubkey *pubkey UNNEEDED) +{ fprintf(stderr, "json_to_pubkey called!\n"); abort(); } +/* Generated stub for json_to_s64 */ +bool json_to_s64(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, s64 *num UNNEEDED) +{ fprintf(stderr, "json_to_s64 called!\n"); abort(); } +/* Generated stub for json_to_short_channel_id */ +bool json_to_short_channel_id(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + struct short_channel_id *scid UNNEEDED) +{ fprintf(stderr, "json_to_short_channel_id called!\n"); abort(); } +/* Generated stub for json_to_short_channel_id_dir */ +bool json_to_short_channel_id_dir(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + struct short_channel_id_dir *scidd UNNEEDED) +{ fprintf(stderr, "json_to_short_channel_id_dir called!\n"); abort(); } +/* Generated stub for json_to_txid */ +bool json_to_txid(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + struct bitcoin_txid *txid UNNEEDED) +{ fprintf(stderr, "json_to_txid called!\n"); abort(); } +/* Generated stub for json_to_u16 */ +bool json_to_u16(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + uint16_t *num UNNEEDED) +{ fprintf(stderr, "json_to_u16 called!\n"); abort(); } +/* Generated stub for json_to_u32 */ +bool json_to_u32(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, u32 *num UNNEEDED) +{ fprintf(stderr, "json_to_u32 called!\n"); abort(); } +/* Generated stub for json_to_u64 */ +bool json_to_u64(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, u64 *num UNNEEDED) +{ fprintf(stderr, "json_to_u64 called!\n"); abort(); } +/* Generated stub for json_tok_bin_from_hex */ +u8 *json_tok_bin_from_hex(const tal_t *ctx UNNEEDED, const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED) +{ fprintf(stderr, "json_tok_bin_from_hex called!\n"); abort(); } +/* Generated stub for json_tok_full */ +const char *json_tok_full(const char *buffer UNNEEDED, const jsmntok_t *t UNNEEDED) +{ fprintf(stderr, "json_tok_full called!\n"); abort(); } +/* Generated stub for json_tok_full_len */ +int json_tok_full_len(const jsmntok_t *t UNNEEDED) +{ fprintf(stderr, "json_tok_full_len called!\n"); abort(); } +/* Generated stub for json_tok_is_null */ +bool json_tok_is_null(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED) +{ fprintf(stderr, "json_tok_is_null called!\n"); abort(); } +/* Generated stub for json_tok_is_num */ +bool json_tok_is_num(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED) +{ fprintf(stderr, "json_tok_is_num called!\n"); abort(); } +/* Generated stub for json_tok_strneq */ +bool json_tok_strneq(const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + const char *str UNNEEDED, size_t len UNNEEDED) +{ fprintf(stderr, "json_tok_strneq called!\n"); abort(); } +/* Generated stub for lease_rates_fromhex */ +struct lease_rates *lease_rates_fromhex(const tal_t *ctx UNNEEDED, + const char *hexdata UNNEEDED, size_t len UNNEEDED) +{ fprintf(stderr, "lease_rates_fromhex called!\n"); abort(); } /* Generated stub for siphash_seed */ const struct siphash_seed *siphash_seed(void) { fprintf(stderr, "siphash_seed called!\n"); abort(); } @@ -127,6 +259,27 @@ static void test_b11(const char *b11str, assert(strlen(reproduce) == strlen(b11str)); } +static const u8 **fallbacks(const tal_t *ctx, + const struct chainparams *chainparams, + const char *addr) +{ + jsmntok_t tok; + const u8 **addrs = tal_arr(ctx, const u8 *, 1); + + tok.type = JSMN_STRING; + tok.start = 0; + tok.end = strlen(addr); + tok.size = tok.end; + + /* Reuse what we use in json_invoice for fallbacks! */ + assert(json_to_address_scriptpubkey(ctx, + chainparams, + addr, &tok, + &addrs[0]) + == ADDRESS_PARSE_SUCCESS); + return addrs; +} + int main(int argc, char *argv[]) { struct bolt11 *b11; @@ -287,6 +440,55 @@ int main(int argc, char *argv[]) test_b11("lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs9qrsgq7ea976txfraylvgzuxs8kgcw23ezlrszfnh8r6qtfpr6cxga50aj6txm9rxrydzd06dfeawfk6swupvz4erwnyutnjq7x39ymw6j38gp7ynn44", b11, "One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon"); dev_bolt11_omit_c_value = false; + /* BOLT #11: + * > ### On mainnet, with fallback (P2TR) address bc1pptdvg0d2nj99568qn6ssdy4cygnwuxgw2ukmnwgwz7jpqjz2kszse2s3lm + * > + * lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4pptdvg0d2nj99568qn6ssdy4cygnwuxgw2ukmnwgwz7jpqjz2kszs9qrsgqy606dznq28exnydt2r4c29y56xjtn3sk4mhgjtl4pg2y4ar3249rq4ajlmj9jy8zvlzw7cr8mggqzm842xfr0v72rswzq9xvr4hknfsqwmn6xd + * + * * `lnbc`: prefix, Lightning on Bitcoin mainnet + * * `20m`: amount (20 milli-bitcoin) + * * `1`: Bech32 separator + * * `pvjluez`: timestamp (1496314658) + * * `s`: payment secret... + * * `p`: payment hash... + * * `h`: tagged field: hash of description... + * * `f`: tagged field: fallback address + * * `p4`: `data_length` (`p` = 1, `4` = 21; 1 * 32 + 21 == 53) + * * `p`: 1, so witness version 1 + * * `ptdvg0d2nj99568qn6ssdy4cygnwuxgw2ukmnwgwz7jpqjz2kszs`: 260 bits = P2TR. + * * `9`: features... + * * `y606dznq28exnydt2r4c29y56xjtn3sk4mhgjtl4pg2y4ar3249rq4ajlmj9jy8zvlzw7cr8mggqzm842xfr0v72rswzq9xvr4hknfsq`: signature + * * `wmn6xd`: Bech32 checksum + * * Signature breakdown: + * * `269fa68a6051f26991ab50eb851494d1a4b9c616aeee892ff50a144af471554a3057b2fee45910e267c4ef6067da10016cf5519237b3ca1c1c2014cc1d6f69a6` hex of signature data (32-byte r, 32-byte s) + * * `0` (int) recovery flag contained in `signature` + * * `6c6e626332306d0b25fe64500d04444444444444444444444444444444444444444444444444444444444444444021a000081018202830384048000810182028303840480008101820283038404808105c343925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c10486a10adac43daa9c8a5a68e09ea10692b82226ee190e572db9b90e17a410484ab4050280704000` hex of data for signing (prefix + data after separator up to the start of the signature) + * * `116fdb0f18352c886deb263f6466eb40e5e6518b80231a1f9df86088bfa48043` hex of SHA256 of the preimage + * */ + msatoshi = AMOUNT_MSAT(20 * (1000ULL * 100000000) / 1000); + b11 = new_bolt11(tmpctx, &msatoshi); + b11->chain = chainparams_for_network("bitcoin"); + b11->timestamp = 1496314658; + b11->payment_secret = tal(b11, struct secret); + memset(b11->payment_secret, 0x11, sizeof(*b11->payment_secret)); + if (!hex_decode("0001020304050607080900010203040506070809000102030405060708090102", + strlen("0001020304050607080900010203040506070809000102030405060708090102"), + &b11->payment_hash, sizeof(b11->payment_hash))) + abort(); + b11->receiver_id = node; + b11->description_hash = tal(b11, struct sha256); + if (!hex_decode("3925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c1", + strlen("3925b6f67e2c340036ed12093dd44e0368df1b6ea26c53dbe4811f58fd5db8c1"), + b11->description_hash, sizeof(*b11->description_hash))) + abort(); + set_feature_bit(&b11->features, 8); + set_feature_bit(&b11->features, 14); + b11->fallbacks = fallbacks(b11, b11->chain, "bc1pptdvg0d2nj99568qn6ssdy4cygnwuxgw2ukmnwgwz7jpqjz2kszse2s3lm"); + + dev_bolt11_omit_c_value = true; + test_b11("lnbc20m1pvjluezsp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4pptdvg0d2nj99568qn6ssdy4cygnwuxgw2ukmnwgwz7jpqjz2kszs9qrsgqy606dznq28exnydt2r4c29y56xjtn3sk4mhgjtl4pg2y4ar3249rq4ajlmj9jy8zvlzw7cr8mggqzm842xfr0v72rswzq9xvr4hknfsqwmn6xd", b11, "One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon"); + dev_bolt11_omit_c_value = false; + /* Malformed bolt11 strings (no '1'). */ badstr = "lnbc20mpvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqscc6gd6ql3jrc5yzme8v4ntcewwz5cnw92tz0pc8qcuufvq7khhr8wpald05e92xw006sq94mg8v2ndf4sefvf9sygkshp5zfem29trqq2yxxz7"; From 257fa59c0689424e3c35f338eb7c6adceb71de62 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 30 Apr 2026 13:17:07 +0930 Subject: [PATCH 11/18] gossipd: don't forget closed channels until 72 blocks, not 12. Touches a pile of tests, but they're easy to find. Changelog-Changed: Protocol: We now wait 72 blocks, not 12, before closing channels (BOLT update) Signed-off-by: Rusty Russell Signed-off-by: Rusty Russell --- Makefile | 2 +- gossipd/gossmap_manage.c | 6 +++--- tests/test_connection.py | 6 +++--- tests/test_db.py | 2 +- tests/test_gossip.py | 28 ++++++++++++++-------------- tests/test_invoices.py | 2 +- tests/test_pay.py | 2 +- tests/test_plugin.py | 2 +- tests/test_splicing.py | 4 ++-- tests/test_wallet.py | 6 ++---- 10 files changed, 29 insertions(+), 31 deletions(-) diff --git a/Makefile b/Makefile index 737bd6055971..e9a6942e084b 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ CCANDIR := ccan # Where we keep the BOLT RFCs BOLTDIR := ../bolts/ -DEFAULT_BOLTVERSION := 40e21b134e76efaa93eb2027ed0bcc3e2afb1d90 +DEFAULT_BOLTVERSION := 4fe3f0b6056638b46dd3e5317947ebc2491758e8 # Can be overridden on cmdline. BOLTVERSION := $(DEFAULT_BOLTVERSION) diff --git a/gossipd/gossmap_manage.c b/gossipd/gossmap_manage.c index 91a24757643b..82a20a8bd617 100644 --- a/gossipd/gossmap_manage.c +++ b/gossipd/gossmap_manage.c @@ -1455,12 +1455,12 @@ void gossmap_manage_channel_spent(struct gossmap_manage *gm, /* BOLT #7: * - once its funding output has been spent OR reorganized out: - * - SHOULD forget a channel after a 12-block delay. + * - SHOULD forget a channel after a 72-block delay. */ - cd.deadline = blockheight + 12; + cd.deadline = blockheight + 72; cd.scid = scid; - /* Remember locally so we can kill it in 12 blocks */ + /* Remember locally so we can kill it in 72 blocks */ status_trace("channel %s closing soon due" " to the funding outpoint being spent", fmt_short_channel_id(tmpctx, scid)); diff --git a/tests/test_connection.py b/tests/test_connection.py index f868b31cdc82..c1abc69ca246 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -4069,7 +4069,7 @@ def test_multichan(node_factory, executor, bitcoind): l2.rpc.close(l3.info['id']) l2.rpc.close(scid23b) - bitcoind.generate_block(13, wait_for_mempool=1) + bitcoind.generate_block(73, wait_for_mempool=1) sync_blockheight(bitcoind, [l1, l2, l3]) # Gossip works as expected. @@ -4120,7 +4120,7 @@ def test_multichan(node_factory, executor, bitcoind): "id": 2, "created_index": 3, "updated_index": 27, - "expiry": 135, + "expiry": 195, "direction": "out", "amount_msat": Millisatoshi(100001001), "payment_hash": inv3['payment_hash'], @@ -4129,7 +4129,7 @@ def test_multichan(node_factory, executor, bitcoind): "id": 3, "created_index": 4, "updated_index": 36, - "expiry": 135, + "expiry": 195, "direction": "out", "amount_msat": Millisatoshi(100001001), "payment_hash": inv4['payment_hash'], diff --git a/tests/test_db.py b/tests/test_db.py index b186ee35eb71..98b3446762b5 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -96,7 +96,7 @@ def test_block_backfill(node_factory, bitcoind, chainparams): # Now close the channel and make sure `l3` cleans up correctly: txid = only_one(l1.rpc.close(l2.info['id'])['txids']) - bitcoind.generate_block(13, wait_for_mempool=txid) + bitcoind.generate_block(73, wait_for_mempool=txid) wait_for(lambda: len(l3.rpc.listchannels()['channels']) == 0) diff --git a/tests/test_gossip.py b/tests/test_gossip.py index 688604da7774..e55492cddd6f 100644 --- a/tests/test_gossip.py +++ b/tests/test_gossip.py @@ -581,8 +581,8 @@ def non_public(node): l1.rpc.dev_fail(l2.info['id']) # We need to wait for the unilateral close to hit the mempool, - # and 12 blocks for nodes to actually forget it. - bitcoind.generate_block(13, wait_for_mempool=1) + # and 72 blocks for nodes to actually forget it. + bitcoind.generate_block(73, wait_for_mempool=1) wait_for(lambda: active(l1) == [scid23, scid23]) wait_for(lambda: active(l2) == [scid23, scid23]) @@ -1445,7 +1445,7 @@ def test_gossip_notices_close(node_factory, bitcoind): txid = only_one(l2.rpc.close(l3.info['id'])['txids']) wait_for(lambda: l2.rpc.listpeerchannels(l3.info['id'])['channels'][0]['state'] == 'CLOSINGD_COMPLETE') - bitcoind.generate_block(13, txid) + bitcoind.generate_block(73, txid) wait_for(lambda: l1.rpc.listchannels()['channels'] == []) wait_for(lambda: l1.rpc.listnodes()['nodes'] == []) @@ -1683,7 +1683,7 @@ def test_gossip_store_compact_while_extending(node_factory, bitcoind, executor): scid23 = only_one(l2.rpc.listpeerchannels(l3.info['id'])['channels'])['short_channel_id'] l2.rpc.close(l3.info['id']) - bitcoind.generate_block(13, wait_for_mempool=1) + bitcoind.generate_block(73, wait_for_mempool=1) wait_for(lambda: l1.rpc.listchannels(scid23) == {'channels': []}) l1.rpc.setchannel(l2.info['id'], 41, 1004) @@ -2008,7 +2008,7 @@ def test_topology_leak(node_factory, bitcoind): # Close and wait for gossip to catchup. txid = only_one(l2.rpc.close(l3.info['id'])['txids']) - bitcoind.generate_block(13, txid) + bitcoind.generate_block(73, txid) wait_for(lambda: len(l1.rpc.listchannels()['channels']) == 2) @@ -2035,7 +2035,7 @@ def test_parms_listforwards(node_factory): assert len(forwards_dep) == 0 -def test_close_12_block_delay(node_factory, bitcoind): +def test_close_72_block_delay(node_factory, bitcoind): l1, l2, l3, l4 = node_factory.line_graph(4, wait_for_announce=True) # Close l1-l2 @@ -2052,16 +2052,16 @@ def test_close_12_block_delay(node_factory, bitcoind): # BOLT #7: # - once its funding output has been spent OR reorganized out: - # - SHOULD forget a channel after a 12-block delay. + # - SHOULD forget a channel after a 72-block delay. - # That implies 12 blocks *after* spending, i.e. 13 blocks deep! + # That implies 72 blocks *after* spending, i.e. 73 blocks deep! - # 12 blocks deep, l4 still sees it - bitcoind.generate_block(10) + # 72 blocks deep, l4 still sees it + bitcoind.generate_block(70) sync_blockheight(bitcoind, [l4]) assert len(l4.rpc.listchannels(source=l1.info['id'])['channels']) == 1 - # 13 blocks deep does it. + # 73 blocks deep does it. bitcoind.generate_block(1) wait_for(lambda: l4.rpc.listchannels(source=l1.info['id'])['channels'] == []) @@ -2490,7 +2490,7 @@ def test_gossmap_lost_node(node_factory, bitcoind): scid23 = only_one(l2.rpc.listpeerchannels(l3.info['id'])['channels'])['short_channel_id'] l2.rpc.close(l3.info['id']) - bitcoind.generate_block(13, wait_for_mempool=1) + bitcoind.generate_block(73, wait_for_mempool=1) # Order of nodes is not stable. sync_blockheight(bitcoind, [l1]) @@ -2522,6 +2522,6 @@ def test_gossip_dying_when_compact(node_factory, bitcoind): wait_for(lambda: len(l1.rpc.listchannels()["channels"]) == 4) l1.rpc.call("dev-compact-gossip-store") - # Now actually close it (12 deep) - bitcoind.generate_block(11) + # Now actually close it (72 deep) + bitcoind.generate_block(71) wait_for(lambda: len(l1.rpc.listchannels()["channels"]) == 2) diff --git a/tests/test_invoices.py b/tests/test_invoices.py index 4b67c9aa79c3..161fbd08931e 100644 --- a/tests/test_invoices.py +++ b/tests/test_invoices.py @@ -362,7 +362,7 @@ def test_invoice_routeboost_private(node_factory, bitcoind): # It will use an explicit exposeprivatechannels even if it thinks its a dead-end l0.rpc.close(l1.info['id']) l0.wait_for_channel_onchain(l1.info['id']) - bitcoind.generate_block(13) + bitcoind.generate_block(73) wait_for(lambda: l2.rpc.listchannels(scid_dummy)['channels'] == []) inv = l2.rpc.invoice(amount_msat=123456, label="inv7", description="?", exposeprivatechannels=scid) diff --git a/tests/test_pay.py b/tests/test_pay.py index cc22f26b8f4f..2d0c30f828fc 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -5891,7 +5891,7 @@ def test_offer_paths(node_factory, bitcoind): # Make scid path invalid by closing it close = l1.rpc.close(paths[0]['first_scid']) - bitcoind.generate_block(13, wait_for_mempool=only_one(close['txids'])) + bitcoind.generate_block(73, wait_for_mempool=only_one(close['txids'])) wait_for(lambda: l5.rpc.listchannels(paths[0]['first_scid']) == {'channels': []}) # Now connect l5->l4, and it will be able to reach l3 via that, and join blinded path. diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 10b93255e28f..2a40d4757995 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -4320,7 +4320,7 @@ def test_sql(node_factory, bitcoind): # This has to wait for the hold_invoice plugin to let go! open(os.path.join(l3.daemon.lightning_dir, TEST_NETWORK, "unhold"), "w").close() txid = only_one(l1.rpc.close(l2.info['id'])['txids']) - bitcoind.generate_block(13, wait_for_mempool=txid) + bitcoind.generate_block(73, wait_for_mempool=txid) wait_for(lambda: len(l3.rpc.listchannels(source=l1.info['id'])['channels']) == 0) assert len(l3.rpc.sql("SELECT * FROM channels WHERE source = X'{}';".format(l1.info['id']))['rows']) == 0 l3.daemon.wait_for_log("Deleting channel: {}".format(scid)) diff --git a/tests/test_splicing.py b/tests/test_splicing.py index 458f432f8630..f713ff7ba14e 100644 --- a/tests/test_splicing.py +++ b/tests/test_splicing.py @@ -229,7 +229,7 @@ def test_splice_gossip(node_factory, bitcoind): bitcoind.generate_block(5, wait_for_mempool=result['txid']) - # l3 will see channel dying, but still consider it OK for 12 blocks. + # l3 will see channel dying, but still consider it OK for 72 blocks. l3.daemon.wait_for_log(f'gossipd: channel {pre_splice_scid} closing soon due to the funding outpoint being spent') assert len(l3.rpc.listchannels(short_channel_id=pre_splice_scid)['channels']) == 2 assert len(l3.rpc.listchannels(source=l1.info['id'])['channels']) == 1 @@ -246,7 +246,7 @@ def test_splice_gossip(node_factory, bitcoind): wait_for(lambda: len(l3.rpc.listchannels(short_channel_id=post_splice_scid)['channels']) == 2) assert len(l3.rpc.listchannels(short_channel_id=pre_splice_scid)['channels']) == 2 - bitcoind.generate_block(7) + bitcoind.generate_block(67) # The old channel should fall off l3's perspective wait_for(lambda: l3.rpc.listchannels(short_channel_id=pre_splice_scid)['channels'] == []) diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 5be686d11ef4..e4d1cc4fdd66 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -2562,7 +2562,7 @@ def test_unspend_during_reorg(node_factory, bitcoind): # Now, l3 sees the close, marks channel dying. l1.rpc.close(l2.info['id']) spentheight = bitcoind.rpc.getblockcount() + 1 - bitcoind.generate_block(14, wait_for_mempool=1) + 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) @@ -2576,12 +2576,10 @@ def test_unspend_during_reorg(node_factory, bitcoind): # Restart, see replay. l3.stop() # This is enough to take channel from dying to dead. - bitcoind.generate_block(10) + bitcoind.generate_block(70) l3.start() # Channel should still be dead. - l3.daemon.wait_for_log(f"Adding block {spentheight}") - sync_blockheight(bitcoind, [l3]) assert only_one(l3.db_query(f"SELECT spendheight as spendheight FROM utxoset WHERE blockheight={blockheight} AND txindex={txindex}"))['spendheight'] == spentheight From 37a3acc214e6bb4001640799e8987a776af4b3a3 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 30 Apr 2026 13:17:10 +0930 Subject: [PATCH 12/18] BOLTs: more textual changes. Attributable errors and some reordering, but nothing beyond text changes for us. --- Makefile | 2 +- common/bolt11.c | 29 ++++++++++--------- common/sphinx.c | 7 ++--- contrib/pyln-proto/pyln/proto/invoice.py | 5 ++-- hsmd/libhsmd.c | 13 ++++----- ...xtracted_peer-shutdown-wrong_funding.patch | 6 ++-- wire/peer_wire.csv | 16 +++++----- 7 files changed, 38 insertions(+), 40 deletions(-) diff --git a/Makefile b/Makefile index e9a6942e084b..f8e586e56aaf 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ CCANDIR := ccan # Where we keep the BOLT RFCs BOLTDIR := ../bolts/ -DEFAULT_BOLTVERSION := 4fe3f0b6056638b46dd3e5317947ebc2491758e8 +DEFAULT_BOLTVERSION := bdf790bfdc10c2e894be3546efdc80a60d93da8c # Can be overridden on cmdline. BOLTVERSION := $(DEFAULT_BOLTVERSION) diff --git a/common/bolt11.c b/common/bolt11.c index bd15e3af1320..26263dd028c0 100644 --- a/common/bolt11.c +++ b/common/bolt11.c @@ -185,7 +185,7 @@ static const char *decode_p(struct bolt11 *b11, /* BOLT #11: * * A reader... - * - MUST fail the payment if any mandatory field (`p`, `h`, `s`, `n`) + * - MUST fail the payment if any field with fixed `data_length` (`p`, `h`, `s`, `n`) * does not have the correct length (52, 52, 52, 53). */ return pull_expected_length(b11, hu5, data, field_len, 52, 'p', @@ -239,7 +239,7 @@ static const char *decode_h(struct bolt11 *b11, /* BOLT #11: * * A reader... - * - MUST fail the payment if any mandatory field (`p`, `h`, `s`, `n`) + * - MUST fail the payment if any field with fixed `data_length` (`p`, `h`, `s`, `n`) * does not have the correct length (52, 52, 52, 53). */ err = pull_expected_length(b11, hu5, data, field_len, 52, 'h', have_h, &hash); @@ -324,7 +324,7 @@ static const char *decode_n(struct bolt11 *b11, /* BOLT #11: * * A reader... - * - MUST fail the payment if any mandatory field (`p`, `h`, `s`, `n`) + * - MUST fail the payment if any field with fixed `data_length` (`p`, `h`, `s`, `n`) * does not have the correct length (52, 52, 52, 53). */ err = pull_expected_length(b11, hu5, data, field_len, 53, 'n', have_n, &b11->receiver_id.k); @@ -360,7 +360,7 @@ static const char *decode_s(struct bolt11 *b11, /* BOLT #11: * * A reader... - * - MUST fail the payment if any mandatory field (`p`, `h`, `s`, `n`) + * - MUST fail the payment if any field with fixed `data_length` (`p`, `h`, `s`, `n`) * does not have the correct length (52, 52, 52, 53). */ err = pull_expected_length(b11, hu5, data, field_len, 52, 's', have_s, &secret); @@ -876,7 +876,8 @@ struct bolt11 *bolt11_decode_nosig(const tal_t *ctx, const char *str, * * 1. `timestamp`: seconds-since-1970 (35 bits, big-endian) * 1. zero or more tagged parts - * 1. `signature`: Bitcoin-style signature of above (520 bits) + * 1. `signature`: compact ECDSA/secp256k1 signature of the above + * (520 bits: 64-byte R||S + 1-byte recovery id) */ err = pull_uint(&hu5, &data, &data_len, &b11->timestamp, 35, false); if (err) @@ -999,13 +1000,12 @@ struct bolt11 *bolt11_decode(const tal_t *ctx, const char *str, /* BOLT #11: * - * A writer...MUST set `signature` to a valid 512-bit - * secp256k1 signature of the SHA2 256-bit hash of the - * human-readable part, represented as UTF-8 bytes, - * concatenated with the data part (excluding the signature) - * with 0 bits appended to pad the data to the next byte - * boundary, with a trailing byte containing the recovery ID - * (0, 1, 2, or 3). + * A writer...MUST set `signature` to a valid + * compact ECDSA signature over secp256k1 of the SHA-256 hash of: + * the human-readable part (as UTF-8 bytes) concatenated with the data part + * (excluding the signature), with 0 bits appended to pad to a byte boundary. + * The signature is encoded as 64 bytes (R || S), followed by a trailing 1-byte + * recovery id in {0,1,2,3}. */ data_len = tal_count(sigdata); err = pull_bits(NULL, &sigdata, &data_len, sig_and_recid, 520, false); @@ -1036,7 +1036,7 @@ struct bolt11 *bolt11_decode(const tal_t *ctx, const char *str, * A reader: * ... * - if a valid `n` field is provided: - * - MUST use the `n` field to validate the signature instead of performing signature recovery. + * - MUST use the `n` field to validate the signature instead of performing public-key recovery. */ if (!have_n) { struct pubkey k; @@ -1319,7 +1319,8 @@ char *bolt11_encode_(const tal_t *ctx, * * 1. `timestamp`: seconds-since-1970 (35 bits, big-endian) * 1. zero or more tagged parts - * 1. `signature`: Bitcoin-style signature of above (520 bits) + * 1. `signature`: compact ECDSA/secp256k1 signature of the above + * (520 bits: 64-byte R||S + 1-byte recovery id) */ push_varlen_uint(&data, b11->timestamp, 35); diff --git a/common/sphinx.c b/common/sphinx.c index 6761c79f945c..7adea81627d5 100644 --- a/common/sphinx.c +++ b/common/sphinx.c @@ -768,9 +768,8 @@ struct onionreply *create_onionreply(const tal_t *ctx, /* BOLT #4: * - * The node generating the error message (_erring node_) builds a return - * packet consisting of - * the following fields: + * The node generating the error message builds a _return + * packet_ consisting of the following fields: * * 1. data: * * [`32*byte`:`hmac`] @@ -816,8 +815,6 @@ struct onionreply *wrap_onionreply(const tal_t *ctx, * The erring node then generates a new key, using the key type `ammag`. * This key is then used to generate a pseudo-random stream, which is * in turn applied to the packet using `XOR`. - * - * The obfuscation step is repeated by every hop along the return path. */ subkey_from_hmac("ammag", shared_secret, &key); result->contents = tal_dup_talarr(result, u8, reply->contents); diff --git a/contrib/pyln-proto/pyln/proto/invoice.py b/contrib/pyln-proto/pyln/proto/invoice.py index 27591cb040e0..9a53f871f672 100755 --- a/contrib/pyln-proto/pyln/proto/invoice.py +++ b/contrib/pyln-proto/pyln/proto/invoice.py @@ -329,7 +329,8 @@ def decode(cls, b): # BOLT #11: # A reader: # - MUST skip over `f` fields that use an unknown `version`. - # - MUST fail the payment if any mandatory field (`p`, `h`, `s`, `n`) does not have the correct length (52, 52, 52, 53). + # - MUST fail the payment if any field with fixed `data_length` (`p`, `h`, `s`, `n`) + # does not have the correct length (52, 52, 52, 53). data_length = len(tagdata) / 5 if tag == 'r': @@ -392,7 +393,7 @@ def decode(cls, b): # BOLT #11: # A reader:... # - if a valid `n` field is provided: - # - MUST use the `n` field to validate the signature instead of performing signature recovery. + # - MUST use the `n` field to validate the signature instead of performing public-key recovery. inv.signature = inv.pubkey.ecdsa_deserialize_compact(sigdecoded[0:64]) if not inv.pubkey.ecdsa_verify(bytearray([ord(c) for c in hrp]) + data.tobytes(), inv.signature): raise ValueError('Invalid signature') diff --git a/hsmd/libhsmd.c b/hsmd/libhsmd.c index f8d35a044ca6..c605198a056f 100644 --- a/hsmd/libhsmd.c +++ b/hsmd/libhsmd.c @@ -1016,13 +1016,12 @@ static u8 *handle_sign_invoice(struct hsmd_client *c, const u8 *msg_in) /* BOLT #11: * - * A writer... MUST set `signature` to a valid 512-bit - * secp256k1 signature of the SHA2 256-bit hash of the - * human-readable part, represented as UTF-8 bytes, - * concatenated with the data part (excluding the signature) - * with 0 bits appended to pad the data to the next byte - * boundary, with a trailing byte containing the recovery ID - * (0, 1, 2, or 3). + * A writer... MUST set `signature` to a valid compact ECDSA signature + * over secp256k1 of the SHA-256 hash of: the human-readable part (as + * UTF-8 bytes) concatenated with the data part (excluding the + * signature), with 0 bits appended to pad to a byte boundary. The + * signature is encoded as 64 bytes (R || S), followed by a trailing + * 1-byte recovery id in {0,1,2,3}. */ /* FIXME: Check invoice! */ diff --git a/wire/extracted_peer-shutdown-wrong_funding.patch b/wire/extracted_peer-shutdown-wrong_funding.patch index 114380237c4e..ec8f52393559 100644 --- a/wire/extracted_peer-shutdown-wrong_funding.patch +++ b/wire/extracted_peer-shutdown-wrong_funding.patch @@ -8,6 +8,6 @@ +tlvtype,shutdown_tlvs,wrong_funding,100 +tlvdata,shutdown_tlvs,wrong_funding,txid,sha256, +tlvdata,shutdown_tlvs,wrong_funding,outnum,u32, - msgtype,closing_signed,39 - msgdata,closing_signed,channel_id,channel_id, - msgdata,closing_signed,fee_satoshis,u64, + msgtype,closing_complete,40 + msgdata,closing_complete,channel_id,channel_id, + msgdata,closing_complete,closer_scriptpubkey_len,u16, diff --git a/wire/peer_wire.csv b/wire/peer_wire.csv index 4b01c56836cc..1dad37d9b7f3 100644 --- a/wire/peer_wire.csv +++ b/wire/peer_wire.csv @@ -241,14 +241,6 @@ msgdata,shutdown,tlvs,shutdown_tlvs, tlvtype,shutdown_tlvs,wrong_funding,100 tlvdata,shutdown_tlvs,wrong_funding,txid,sha256, tlvdata,shutdown_tlvs,wrong_funding,outnum,u32, -msgtype,closing_signed,39 -msgdata,closing_signed,channel_id,channel_id, -msgdata,closing_signed,fee_satoshis,u64, -msgdata,closing_signed,signature,signature, -msgdata,closing_signed,tlvs,closing_signed_tlvs, -tlvtype,closing_signed_tlvs,fee_range,1 -tlvdata,closing_signed_tlvs,fee_range,min_fee_satoshis,u64, -tlvdata,closing_signed_tlvs,fee_range,max_fee_satoshis,u64, msgtype,closing_complete,40 msgdata,closing_complete,channel_id,channel_id, msgdata,closing_complete,closer_scriptpubkey_len,u16, @@ -273,6 +265,14 @@ tlvtype,closing_tlvs,closee_output_only,2 tlvdata,closing_tlvs,closee_output_only,sig,signature, tlvtype,closing_tlvs,closer_and_closee_outputs,3 tlvdata,closing_tlvs,closer_and_closee_outputs,sig,signature, +msgtype,closing_signed,39 +msgdata,closing_signed,channel_id,channel_id, +msgdata,closing_signed,fee_satoshis,u64, +msgdata,closing_signed,signature,signature, +msgdata,closing_signed,tlvs,closing_signed_tlvs, +tlvtype,closing_signed_tlvs,fee_range,1 +tlvdata,closing_signed_tlvs,fee_range,min_fee_satoshis,u64, +tlvdata,closing_signed_tlvs,fee_range,max_fee_satoshis,u64, msgtype,update_add_htlc,128 msgdata,update_add_htlc,channel_id,channel_id, msgdata,update_add_htlc,id,u64, From 66a92fd23db27fe191205ae32744b4fafee77c0d Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 30 Apr 2026 13:17:10 +0930 Subject: [PATCH 13/18] common: assume OPT_PAYMENT_SECRET. This means we can assume support, but we *can't* assume it's present, because of keysend, which doesn't use it. Signed-off-by: Rusty Russell --- Makefile | 2 +- common/features.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index f8e586e56aaf..5a5f6feedb96 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ CCANDIR := ccan # Where we keep the BOLT RFCs BOLTDIR := ../bolts/ -DEFAULT_BOLTVERSION := bdf790bfdc10c2e894be3546efdc80a60d93da8c +DEFAULT_BOLTVERSION := d98366c900e20eb5475be8dee0c58878dca1f967 # Can be overridden on cmdline. BOLTVERSION := $(DEFAULT_BOLTVERSION) diff --git a/common/features.h b/common/features.h index 5866b105ef67..2861573647ea 100644 --- a/common/features.h +++ b/common/features.h @@ -109,7 +109,7 @@ struct feature_set *feature_set_dup(const tal_t *ctx, * | 8/9 | `var_onion_optin` |... ASSUMED ... * | 10/11 | `gossip_queries_ex` |... IN ... * | 12/13 | `option_static_remotekey` |... ASSUMED ... - * | 14/15 | `payment_secret` |... IN9 ... + * | 14/15 | `payment_secret` |... ASSUMED ... * | 16/17 | `basic_mpp` |... IN9 ... * | 18/19 | `option_support_large_channel` |... IN ... * | 22/23 | `option_anchors` |... INT ... @@ -119,7 +119,7 @@ struct feature_set *feature_set_dup(const tal_t *ctx, * | 34/35 | `option_quiesce` |... IN ... * | 38/39 | `option_onion_messages` |... IN ... * | 42/43 | `option_provide_storage` |... IN ... - * | 44/45 | `option_channel_type` |... IN ... + * | 44/45 | `option_channel_type` |... ASSUMED ... * | 46/47 | `option_scid_alias` | ... INT ... * | 48/49 | `option_payment_metadata` |... 9 ... * | 50/51 | `option_zeroconf` | ... INT ... From 1c8cae616e59f0cf47bac4b6ba3af78f5782a4e7 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 30 Apr 2026 13:17:10 +0930 Subject: [PATCH 14/18] tools/generate-wire.py: don't set TLV fields to NULL if they're empty. There's a new BOLT 12 test, which checks that the ->currencies array isn't empty. We were treating it as missing, which is wrong. So allocate empty arrays when they appear, instead of setting them to NULL. Signed-off-by: Rusty Russell --- tools/gen/impl_template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/gen/impl_template b/tools/gen/impl_template index c3b97d5f517d..7308aac69601 100644 --- a/tools/gen/impl_template +++ b/tools/gen/impl_template @@ -91,7 +91,7 @@ ${fieldname} = tal_arr(${ctx}, ${typename}, ${f.size('*plen')}); fromwire_${type_}_array(cursor, plen, ${fieldname}, ${f.size('*plen')}); % else: % if f.is_varlen(): -${fieldname} = ${f.size('*plen')} ? tal_arr(${ctx}, ${typename}, 0) : NULL; +${fieldname} = tal_arr(${ctx}, ${typename}, 0); % endif % if f.is_implicit_len(): while (*plen != 0) { From a15b88f074bb5eac8dcb1fb91c5016996b10f19e Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 30 Apr 2026 13:17:10 +0930 Subject: [PATCH 15/18] common: update BOLTs, reject "empty offer_chains" offers. It's a bit moot, since we can't pay them anyway, but this brings us into line with the test vectors. It *did* catch that we treated empty as missing, though. Signed-off-by: Rusty Russell --- Makefile | 2 +- common/bolt12.c | 6 +++++- common/test/run-bolt12-encode-test.c | 5 +++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 5a5f6feedb96..8e12c6ae937e 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ CCANDIR := ccan # Where we keep the BOLT RFCs BOLTDIR := ../bolts/ -DEFAULT_BOLTVERSION := d98366c900e20eb5475be8dee0c58878dca1f967 +DEFAULT_BOLTVERSION := 34455ffe28b308dd7ac7552234d565890af8605b # Can be overridden on cmdline. BOLTVERSION := $(DEFAULT_BOLTVERSION) diff --git a/common/bolt12.c b/common/bolt12.c index 4621e62f6960..d4fcd0053437 100644 --- a/common/bolt12.c +++ b/common/bolt12.c @@ -28,7 +28,7 @@ bool bolt12_chains_match(const struct bitcoin_blkid *chains, * - if the node does not accept bitcoin invoices: * - MUST NOT respond to the offer * - otherwise: (`offer_chains` is set): - * - if the node does not accept invoices for any of the `chains`: + * - if the node does not accept invoices for at least one of the `chains`: * - MUST NOT respond to the offer */ if (!chains) { @@ -62,6 +62,10 @@ static char *check_features_and_chain(const tal_t *ctx, if (must_be_chain) { if (!bolt12_chains_match(chains, num_chains, must_be_chain)) return tal_fmt(ctx, "wrong chain"); + } else { + /* Chains is *empty*, that can never work. */ + if (chains && tal_count(chains) == 0) + return tal_fmt(ctx, "offer_chains with zero entries"); } if (our_features) { diff --git a/common/test/run-bolt12-encode-test.c b/common/test/run-bolt12-encode-test.c index 911ce003c53b..43f3e5e0eb48 100644 --- a/common/test/run-bolt12-encode-test.c +++ b/common/test/run-bolt12-encode-test.c @@ -448,6 +448,11 @@ int main(int argc, char *argv[]) offer->offer_paths = paths; offer->offer_paths[1]->path = NULL; print_invalid_offer(offer, "Second offer_path is empty"); + offer->offer_paths = NULL; + + offer->offer_chains = tal_arr(offer, struct bitcoin_blkid, 0); + print_invalid_offer(offer, "offer_chains with zero entries"); + offer->offer_chains = NULL; printf("]\n"); common_shutdown(); From af68a3d2dade7c86aaddb0a3f70073899f6f3a36 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 30 Apr 2026 13:17:10 +0930 Subject: [PATCH 16/18] BOLTs: update for splicing: commit_sig retranmission. This needs Dusty's examination... Signed-off-by: Rusty Russell --- Makefile | 2 +- channeld/channeld.c | 6 +++--- openingd/dualopend.c | 17 ++++++++--------- wire/extracted_peer_11_splice.patch | 10 +++------- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/Makefile b/Makefile index 8e12c6ae937e..a511542d9da2 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ CCANDIR := ccan # Where we keep the BOLT RFCs BOLTDIR := ../bolts/ -DEFAULT_BOLTVERSION := 34455ffe28b308dd7ac7552234d565890af8605b +DEFAULT_BOLTVERSION := b9a1206eb2d7fe7c535e3399c212f289e88b2898 # Can be overridden on cmdline. BOLTVERSION := $(DEFAULT_BOLTVERSION) diff --git a/channeld/channeld.c b/channeld/channeld.c index 6fa715803057..11d8fdeea572 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -6087,9 +6087,9 @@ static void peer_reconnect(struct peer *peer, /* BOLT #2: * * - otherwise: - * - if `next_commitment_number` is not 1 greater than the - * commitment number of the last `commitment_signed` message the - * receiving node has sent: + * - if `next_commitment_number` is not equal to the commitment + * number of the next `commitment_signed` that the receiving + * node would send: * - SHOULD send an `error` and fail the channel. */ } else if (next_commitment_number != peer->next_index[REMOTE]) diff --git a/openingd/dualopend.c b/openingd/dualopend.c index bf70a3182cf3..e7da540dab70 100644 --- a/openingd/dualopend.c +++ b/openingd/dualopend.c @@ -3970,13 +3970,10 @@ static void do_reconnect_dance(struct state *state) /* BOLT #2: * - * - if it has sent `commitment_signed` for an - * interactive transaction construction but it has - * not received `tx_signatures`: - * - MUST set `next_funding_txid` to the txid of that - * interactive transaction. - * - otherwise: - * - MUST NOT set `next_funding_txid`. + * - if it has sent `commitment_signed` for an interactive transaction construction but + * it has not received `tx_signatures`: + * - MUST include the `next_funding` TLV. + * - MUST set `next_funding_txid` to the txid of that interactive transaction. */ tlvs = tlv_channel_reestablish_tlvs_new(tmpctx); if (!tx_state->remote_funding_sigs_rcvd) { @@ -4041,10 +4038,11 @@ static void do_reconnect_dance(struct state *state) /* BOLT #2: * A receiving node: - * - if `next_funding_txid` is set: + * - if the `next_funding` TLV is set: * - if `next_funding_txid` matches the latest interactive funding transaction: * - if it has not received `tx_signatures` for that funding transaction: - * - MUST retransmit its `commitment_signed` for that funding transaction. + * - if the `commitment_signed` bit is set in `retransmit_flags`: + * - MUST retransmit its `commitment_signed` for that funding transaction. * - if it has already received `commitment_signed` and it should sign first, * as specified in the [`tx_signatures` requirements](#the-tx_signatures-message): * - MUST send its `tx_signatures` for that funding transaction. @@ -4070,6 +4068,7 @@ static void do_reconnect_dance(struct state *state) if (!tx_state->has_commitments) send_our_sigs = false; } + /* FIXME: examine retransmit_flags! */ if (send_our_sigs && psbt_side_finalized(tx_state->psbt, state->our_role)) { msg = psbt_to_tx_sigs_msg(NULL, state, tx_state->psbt); peer_write(state->pps, take(msg)); diff --git a/wire/extracted_peer_11_splice.patch b/wire/extracted_peer_11_splice.patch index e4df1e2eb953..09a76c942683 100644 --- a/wire/extracted_peer_11_splice.patch +++ b/wire/extracted_peer_11_splice.patch @@ -50,14 +50,10 @@ index f3a42e5537..4b01c56836 100644 msgtype,revoke_and_ack,133 msgdata,revoke_and_ack,channel_id,channel_id, msgdata,revoke_and_ack,per_commitment_secret,byte,32 -@@ -301,8 +326,12 @@ msgdata,channel_reestablish,next_commitment_number,u64, - msgdata,channel_reestablish,next_revocation_number,u64, - msgdata,channel_reestablish,your_last_per_commitment_secret,byte,32 - msgdata,channel_reestablish,my_current_per_commitment_point,point, --tlvtype,channel_reestablish_tlvs,next_funding,0 -+tlvtype,channel_reestablish_tlvs,next_funding,1 +@@ -301,6 +326,9 @@ msgdata,channel_reestablish,next_commitment_number,u64, + tlvtype,channel_reestablish_tlvs,next_funding,1 tlvdata,channel_reestablish_tlvs,next_funding,next_funding_txid,sha256, -+tlvdata,channel_reestablish_tlvs,next_funding,retransmit_flags,byte, + tlvdata,channel_reestablish_tlvs,next_funding,retransmit_flags,byte, +tlvtype,channel_reestablish_tlvs,my_current_funding_locked,5 +tlvdata,channel_reestablish_tlvs,my_current_funding_locked,my_current_funding_locked_txid,sha256, +tlvdata,channel_reestablish_tlvs,my_current_funding_locked,retransmit_flags,byte, From ce6479f020161bcd662d125896b3f65f31c40bd6 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 30 Apr 2026 13:17:10 +0930 Subject: [PATCH 17/18] bech32: check for invalid/unnecessary trailing bits. There's a new test for bolt12 in commit 7153bed9705d7493 ("BOLT 12: add test vector for invalid bech32 padding (#1312)") which requires us to b stricter in decoding. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Rusty Russell --- Makefile | 2 +- common/bech32_util.c | 20 +++++++++++++++++--- common/bech32_util.h | 3 ++- plugins/offers.c | 5 ++++- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index a511542d9da2..db14d23efc99 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ CCANDIR := ccan # Where we keep the BOLT RFCs BOLTDIR := ../bolts/ -DEFAULT_BOLTVERSION := b9a1206eb2d7fe7c535e3399c212f289e88b2898 +DEFAULT_BOLTVERSION := 7153bed9705d7493065d9b818d25b282ef0a7c5e # Can be overridden on cmdline. BOLTVERSION := $(DEFAULT_BOLTVERSION) diff --git a/common/bech32_util.c b/common/bech32_util.c index b72777e7dc06..3722a19cb200 100644 --- a/common/bech32_util.c +++ b/common/bech32_util.c @@ -31,12 +31,16 @@ static u8 get_u5_bit(const u5 *src, size_t bitoff) return ((src[bitoff / 5] >> (4 - (bitoff % 5))) & 1); } -void bech32_pull_bits(u8 **data, const u5 *src, size_t nbits) +bool bech32_pull_bits(u8 **data, const u5 *src, size_t nbits) { size_t i; size_t data_len = tal_count(*data); + size_t pad = nbits % 8; + + /* More than 4 padding bits means a superfluous u5 group was added. */ + if (pad >= 5) + return false; - /* We discard trailing bits. */ for (i = 0; i + 8 <= nbits; i += 8) { tal_resize(data, data_len+1); (*data)[data_len] = 0; @@ -46,6 +50,13 @@ void bech32_pull_bits(u8 **data, const u5 *src, size_t nbits) } data_len++; } + + /* Padding bits must all be zero. */ + for (size_t b = 0; b < pad; b++) { + if (get_u5_bit(src, i + b)) + return false; + } + return true; } /* Returns a char, tracks case. */ @@ -95,7 +106,10 @@ bool from_bech32_charset(const tal_t *ctx, goto fail; *data = tal_arr(ctx, u8, 0); - bech32_pull_bits(data, u5data, tal_bytelen(u5data) * 5); + if (!bech32_pull_bits(data, u5data, tal_bytelen(u5data) * 5)) { + tal_free(*data); + goto fail; + } tal_free(u5data); return true; diff --git a/common/bech32_util.h b/common/bech32_util.h index f2857bde4935..fbd71d0470bf 100644 --- a/common/bech32_util.h +++ b/common/bech32_util.h @@ -12,8 +12,9 @@ void bech32_push_bits(u5 **data, const void *src, size_t nbits); /** * Push the bytes in src in 8 bit format onto the end of data. + * Returns false if padding bits are non-zero or exceed 4 bits. */ -void bech32_pull_bits(u8 **data, const u5 *src, size_t nbits); +bool bech32_pull_bits(u8 **data, const u5 *src, size_t nbits); /** * Checksumless bech32 routines. diff --git a/plugins/offers.c b/plugins/offers.c index 436a32f4ea7c..6c049334c7d9 100644 --- a/plugins/offers.c +++ b/plugins/offers.c @@ -538,7 +538,10 @@ static u8 *encrypted_decode(const tal_t *ctx, const char *str, char **fail) { goto fail; } u8 *data8bit = tal_arr(data, u8, 0); - bech32_pull_bits(&data8bit, data, datalen*5); + if (!bech32_pull_bits(&data8bit, data, datalen*5)) { + *fail = tal_fmt(ctx, "invalid bech32 padding"); + goto fail; + } return data8bit; fail: From a04c1a24e177054e0060c48ede120f079cebe3e0 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Thu, 30 Apr 2026 13:17:10 +0930 Subject: [PATCH 18/18] BOLT12: don't allow zero-amount offers. And enhance some of our quotes to use `...` at the start to link them. As they were, we didn't notice when a new requirement appeared in the middle. Signed-off-by: Rusty Russell --- Makefile | 2 +- common/bolt12.c | 15 +++++++++++++-- common/test/run-bolt12-encode-test.c | 12 ++++++++++++ common/test/run-bolt12-format-string-test.c | 1 + devtools/bolt12-cli.c | 5 +++++ 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index db14d23efc99..61c86087a645 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ CCANDIR := ccan # Where we keep the BOLT RFCs BOLTDIR := ../bolts/ -DEFAULT_BOLTVERSION := 7153bed9705d7493065d9b818d25b282ef0a7c5e +DEFAULT_BOLTVERSION := 311119388a46dfa859da3d2eda0ca836cfc5f078 # Can be overridden on cmdline. BOLTVERSION := $(DEFAULT_BOLTVERSION) diff --git a/common/bolt12.c b/common/bolt12.c index d4fcd0053437..1f2ccf1b659d 100644 --- a/common/bolt12.c +++ b/common/bolt12.c @@ -227,7 +227,17 @@ struct tlv_offer *offer_decode(const tal_t *ctx, } /* BOLT #12: - * + *... + * - if `offer_amount` is set and is not greater than zero: + * - MUST NOT respond to the offer. + */ + if (offer->offer_amount && *offer->offer_amount == 0) { + *fail = tal_strdup(ctx, "Offer contains a zero amount"); + return tal_free(offer); + } + + /* BOLT #12: + *... * - if `offer_currency` is set and `offer_amount` is not set: * - MUST NOT respond to the offer. */ @@ -237,7 +247,7 @@ struct tlv_offer *offer_decode(const tal_t *ctx, } /* BOLT #12: - * + *... * - if neither `offer_issuer_id` nor `offer_paths` are set: * - MUST NOT respond to the offer. */ @@ -247,6 +257,7 @@ struct tlv_offer *offer_decode(const tal_t *ctx, } /* BOLT #12: + *... * - if `num_hops` is 0 in any `blinded_path` in `offer_paths`: * - MUST NOT respond to the offer. */ diff --git a/common/test/run-bolt12-encode-test.c b/common/test/run-bolt12-encode-test.c index 43f3e5e0eb48..9391b1a38a08 100644 --- a/common/test/run-bolt12-encode-test.c +++ b/common/test/run-bolt12-encode-test.c @@ -429,6 +429,8 @@ 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: + * - 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: @@ -438,8 +440,18 @@ int main(int argc, char *argv[]) print_invalid_offer(offer, "Missing offer_description and offer_amount"); offer->offer_description = tal_utf8(tmpctx, "Test vectors"); + offer->offer_amount = tal(offer, u64); + *offer->offer_amount = 0; + print_invalid_offer(offer, "Zero offer_amount"); + offer->offer_amount = tal_free(offer->offer_amount); + offer->offer_currency = tal_utf8(offer, "USD"); print_invalid_offer(offer, "Missing offer_amount with offer_currency"); + + offer->offer_amount = tal(offer, u64); + *offer->offer_amount = 0; + print_invalid_offer(offer, "Zero offer_amount with currency"); + offer->offer_amount = tal_free(offer->offer_amount); offer->offer_currency = NULL; offer->offer_issuer_id = NULL; diff --git a/common/test/run-bolt12-format-string-test.c b/common/test/run-bolt12-format-string-test.c index d517a60cf11d..10f6748b2ddd 100644 --- a/common/test/run-bolt12-format-string-test.c +++ b/common/test/run-bolt12-format-string-test.c @@ -128,6 +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. * - 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). diff --git a/devtools/bolt12-cli.c b/devtools/bolt12-cli.c index a72d57a1d71d..3a81467af996 100644 --- a/devtools/bolt12-cli.c +++ b/devtools/bolt12-cli.c @@ -95,6 +95,7 @@ static bool print_offer_amount(const struct bitcoin_blkid *chains, /* BOLT #12: * - 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. * - 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). @@ -150,6 +151,10 @@ static bool print_offer_amount(const struct bitcoin_blkid *chains, currency); } + if (amount == 0) { + printf(" *** INVALID zero offer_amount"); + ok = false; + } return ok; }