global: more missing BOLT quotes.
What changed, and why it matters
This commit only adds documentation comments quoting the BOLT (Basis of Lightning Technology) specification and explaining how the existing code follows it. It does not change any program logic, function behavior, or security controls. A few comments note that the current implementation is incomplete ("FIXME"), but those are observations, not fixes. There is no evidence this commit addresses a security vulnerability or changes anything an attacker could exploit.
No security action required. Treat as a normal documentation/maintenance commit. If the FIXME comments about incomplete BOLT compliance are concerning, review those specific areas separately, but this commit itself does not alter risk.
Security signals we found
No functional code changes
Only BOLT specification quotes and explanatory comments added
FIXME comments note incomplete BOLT compliance but do not patch it
No bug fixes, input validation changes, or memory safety changes
Evidence from the diff
The diff is a documentation-only change across 21 files. It inserts BOLT specification quotations and implementation notes before existing code blocks. No executable logic, control flow, data structures, or APIs are modified. Two comments in onchain_control.c and one in offers.c explicitly mark behavior as not fully compliant with the quoted BOLT requirement, but no code is changed to improve compliance. The commit title and message indicate the sole purpose is to add missing BOLT quotes.
Changed components
channeld/channeld.cclosingd/closingd.ccommon/bech32_util.ccommon/bolt11.ccommon/channel_id.ccommon/close_tx.cgossipd/gossipd.clightningd/channel.clightningd/closing_control.clightningd/invoice.clightningd/lightningd.clightningd/onchain_control.clightningd/peer_control.clightningd/peer_htlcs.conchaind/onchaind.copeningd/openingd.cplugins/fetchinvoice.cplugins/offers.cplugins/offers_inv_hook.cwallet/walletrpc.cwire/tlvstream.cInspect captured patch +227 / −13
diff --git a/channeld/channeld.c b/channeld/channeld.c
index ad4b6734..6fa71580 100644
--- a/channeld/channeld.c
+++ b/channeld/channeld.c
@@ -658,9 +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,
diff --git a/closingd/closingd.c b/closingd/closingd.c
index e3c22ece..fdf2c263 100644
--- a/closingd/closingd.c
+++ b/closingd/closingd.c
@@ -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))
diff --git a/common/bech32_util.c b/common/bech32_util.c
index bae0f1e8..b72777e7 100644
--- a/common/bech32_util.c
+++ b/common/bech32_util.c
@@ -105,6 +105,17 @@ fail:
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)
{
diff --git a/common/bolt11.c b/common/bolt11.c
index d1599680..bd15e3af 100644
--- a/common/bolt11.c
+++ b/common/bolt11.c
@@ -1407,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))
diff --git a/common/channel_id.c b/common/channel_id.c
index 5caffe23..dea8ce72 100644
--- a/common/channel_id.c
+++ b/common/channel_id.c
@@ -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)
{
diff --git a/common/close_tx.c b/common/close_tx.c
index 8d6805b7..4bd985bd 100644
--- a/common/close_tx.c
+++ b/common/close_tx.c
@@ -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));
diff --git a/gossipd/gossipd.c b/gossipd/gossipd.c
index a2c6fdf2..b9f45424 100644
--- a/gossipd/gossipd.c
+++ b/gossipd/gossipd.c
@@ -495,6 +495,15 @@ static void inject_gossip(struct daemon *daemon, const u8 *msg)
/*~ This is where lightningd tells us that a channel's funding transaction has
* been spent. */
+/* BOLT #7:
+ * ## Pruning the Network View
+ *...
+ * ### Requirements
+ *
+ * A node:
+ * - SHOULD monitor the funding transactions in the blockchain, to identify
+ * channels that are being closed.
+ */
static void handle_outpoints_spent(struct daemon *daemon, const u8 *msg)
{
struct short_channel_id *scids;
diff --git a/lightningd/channel.c b/lightningd/channel.c
index 2254d051..62d6d995 100644
--- a/lightningd/channel.c
+++ b/lightningd/channel.c
@@ -1088,6 +1088,14 @@ static void channel_fail_perm(struct channel *channel,
why);
}
+ /* BOLT #5:
+ * A node:
+ * - if a *local commitment transaction* has NOT ever contained a `to_local`
+ * or HTLC output:
+ * - MAY simply forget the channel.
+ */
+ /* FIXME: We only implement a subset of this; we keep waiting
+ * as long as it was finished opening. */
if (channel_state_open_uncommitted(channel->state)) {
delete_channel(channel, false);
return;
diff --git a/lightningd/closing_control.c b/lightningd/closing_control.c
index b80821ff..9d1859c7 100644
--- a/lightningd/closing_control.c
+++ b/lightningd/closing_control.c
@@ -433,6 +433,9 @@ void peer_start_closingd(struct channel *channel, struct peer_fd *peer_fd)
}
/* BOLT #3:
+ * ## Legacy Closing Transaction
+ *...
+ * ### Requirements
*
* Each node offering a signature:
* - MUST round each output down to whole satoshis.
diff --git a/lightningd/invoice.c b/lightningd/invoice.c
index 28bc54ce..f214b407 100644
--- a/lightningd/invoice.c
+++ b/lightningd/invoice.c
@@ -1235,6 +1235,12 @@ static struct command_result *json_invoice(struct command *cmd,
info->b11->description_hash = NULL;
info->b11->payment_secret = tal_dup(info->b11, struct secret,
&payment_secret);
+ /* BOLT #11:
+ *
+ * A writer:
+ * - MUST set the `9` field to a feature vector compliant with the
+ * [BOLT 9 origin node requirements](09-features.md#requirements).
+ */
info->b11->features = tal_dup_talarr(info->b11, u8,
cmd->ld->our_features
->bits[BOLT11_FEATURE]);
diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c
index 7dba1153..3e21caa6 100644
--- a/lightningd/lightningd.c
+++ b/lightningd/lightningd.c
@@ -890,6 +890,21 @@ static struct feature_set *default_features(const tal_t *ctx)
* option_data_loss_protect (all but 11 nodes)
* option_static_remotekey (all but 16 nodes)
*/
+ /* BOLT #9:
+ * The origin node:
+ * * If it supports a feature above, SHOULD set the corresponding odd
+ * bit in all feature fields indicated by the Context column unless
+ * indicated that it must set the even feature bit instead.
+ * * If it requires a feature above, MUST set the corresponding even
+ * feature bit in all feature fields indicated by the Context column,
+ * unless indicated that it must set the odd feature bit instead.
+ * * MUST NOT set feature bits it does not support.
+ * * MUST NOT set feature bits in fields not specified by the table above.
+ * * MUST NOT set both the optional and mandatory bits.
+ * * MUST set all transitive feature dependencies.
+ * * MUST support:
+ * * `var_onion_optin`
+ */
struct feature_set *ret = NULL;
static const u32 features[] = {
COMPULSORY_FEATURE(OPT_DATA_LOSS_PROTECT),
diff --git a/lightningd/onchain_control.c b/lightningd/onchain_control.c
index b6dfc4dd..4457f82b 100644
--- a/lightningd/onchain_control.c
+++ b/lightningd/onchain_control.c
@@ -932,6 +932,15 @@ static struct bitcoin_tx *onchaind_tx_unsigned(const tal_t *ctx,
weight = bitcoin_tx_weight(tx) + 1 + 3 + 73 + 0 + tal_count(info->wscript);
weight += elements_tx_overhead(chainparams, 1, 1);
+ /* BOLT #5:
+ * A node which broadcasts an HTLC-success or HTLC-timeout transaction for a
+ * commitment transaction:
+ * - if `option_anchors` applies:
+ * - MUST combine it with inputs contributing sufficient fee to ensure timely
+ * inclusion in a block.
+ * - MAY combine it with other transactions.
+ */
+ /* FIXME: We don't combine! */
block_target = info->deadline_block;
for (;;) {
u32 feerate;
@@ -1305,6 +1314,21 @@ static void create_onchain_tx(struct channel *channel,
welements)));
}
+/* BOLT #5:
+ *
+ * - if `option_anchors` applies:
+ * - MAY use a single transaction to *resolve* all the outputs.
+ * - if confirmation doesn't happen before reaching `security_delay` blocks from
+ * expiry:
+ * - SHOULD *resolve* revoked outputs in their own, separate penalty transactions. A previous
+ * penalty transaction claiming multiple revoked outputs at once may be blocked from confirming
+ * because of a transaction pinning attack.
+ * - otherwise:
+ * - MAY use a single transaction to *resolve* all the outputs.
+ * - MUST handle its transactions being invalidated by HTLC transactions.
+ */
+/* FIXME: we always use one transaction per HTLC, even when we have
+ * plenty of time and could combine them. */
static void handle_onchaind_spend_to_us(struct channel *channel,
const u8 *msg)
{
diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c
index ddcdb27e..ed006518 100644
--- a/lightningd/peer_control.c
+++ b/lightningd/peer_control.c
@@ -228,6 +228,14 @@ u8 *p2tr_for_keyidx(const tal_t *ctx, struct lightningd *ld, u64 keyidx)
return scriptpubkey_p2tr(ctx, &shutdownkey);
}
+/* BOLT #2:
+ * A node:
+ * - MUST NOT broadcast old (revoked) commitment transactions,
+ * - Note: doing so will allow the other node to seize all channel funds.
+ * - SHOULD NOT sign commitment transactions, unless it's about to broadcast
+ * them (due to a failed connection),
+ * - Note: this is to reduce the above risk.
+ */
static struct bitcoin_tx *sign_last_tx(const tal_t *ctx,
const struct channel *channel,
const struct bitcoin_tx *last_tx,
diff --git a/lightningd/peer_htlcs.c b/lightningd/peer_htlcs.c
index a9972627..53b01ede 100644
--- a/lightningd/peer_htlcs.c
+++ b/lightningd/peer_htlcs.c
@@ -2625,6 +2625,12 @@ void peer_got_revoke(struct channel *channel, const u8 *msg)
badonions = tal_arrz(msg, enum onion_wire, tal_count(changed));
failmsgs = tal_arrz(msg, u8 *, tal_count(changed));
for (i = 0; i < tal_count(changed); i++) {
+ /* BOLT #2:
+ * A node:
+ * - until an incoming HTLC has been irrevocably committed:
+ * - MUST NOT offer the corresponding outgoing HTLC
+ * (`update_add_htlc`) in response to that incoming HTLC.
+ */
/* If we're doing final accept, we need to forward */
if (changed[i].newstate == RCVD_ADD_ACK_REVOCATION) {
peer_accepted_htlc(failmsgs,
diff --git a/onchaind/onchaind.c b/onchaind/onchaind.c
index e326afca..f7e3dacb 100644
--- a/onchaind/onchaind.c
+++ b/onchaind/onchaind.c
@@ -961,6 +961,10 @@ static void billboard_update(struct tracked_output **outs)
output_type_name(best->output_type), best->depth);
}
+/* BOLT #5:
+ * - SHOULD extract the payment preimage from the transaction input witness, if
+ * it's not already known.
+ */
static void handle_htlc_onchain_fulfill(struct tracked_output *out,
const struct tx_parts *tx_parts,
const struct bitcoin_outpoint *htlc_outpoint)
@@ -1376,7 +1380,14 @@ static void tx_new_depth(struct tracked_output **outs,
{
size_t i;
- /* Special handling for commitment tx reaching depth */
+ /* BOLT #5:
+ * - for any committed HTLC that does NOT have an output in this
+ * commitment transaction:
+ *...
+ * - otherwise:
+ * - once the commitment transaction has reached reasonable depth:
+ * - MUST fail the corresponding incoming HTLC (if any).
+ */
if (bitcoin_txid_eq(&outs[0]->resolved->txid, txid)
&& depth >= reasonable_depth
&& missing_htlc_msgs) {
@@ -2036,6 +2047,11 @@ static enum side matches_direction(const size_t *matches,
static void note_missing_htlcs(u8 **htlc_scripts,
const struct htlcs_info *htlcs_info)
{
+ /* BOLT #5:
+ * - for any committed HTLC that does NOT have an output in this
+ * commitment transaction:
+ */
+ /* See onchain_control.c for the rest of the quote! */
for (size_t i = 0; i < tal_count(htlcs_info->htlcs); i++) {
u8 *msg;
@@ -2161,9 +2177,18 @@ static void handle_our_unilateral(const struct tx_parts *tx,
/* BOLT #5:
*
- * In this case, a node discovers its *local commitment transaction*,
- * which *resolves* the funding transaction output.
+ * A node:
+ * - upon discovering its *local commitment transaction*:
+ * - SHOULD spend the `to_local` output to a convenient address.
+ * - MUST wait until the `OP_CHECKSEQUENCEVERIFY` delay has passed (as
+ * specified by the remote node's `to_self_delay` field) before spending the
+ * output.
+ * - Note: if the output is spent (as recommended), the output is *resolved*
+ * by the spending transaction, otherwise it is considered *resolved* by the
+ * commitment transaction itself.
*/
+ /* We do NOT spend our unilateral, we remember how to spend it
+ * in the wallet, thus it's immediately resolved. */
resolved_by_other(outs[0], &tx->txid, OUR_UNILATERAL);
/* Figure out what delayed to-us output looks like */
@@ -2267,7 +2292,7 @@ static void handle_our_unilateral(const struct tx_parts *tx,
&& wally_tx_output_scripteq(tx->outputs[i],
script[REMOTE])) {
/* BOLT #5:
- *
+ *...
* - MAY ignore the `to_remote` output.
* - Note: No action is required by the local
* node, as `to_remote` is considered *resolved*
@@ -2409,7 +2434,7 @@ static void handle_our_unilateral(const struct tx_parts *tx,
*
* - MUST handle HTLCs offered by itself as specified
* in [HTLC Output Handling: Local Commitment,
- * Local Offers]
+ * Local Offers](#htlc-output-handling-local-commitment-local-offers).
*/
out = new_tracked_output(&outs, &outpoint,
tx_blockheight,
@@ -2431,10 +2456,9 @@ static void handle_our_unilateral(const struct tx_parts *tx,
NULL, NULL,
remote_htlc_sigs);
/* BOLT #5:
- *
- * - MUST handle HTLCs offered by the remote node
- * as specified in [HTLC Output Handling: Local
- * Commitment, Remote Offers]
+ * - MUST handle HTLCs offered by the remote node
+ * as specified in [HTLC Output Handling: Local
+ * Commitment, Remote Offers](#htlc-output-handling-local-commitment-remote-offers).
*/
/* Tells us which htlc to use */
which_htlc = resolve_their_htlc(out, matches,
diff --git a/openingd/openingd.c b/openingd/openingd.c
index cf63b3c2..cc51e1e6 100644
--- a/openingd/openingd.c
+++ b/openingd/openingd.c
@@ -410,15 +410,20 @@ static u8 *funder_channel_start(struct state *state, u8 channel_flags,
their_mindepth);
/* BOLT #2:
- * - if `channel_type` is set, and `channel_type` was set in
- * `open_channel`, and they are not equal types:
- * - MUST fail the channel.
+ * - if `option_channel_type` was negotiated but the message doesn't
+ * include a `channel_type`:
+ * - MAY fail the channel.
*/
if (!accept_tlvs->channel_type) {
negotiation_failed(state,
"accept_channel without a channel_type");
}
+ /* BOLT #2:
+ * - if `channel_type` is set, and `channel_type` was set in
+ * `open_channel`, and they are not equal types:
+ * - MUST fail the channel.
+ */
/* Simple case: caller specified, don't allow any variants */
if (!featurebits_eq(accept_tlvs->channel_type, state->channel_type->features)) {
negotiation_failed(state,
diff --git a/plugins/fetchinvoice.c b/plugins/fetchinvoice.c
index 848f3504..c8b003dd 100644
--- a/plugins/fetchinvoice.c
+++ b/plugins/fetchinvoice.c
@@ -368,6 +368,16 @@ struct command_result *handle_invoice_onion_message(struct command *cmd,
struct sent *sent;
struct command_result *err;
+ /* BOLT #4:
+ * - otherwise (it is the final node):
+ * - if `path_id` is set and corresponds to a path the reader has previously published in a `reply_path`:
+ * - if the onion message is not a reply to that previous onion:
+ * - MUST ignore the onion message
+ * - otherwise (unknown or unset `path_id`):
+ * - if the onion message is a reply to an onion message which contained a `path_id`:
+ * - MUST respond (or not respond) exactly as if it did not send the initial onion message.
+ */
+ /* We unmarshal the path_id into our secret: if it is NULL or wrong, we exit here */
sent = find_sent_by_secret(pathsecret);
if (!sent)
return NULL;
@@ -771,6 +781,14 @@ static struct command_result *invreq_done(struct command *cmd,
payload->invoice_request = tal_arr(payload, u8, 0);
towire_tlv_invoice_request(&payload->invoice_request, sent->invreq);
+ /* BOLT #12:
+ * - if it chooses to send an invoice request, it sends an onion message:
+ * - if `offer_paths` is set:
+ * - MUST send the onion message via any path in `offer_paths` to the final
+ * `onion_msg_hop`.`blinded_node_id` in that path
+ * - otherwise:
+ * - MUST send the onion message to `offer_issuer_id`
+ */
/* Don't expect a reply message for cancel */
return send_message(cmd, sent,
sent->invreq->invreq_recurrence_cancel ? false : true,
diff --git a/plugins/offers.c b/plugins/offers.c
index b2e8b722..436a32f4 100644
--- a/plugins/offers.c
+++ b/plugins/offers.c
@@ -288,6 +288,19 @@ static struct command_result *onion_message_recv(struct command *cmd,
return res;
}
+ /* BOLT #4:
+ * - otherwise (it is the final node):
+ * - if `path_id` is set and corresponds to a path the reader has previously published in a `reply_path`:
+ * - if the onion message is not a reply to that previous onion:
+ * - MUST ignore the onion message
+ * - otherwise (unknown or unset `path_id`):
+ * - if the onion message is a reply to an onion message which contained a `path_id`:
+ * - MUST respond (or not respond) exactly as if it did not send the initial onion message.
+ */
+ /* FIXME: Technically, we don't meet the first half of this: we
+ * always do parsing and sanity checking before checking the
+ * secret (thus the path_id). But we ignore it *after* that.
+ */
replytok = json_get_member(buf, om, "reply_blindedpath");
if (replytok) {
reply_path = json_to_blinded_path(cmd, buf, replytok);
diff --git a/plugins/offers_inv_hook.c b/plugins/offers_inv_hook.c
index 9de434c8..ff2014e3 100644
--- a/plugins/offers_inv_hook.c
+++ b/plugins/offers_inv_hook.c
@@ -257,6 +257,10 @@ struct command_result *handle_invoice(struct command *cmd,
fmt_sha256(tmpctx, &inv->invreq_id));
}
} else {
+ /* BOLT #12:
+ * - if `invreq_paths` is present:
+ * - MUST reject the invoice if it did not arrive via one of those paths.
+ */
/* Didn't use path. Was it supposed to? */
if (inv->inv->invreq_paths) {
if (command_dev_apis(cmd))
diff --git a/wallet/walletrpc.c b/wallet/walletrpc.c
index 700367cc..045ce715 100644
--- a/wallet/walletrpc.c
+++ b/wallet/walletrpc.c
@@ -459,7 +459,12 @@ static struct command_result *json_listfunds(struct command *cmd,
p = peer_node_id_map_next(cmd->ld->peers, &it)) {
struct channel *c;
list_for_each(&p->channels, c, list) {
- /* We don't print out uncommitted channels */
+ /* We don't print out uncommitted channels, which makes us meet this: */
+ /* BOLT #2:
+ * The receiving node MUST NOT:
+ * - consider funds received, using `push_msat`, to be received
+ * until the funding transaction has reached sufficient depth.
+ */
if (channel_state_uncommitted(c->state))
continue;
json_object_start(response, NULL);
diff --git a/wire/tlvstream.c b/wire/tlvstream.c
index 8afb7f2b..8f276a72 100644
--- a/wire/tlvstream.c
+++ b/wire/tlvstream.c
@@ -203,6 +203,13 @@ bool fromwire_tlv(const u8 **cursor, size_t *max,
}
}
+ /* BOLT #1:
+ * - otherwise, if `type` is unknown:
+ * - if `type` is even:
+ * - MUST fail to parse the `tlv_stream`.
+ * - otherwise, if `type` is odd:
+ * - MUST discard the next `length` bytes.
+ */
if (!field.meta && !tlv_type_is_allowed(&field, extra_types)) {
SUPERVERBOSE("unknown even");
if (err_type != NULL)
Why this scored 20/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.