Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
86c6421
BOLT quote corrections in Python files (previously unchecked).
rustyrussell Apr 10, 2026
8904206
Fix C comments which will accidentally trigger Python BOLT quote chec…
rustyrussell Apr 10, 2026
066d893
tests: update test_forward_different_fees_and_cltv to use default min…
rustyrussell Apr 10, 2026
d2a903e
Update outdated BOLT quotes in channel open/close handling.
rustyrussell Apr 10, 2026
1ec8ce5
Update outdated BOLT quotes in HTLC/closing handling.
rustyrussell Apr 10, 2026
a3d0cd3
Update outdated BOLT #11 quotes in invoice handling.
rustyrussell Apr 10, 2026
57457fd
devtools: import check_quotes.py from lnprototest.
rustyrussell Apr 10, 2026
83e0754
devtools/check_quotes.py: add -k option, make `...` more nuanced.
rustyrussell Apr 10, 2026
498e583
BOLT quotes: split cross-section quotes; prepare for new `...` semant…
rustyrussell Apr 10, 2026
ce675c3
check-source-bolt: replace C check-bolt tool with Python check_quotes…
rustyrussell Apr 10, 2026
276352f
check_quotes.py: add --coverage tracking; add devtools/bolt-coverage.py.
rustyrussell Apr 10, 2026
52d1b81
Makefile: add check-requirements-coverage
rustyrussell Apr 10, 2026
2f3d855
global: add the "easy" missing quotes.
rustyrussell Apr 10, 2026
fa52a44
common/sphinx: don't create unused keys, fill in BOLT quotes.
rustyrussell Apr 10, 2026
b97ab85
common: follow BOLT 4 requirements to make decryption constant time.
rustyrussell Apr 10, 2026
f7b3201
lightningd: make sure we don't send channel_updates for unasked for c…
rustyrussell Apr 10, 2026
10a62b2
global: more missing BOLT quotes.
rustyrussell Apr 10, 2026
34586a0
common: correctly refuse to accept wireaddr with port == 0.
rustyrussell Apr 10, 2026
ce5845e
gossipd: correclty ignore channel_announcement and channel_update for…
rustyrussell Apr 10, 2026
fce19f8
channeld: update dust limit checks to the final version of PR 919.
rustyrussell Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -560,32 +560,51 @@ print-hdr-to-check:
# If you want to check a specific variant of quotes use:
# make check-source-bolt BOLTVERSION=xxx
ifeq ($(BOLTVERSION),$(DEFAULT_BOLTVERSION))
CHECK_BOLT_PREFIX=
CHECK_BOLT_COMMIT=
else
CHECK_BOLT_PREFIX=--prefix="BOLT-$(BOLTVERSION)"
CHECK_BOLT_COMMIT=--include-commit=$(BOLTVERSION)
endif

CHECK_QUOTES := devtools/check_quotes.py

BOLT_COVERAGE_FLAGS=$(if $(COVERAGE_FILE),--coverage=$(COVERAGE_FILE),)

# Any mention of BOLT# must be followed by an exact quote, modulo whitespace.
bolt-check/%: % bolt-precheck devtools/check-bolt
@if [ -d .tmp.lightningrfc ]; then devtools/check-bolt $(CHECK_BOLT_PREFIX) .tmp.lightningrfc $<; else echo "Not checking BOLTs: BOLTDIR $(BOLTDIR) does not exist" >&2; fi
bolt-check/%: % bolt-precheck
@if [ -d .tmp.lightningrfc ]; then uv run $(CHECK_QUOTES) -k $(CHECK_BOLT_COMMIT) --comment-start "/* " --comment-continue "*" --comment-end "*/" --boltdir .tmp.lightningrfc $(BOLT_COVERAGE_FLAGS) $<; else echo "Not checking BOLTs: BOLTDIR $(BOLTDIR) does not exist" >&2; fi

bolt-check-py/%: % bolt-precheck
@if [ -d .tmp.lightningrfc ]; then uv run $(CHECK_QUOTES) -k $(CHECK_BOLT_COMMIT) --boltdir .tmp.lightningrfc $(BOLT_COVERAGE_FLAGS) $<; else echo "Not checking BOLTs: BOLTDIR $(BOLTDIR) does not exist" >&2; fi

bolt-check-rs/%: % bolt-precheck
@if [ -d .tmp.lightningrfc ]; then uv run $(CHECK_QUOTES) -k $(CHECK_BOLT_COMMIT) --comment-start "// " --comment-continue "//" --boltdir .tmp.lightningrfc $(BOLT_COVERAGE_FLAGS) $<; else echo "Not checking BOLTs: BOLTDIR $(BOLTDIR) does not exist" >&2; fi

