Fix tests for bolt12-decode. new cases added by autogenerated mocks after updating the default BOLT spec version.
What changed, and why it matters
This commit fixes two validation checks in Core Lightning's code for handling BOLT12 offers and Bech32-encoded data. One fix prevents an offer from claiming it supports zero blockchains, which could confuse or misroute payments. The other fix rejects malformed Bech32 strings where leftover padding bits are invalid. These are correctness and protocol-conformance fixes rather than obvious critical vulnerabilities, but they close gaps that could be abused to bypass validation or cause undefined behavior.
Treat as a routine correctness and spec-conformance patch. Reviewers should verify the new validation rules match the relevant BOLT12 and Bech32 specifications, and consider whether any callers relied on the previous permissive behavior. No emergency response is indicated based on the supplied materials.
Security signals we found
Input validation hardening for BOLT12 offer parsing
Bech32 padding-bit validation added to charset decoder
Rejection of zero-length `offer_chains` TLV
Commit message describes changes as test/spec conformance fixes, not security fixes
Evidence from the diff
The patch adds two validation rules: (1) in common/bolt12.c, it rejects offer_chains TLV entries with zero length, ensuring at least one chain is listed when the field is present; and (2) in common/bech32_util.c, it enforces that Bech32 data padding bits are zero and fewer than 5 bits, preventing invalid padding states during 5-bit-to-byte conversion. The commit message frames these as test fixes driven by updated BOLT spec test vectors, not as a security advisory.
Changed components
common/bech32_util.ccommon/bolt12.cBOLT12 offer decode pathBech32 string decoding utilitiesInspect captured patch +34 / −2
diff --git a/common/bech32_util.c b/common/bech32_util.c
index 3722a19c..fd952b6a 100644
--- a/common/bech32_util.c
+++ b/common/bech32_util.c
@@ -79,7 +79,7 @@ bool from_bech32_charset(const tal_t *ctx,
u5 *u5data;
const char *sep;
bool upper = false, lower = false;
- size_t datalen;
+ size_t datalen, nbits, trailing;
sep = memchr(bech32, '1', bech32_len);
if (!sep)
@@ -105,6 +105,18 @@ bool from_bech32_charset(const tal_t *ctx,
if (upper && lower)
goto fail;
+ /* Padding: converting N 5-bit groups to bytes leaves (N*5 % 8) trailing
+ * bits. These must be zero and fewer than 5 (otherwise a full 5-bit
+ * group is wasted as padding, which is invalid). */
+ nbits = datalen * 5;
+ trailing = nbits % 8;
+ if (trailing >= 5)
+ goto fail;
+ for (size_t i = nbits - trailing; i < nbits; i++) {
+ if (get_u5_bit(u5data, i))
+ goto fail;
+ }
+
*data = tal_arr(ctx, u8, 0);
if (!bech32_pull_bits(data, u5data, tal_bytelen(u5data) * 5)) {
tal_free(*data);
diff --git a/common/bolt12.c b/common/bolt12.c
index 3b80f057..4b75af62 100644
--- a/common/bolt12.c
+++ b/common/bolt12.c
@@ -10,7 +10,8 @@
#include <inttypes.h>
#include <time.h>
-/* If chains is NULL, max_num_chains is ignored */
+/* If chains is NULL, max_num_chains is ignored.
+ * If must_be_chain is NULL, only structural validity is checked. */
bool bolt12_chains_match(const struct bitcoin_blkid *chains,
size_t max_num_chains,
const struct chainparams *must_be_chain)
@@ -31,6 +32,13 @@ bool bolt12_chains_match(const struct bitcoin_blkid *chains,
* - if the node does not accept invoices for at least one of the `chains`:
* - MUST NOT respond to the offer
*/
+ if (chains && max_num_chains == 0)
+ return false;
+
+ /* No specific chain required: structurally valid. */
+ if (!must_be_chain)
+ return true;
+
if (!chains) {
max_num_chains = 1;
chains = &chainparams_for_network("bitcoin")->genesis_blockhash;
@@ -189,6 +197,18 @@ struct tlv_offer *offer_decode(const tal_t *ctx,
return NULL;
}
+ /* BOLT #12:
+ * - otherwise: (`offer_chains` is set):
+ * - if the node does not accept invoices for at least one of the `chains`:
+ * - MUST NOT respond to the offer
+ */
+ for (size_t i = 0; i < tal_count(offer->fields); i++) {
+ if (offer->fields[i].numtype == 2 && offer->fields[i].length == 0) {
+ *fail = tal_strdup(ctx, "offer_chains must have at least one entry");
+ return tal_free(offer);
+ }
+ }
+
*fail = check_features_and_chain(ctx,
our_features, must_be_chain,
offer->offer_features,
Why this scored 38/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.