LOCAL_BOLTDIR=.tmp.lightningrfc

bolt-precheck:
@[ -d $(BOLTDIR) ] || exit 0; set -e; if [ -z "$(BOLTVERSION)" ]; then rm -rf $(LOCAL_BOLTDIR); ln -sf $(BOLTDIR) $(LOCAL_BOLTDIR); exit 0; fi; [ "$$(git -C $(LOCAL_BOLTDIR) rev-list --max-count=1 HEAD 2>/dev/null)" != "$(BOLTVERSION)" ] || exit 0; rm -rf $(LOCAL_BOLTDIR) && git clone -q $(BOLTDIR) $(LOCAL_BOLTDIR) && cd $(LOCAL_BOLTDIR) && git checkout -q $(BOLTVERSION)

check-source-bolt: $(ALL_NONGEN_SRCFILES:%=bolt-check/%)
PYSRC=$(shell git ls-files "*.py" | grep -v /text.py)
RUSTSRC=$(shell git ls-files "*.rs")

check-source-bolt: $(ALL_NONGEN_SRCFILES:%=bolt-check/%) $(PYSRC:%=bolt-check-py/%) $(RUSTSRC:%=bolt-check-rs/%)

check-requirements-coverage: bolt-precheck
@if [ ! -d .tmp.lightningrfc ]; then echo "Not checking BOLTs: BOLTDIR $(BOLTDIR) does not exist" >&2; exit 1; fi
@f=/tmp/cln-bolt-coverage.$$$$; \
rm -f $$f; \
$(MAKE) check-source-bolt COVERAGE_FILE=$$f && \
uv run devtools/bolt-coverage.py --coverage $$f --boltdir .tmp.lightningrfc; \
rc=$$?; rm -f $$f; exit $$rc

check-whitespace/%: %
@if grep -Hn '[ ]$$' $<; then echo Extraneous whitespace found >&2; exit 1; fi

check-whitespace: check-whitespace/Makefile check-whitespace/devtools/check-bolt.c $(ALL_NONGEN_SRCFILES:%=check-whitespace/%)
check-whitespace: check-whitespace/Makefile $(ALL_NONGEN_SRCFILES:%=check-whitespace/%)

check-spelling:
@tools/check-spelling.sh

PYSRC=$(shell git ls-files "*.py" | grep -v /text.py)

# Some tests in pyln will need to find lightningd to run, so have a PATH that
# allows it to find that
PYLN_PATH=$(shell pwd)/lightningd:$(PATH)
Expand Down
6 changes: 3 additions & 3 deletions bitcoin/script.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,13 @@ u8 **bitcoin_witness_sig_and_element(const tal_t *ctx,
const void *elem, size_t elemsize,
const u8 *witnessscript);

/* BOLT #3 to-local output */
/* to-local output (BOLT #3) */
u8 *bitcoin_wscript_to_local(const tal_t *ctx, u16 to_self_delay,
u32 lease_remaining,
const struct pubkey *revocation_pubkey,
const struct pubkey *local_delayedkey);

/* BOLT #3 offered/accepted HTLC outputs */
/* offered/accepted HTLC outputs (BOLT #3) */
u8 *bitcoin_wscript_htlc_offer(const tal_t *ctx,
const struct pubkey *localhtlckey,
const struct pubkey *remotehtlckey,
Expand Down Expand Up @@ -151,7 +151,7 @@ u8 *bitcoin_wscript_htlc_receive_ripemd(const tal_t *ctx,
bool option_anchor_outputs,
bool option_anchors_zero_fee_htlc_tx);

/* BOLT #3 HTLC-success/HTLC-timeout output */
/* HTLC-success/HTLC-timeout output (BOLT #3) */
u8 *bitcoin_wscript_htlc_tx(const tal_t *ctx,
u16 to_self_delay,
const struct pubkey *revocation_pubkey,
Expand Down
4 changes: 3 additions & 1 deletion bitcoin/short_channel_id.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ static inline size_t hash_scid(struct short_channel_id scid)
* - MUST set `node_id_1` and `node_id_2` to the public keys of the two nodes
* operating the channel, such that `node_id_1` is the lexicographically-lesser of the
* two compressed keys sorted in ascending lexicographic order.
*...
*/
/* BOLT #7:
*
* - if the origin node is `node_id_1` in the message:
* - MUST set the `direction` bit of `channel_flags` to 0.
* - otherwise:
Expand Down
15 changes: 15 additions & 0 deletions channeld/channeld.c
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,21 @@ static void handle_peer_add_htlc(struct peer *peer, const u8 *msg)
"Bad peer_add_htlc %s", tal_hex(msg, msg));
}

/* BOLT #2:
* - if the sender did not previously acknowledge the commitment of that HTLC:
* - MUST ignore a repeated `id` value after a reconnection.
*/
/* We do this is a subtle way: we only save the HTLC to the db once we
* have received commitment_signed, which is when we send the
* acknowledgement. So if we disconnect, we restart channeld which
* doesn't know about the HTLC, thus rexmits are fine.
*
* If we got the commitment_signed, we increment the reestablish fields
* so they know not to send it again. */
/* BOLT #2:
* - MUST allow multiple HTLCs with the same `payment_hash`.
*/
/* We do: the key is the id, not the payment_hash */
add_err = channel_add_htlc(peer->channel, REMOTE, id, amount,
cltv_expiry, &payment_hash,
onion_routing_packet,
Expand Down
32 changes: 23 additions & 9 deletions channeld/full_channel.c
Original file line number Diff line number Diff line change
Expand Up @@ -1434,16 +1434,30 @@ bool channel_update_feerate(struct channel *channel, u32 feerate_per_kw)
if (!can_opener_afford_feerate(channel, feerate_per_kw))
return false;

/* BOLT-919 #2:
* - if the `dust_balance_on_holder_tx` at the
* new `dust_buffer_feerate` is superior to
* the `max_dust_htlc_exposure_msat`:
* ...
* - MAY fail the channel
/* BOLT #2:
* - if `option_anchors` was not negotiated:
* - if the `update_fee` increases `feerate_per_kw`:
* - if the dust balance of the remote transaction at the
* updated `feerate_per_kw` is greater then `max_dust_htlc_exposure_msat`:
* - MAY fail the channel
* - if the dust balance of the local transaction at the
* updated `feerate_per_kw` is greater than `max_dust_htlc_exposure_msat`:
* - MAY fail the channel
*/
if (!htlc_dust_ok(channel, feerate_per_kw, REMOTE) ||
!htlc_dust_ok(channel, feerate_per_kw, LOCAL))
return false;
if (!channel_has(channel, OPT_ANCHORS_ZERO_FEE_HTLC_TX)) {
if (feerate_per_kw > channel_feerate(channel, REMOTE)) {
if (!htlc_dust_ok(channel, feerate_per_kw, REMOTE)) {
status_unusual("Feerate %u is too dusty for remote",
feerate_per_kw);
return false;
}
}
if (!htlc_dust_ok(channel, feerate_per_kw, LOCAL)) {
status_unusual("Feerate %u is too dusty for local",
feerate_per_kw);
return false;
}
}

status_debug("Setting %s feerate to %u",
side_to_str(!channel->opener), feerate_per_kw);
Expand Down
3 changes: 3 additions & 0 deletions channeld/test/run-commit_tx.c
Original file line number Diff line number Diff line change
Expand Up @@ -584,12 +584,15 @@ int main(int argc, const char *argv[])
* INTERNAL: local_delayed_payment_basepoint_secret: 333333333333333333333333333333333333333333333333333333333333333301
* INTERNAL: remote_payment_basepoint_secret: 444444444444444444444444444444444444444444444444444444444444444401
* x_local_per_commitment_secret: 1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a0908070605040302010001
*...
* # From remote_revocation_basepoint_secret
* INTERNAL: remote_revocation_basepoint: 02466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27
*...
* # From local_delayed_payment_basepoint_secret
* INTERNAL: local_delayed_payment_basepoint: 023c72addb4fdf09af94f0c94d7fe92a386a7e70cf8a1d85916386bb2535c7b1b1
* INTERNAL: local_per_commitment_point: 025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486
* INTERNAL: remote_privkey: 8deba327a7cc6d638ab0eb025770400a6184afcba6713c210d8d10e199ff2fda01
*...
* # From local_delayed_payment_basepoint_secret, local_per_commitment_point and local_delayed_payment_basepoint
* INTERNAL: local_delayed_privkey: adf3464ce9c2f230fd2582fda4c6965e4993ca5524e8c9580e3df0cf226981ad01
* -->
Expand Down
1 change: 1 addition & 0 deletions channeld/test/run-full_channel.c
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ int main(int argc, const char *argv[])
*
* # From remote_revocation_basepoint_secret
* INTERNAL: remote_revocation_basepoint: 02466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27
*...
* # From local_delayed_payment_basepoint_secret
* INTERNAL: local_delayed_payment_basepoint: 023c72addb4fdf09af94f0c94d7fe92a386a7e70cf8a1d85916386bb2535c7b1b1
*/
Expand Down
27 changes: 18 additions & 9 deletions closingd/closingd.c
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ static struct bitcoin_tx *close_tx(const tal_t *ctx,
struct bitcoin_tx *tx;
struct amount_sat out_minus_fee[NUM_SIDES];

/* BOLT #3:
* ## Legacy Closing Transaction
*...
* ### Requirements
*
* Each node offering a signature:
*...
* - MUST subtract the fee given by `fee_satoshis` from the output to the funder.
*/
out_minus_fee[LOCAL] = out[LOCAL];
out_minus_fee[REMOTE] = out[REMOTE];
if (!amount_sat_sub(&out_minus_fee[opener], out[opener], fee))
Expand Down Expand Up @@ -159,10 +168,9 @@ static void send_offer(struct per_peer_state *pps,

/* BOLT #3:
*
* ## Legacy Closing Transaction
*...
* Each node offering a signature... MAY eliminate its
* own output.
* Each node offering a signature:
* ...
* - MAY eliminate its own output.
*/
/* (We don't do this). */
wire_sync_write(HSM_FD,
Expand Down Expand Up @@ -591,11 +599,12 @@ static void calc_fee_bounds(size_t expected_weight,

/* BOLT #2:
* - if it is not the funder:
* - SHOULD set `max_fee_satoshis` to at least the `max_fee_satoshis`
* received
*...
* Note that the non-funder is not paying the fee, so there is
* no reason for it to have a maximum feerate.
* - SHOULD set `max_fee_satoshis` to at least the `max_fee_satoshis` received
*/
/* BOLT #2:
*
* Note that the non-funder is not paying the fee, so there is no reason for it
* to have a maximum feerate.
*/
if (opener == REMOTE) {
*maxfee = funding;
Expand Down
11 changes: 11 additions & 0 deletions common/bech32_util.c
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ bool from_bech32_charset(const tal_t *ctx,
return false;
}

/* BOLT #12:
* # Encoding
*...
* ## Requirements
* Writers of a bolt12 string:
* - MUST either use all lowercase or all UPPERCASE.
* - SHOULD use uppercase for QR codes.
* - SHOULD use lower case otherwise.
* - MAY use `+`, optionally followed by whitespace, to separate large bolt12 strings.
*/
/* We use lower case, and we leave it to the caller to upcase (and +-break) if it wants */
char *to_bech32_charset(const tal_t *ctx,
const char *hrp, const u8 *data)
{
Expand Down
2 changes: 2 additions & 0 deletions common/blindedpath.c
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ bool unblind_onion(const struct pubkey *path_key,
* - Tweak `public_key` by multiplying by $`HMAC256(\text{"blinded\_node\_id"}, blinding\_ss)`$.
* - or (equivalently):
* - Tweak its own `node_privkey` below by multiplying by $`HMAC256(\text{"blinded\_node\_id"}, blinding\_ss)`$.
* - Derive the shared secret `ss` as ECDH(`public_key`, `node_privkey`)
* (see [Shared Secret](#shared-secret)).
*/
ecdh(path_key, ss);
subkey_from_hmac("blinded_node_id", ss, &hmac);
Expand Down
20 changes: 16 additions & 4 deletions common/bolt11.c
Original file line number Diff line number Diff line change
Expand Up @@ -1028,10 +1028,15 @@ struct bolt11 *bolt11_decode(const tal_t *ctx, const char *str,

/* BOLT #11:
*
* A reader... MUST check that the `signature` is valid (see
* the `n` tagged field specified below). ... A reader...
* MUST use the `n` field to validate the signature instead of
* performing signature recovery.
* A reader:
* - MUST check that the `signature` is valid (see the `n` tagged field specified below).
*/
/* 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.
*/
if (!have_n) {
struct pubkey k;
Expand Down Expand Up @@ -1402,6 +1407,13 @@ char *bolt11_encode_(const tal_t *ctx,

bech32_push_bits(&data, sig_and_recid, sizeof(sig_and_recid) * CHAR_BIT);

/* BOLT #11:
* A writer:
* - MUST encode the payment request in Bech32 (see BIP-0173)
* - SHOULD use upper case for QR codes (see BIP-0173)
* - MAY exceed the 90-character limit specified in BIP-0173.
*/
/* We let the user upcase if they want */
output = tal_arr(ctx, char, strlen(hrp) + tal_count(data) + 8);
if (!bech32_encode(output, hrp, data, tal_count(data), (size_t)-1,
BECH32_ENCODING_BECH32))
Expand Down
4 changes: 3 additions & 1 deletion common/channel_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
* * [`u32`:`feerate_per_kw`]
* * [`u16`:`to_self_delay`]
* * [`u16`:`max_accepted_htlcs`]
*...
*/
/* BOLT #2:
*
* 1. type: 33 (`accept_channel`)
* 2. data:
* * [`32*byte`:`temporary_channel_id`]
Expand Down
9 changes: 9 additions & 0 deletions common/channel_id.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@
#include <common/utils.h>
#include <wire/wire.h>

/* BOLT #2:
* ### The `funding_signed` Message
*...
* #### Requirements
*...
* The sender MUST set:
* - `channel_id` by exclusive-OR of the `funding_txid` and the
* `funding_output_index` from the `funding_created` message.
*/
void derive_channel_id(struct channel_id *channel_id,
const struct bitcoin_outpoint *outpoint)
{
Expand Down
11 changes: 11 additions & 0 deletions common/close_tx.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ struct bitcoin_tx *create_close_tx(const tal_t *ctx,
struct amount_sat total_out;
u8 *script;

/* BOLT #3:
* ## Legacy Closing Transaction
*...
* ### Requirements
*
* Each node offering a signature:
* - MUST round each output down to whole satoshis.
* - MUST subtract the fee given by `fee_satoshis` from the output to the funder.
* - MUST remove any output below its own `dust_limit_satoshis`.
* - MAY eliminate its own output.
*/
assert(amount_sat_add(&total_out, to_us, to_them));
assert(amount_sat_less_eq(total_out, funding_sats));

Expand Down
6 changes: 3 additions & 3 deletions common/cryptomsg.c
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ static void le64_nonce(unsigned char *npub, u64 nonce)
{
/* BOLT #8:
*
* ...with nonce `n` encoded as 32 zero bits, followed by a
* *little-endian* 64-bit value. Note: this follows the Noise Protocol
* convention, rather than our normal endian
* with nonce `n` encoded as 32 zero bits, followed by a
* *little-endian* 64-bit value. Note: this follows the Noise
* Protocol convention, rather than our normal endian.
*/
le64 le_nonce = cpu_to_le64(nonce);
const size_t zerolen = crypto_aead_chacha20poly1305_ietf_NPUBBYTES - sizeof(le_nonce);
Expand Down
3 changes: 3 additions & 0 deletions common/gossip_constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ enum query_option_flags {
* either direction is older than two weeks (1209600 seconds):
* - MAY prune the channel.
* - MAY ignore the channel.
* - Note: this is an individual node policy and MUST NOT be enforced by
* forwarding peers, e.g. by closing channels when receiving outdated
* gossip messages.
*/
#define GOSSIP_PRUNE_INTERVAL(dev_fast_gossip_prune_flag) \
DEV_FAST_GOSSIP(dev_fast_gossip_prune_flag, 120, 1209600)
Expand Down
1 change: 1 addition & 0 deletions common/htlc_trim.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ bool htlc_is_trimmed(enum side htlc_owner,
* - MUST NOT contain that output.
* - otherwise:
* - MUST be generated as specified in
* [Received HTLC Outputs](#received-htlc-outputs).
*/
else
htlc_fee = htlc_success_fee(feerate_per_kw,
Expand Down
4 changes: 3 additions & 1 deletion common/key_derive.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
* - The `remote_htlcpubkey` uses the remote node's `htlc_basepoint`;
* - The `local_delayedpubkey` uses the local node's `delayed_payment_basepoint`;
* - The `remote_delayedpubkey` uses the remote node's `delayed_payment_basepoint`.
*...
*/
/* BOLT #3:
*
* The `remotepubkey` is simply the remote node's `payment_basepoint`.
*/

Expand Down
6 changes: 6 additions & 0 deletions common/onion_message_parse.c
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ const char *onion_message_parse(const tal_t *ctx,
"onion_message_parse: invalid encrypted_recipient_data %s",
tal_hex(tmpctx, om->encrypted_recipient_data));
}
/* BOLT #4:
* - if it forwards the message:
* - MUST set `path_key` in the forwarded `onion_message`
* to the next `path_key` as calculated in
* [Route Blinding](#route-blinding).
*/
*next_onion_msg = towire_onion_message(ctx,
&next_path_key,
serialize_onionpacket(tmpctx, rs->next));
Expand Down
Loading