decode: don't treat every failure as a rune.
What changed, and why it matters
This commit improves the 'decode' command in Core Lightning so that when it receives a malformed string, it no longer incorrectly labels every failure as a 'malformed rune'. It adds logic to guess the intended type of the input (e.g., a BOLT11 invoice, BOLT12 offer, emergency recovery string, or rune) and returns a more useful error message. There is no direct security vulnerability being fixed; it is a usability and diagnostic improvement.
No urgent security action required. Treat as a normal bugfix/usability improvement. Reviewers may optionally verify that guess_type() correctly handles edge cases and that the new error paths do not leak sensitive decoder internals.
Security signals we found
No memory safety, authentication, authorization, or cryptographic flaw is evident in the diff.
Change is purely diagnostic/usability: better error messages for malformed decode input.
The commit message and changelog explicitly frame this as a user-facing fix for misleading error reporting.
No new attack surface is introduced; input is still parsed by existing decoders.
Evidence from the diff
The patch refactors param_decodable() in plugins/offers.c to introduce a guess_type() helper that inspects the input prefix and structure to classify it as a BOLT12 offer/invoice/invoice_request, emergency recovery string, BOLT11 invoice, or other. Previously, because any bech32 string can be parsed as a rune, decode would often report a rune parsing error even for clearly non-rune inputs. The new logic passes the failure pointer to the most likely decoder so the returned error reflects the actual intended format. It also adds str_uppering() and a unit test for guess_type().
Changed components
plugins/offers.ccommon/utils.ccommon/utils.hplugins/libplugin.hplugins/test/run-decode_guess_type.ctests/test_pay.pyInspect captured patch +405 / −22
diff --git a/common/utils.c b/common/utils.c
index aba47452..83c8b594 100644
--- a/common/utils.c
+++ b/common/utils.c
@@ -215,6 +215,15 @@ char *str_lowering(const void *ctx, const char *string TAKES)
return ret;
}
+char *str_uppering(const void *ctx, const char *string TAKES)
+{
+ char *ret;
+
+ ret = tal_strdup(ctx, string);
+ for (char *p = ret; *p; p++) *p = toupper(*p);
+ return ret;
+}
+
/* Realloc helper for tal membufs */
void *membuf_tal_resize(struct membuf *mb, void *rawelems, size_t newsize)
{
diff --git a/common/utils.h b/common/utils.h
index d9b441e4..a95da849 100644
--- a/common/utils.h
+++ b/common/utils.h
@@ -188,6 +188,15 @@ void *membuf_tal_resize(struct membuf *mb, void *rawelems, size_t newsize);
*/
char *str_lowering(const void *ctx, const char *string TAKES);
+/**
+ * tal_struppering - return the same string by in upper case.
+ * @ctx: the context to tal from (often NULL)
+ * @string: the string that is going to be UPPERED (can be take())
+ *
+ * FIXME: move this in ccan
+ */
+char *str_uppering(const void *ctx, const char *string TAKES);
+
/**
* Assign two different structs which are the same size.
* We use this for assigning secrets <-> sha256 for example.
diff --git a/plugins/libplugin.h b/plugins/libplugin.h
index bcc68dc0..6b3ad7bf 100644
--- a/plugins/libplugin.h
+++ b/plugins/libplugin.h
@@ -416,14 +416,12 @@ command_success(struct command *cmd, const struct json_out *result)
NON_NULL_ARGS(1, 2);
/* End a hook normally (with "result": "continue") */
-struct command_result *WARN_UNUSED_RESULT
-command_hook_success(struct command *cmd)
- NON_NULL_ARGS(1);
+struct command_result *command_hook_success(struct command *cmd)
+ WARN_UNUSED_RESULT NON_NULL_ARGS(1);
/* End a notification handler. */
-struct command_result *WARN_UNUSED_RESULT
-notification_handled(struct command *cmd)
- NON_NULL_ARGS(1);
+struct command_result *notification_handled(struct command *cmd)
+ WARN_UNUSED_RESULT NON_NULL_ARGS(1);
/* End a command created with aux_command. */
struct command_result *WARN_UNUSED_RESULT
diff --git a/plugins/offers.c b/plugins/offers.c
index 2cefc1ff..8e847ab5 100644
--- a/plugins/offers.c
+++ b/plugins/offers.c
@@ -2,6 +2,7 @@
#include "config.h"
#include <ccan/array_size/array_size.h>
#include <ccan/bitops/bitops.h>
+#include <ccan/mem/mem.h>
#include <ccan/rune/rune.h>
#include <ccan/tal/str/str.h>
#include <common/bech32.h>
@@ -492,6 +493,91 @@ fail:
return NULL;
}
+enum likely_type {
+ LIKELY_BOLT12_OFFER,
+ LIKELY_BOLT12_INV,
+ LIKELY_BOLT12_INVREQ,
+ LIKELY_EMERGENCY_RECOVER,
+ LIKELY_BOLT11,
+ LIKELY_OTHER,
+};
+
+/* Pull, either case! Advances tok->start on success. */
+static bool tok_pull(const char *buffer, jsmntok_t *tok, const char *lowerstr)
+{
+ if (!json_tok_startswith(buffer, tok, lowerstr)) {
+ const char *upperstr = str_uppering(tmpctx, lowerstr);
+ if (!json_tok_startswith(buffer, tok, upperstr))
+ return false;
+ }
+ tok->start += strlen(lowerstr);
+ return true;
+}
+
+static enum likely_type guess_type(const char *buffer, const jsmntok_t *tok)
+{
+ jsmntok_t tok_copy = *tok;
+
+ if (tok_pull(buffer, &tok_copy, "lno1"))
+ return LIKELY_BOLT12_OFFER;
+ if (tok_pull(buffer, &tok_copy, "lni1"))
+ return LIKELY_BOLT12_INV;
+ if (tok_pull(buffer, &tok_copy, "lnr1"))
+ return LIKELY_BOLT12_INVREQ;
+ if (tok_pull(buffer, &tok_copy, "clnemerg1"))
+ return LIKELY_EMERGENCY_RECOVER;
+ /* BOLT #11:
+ *
+ * The human-readable part of a Lightning invoice consists of
+ * two sections:
+ *
+ * 1. `prefix`: `ln` + BIP-0173 currency prefix (e.g. `lnbc`
+ * for Bitcoin mainnet, `lntb` for Bitcoin testnet, `lntbs`
+ * for Bitcoin signet, and `lnbcrt` for Bitcoin regtest)
+ *
+ * 1. `amount`: optional number in that currency, followed by
+ * an optional `multiplier` letter. The unit encoded here
+ * is the 'social' convention of a payment unit -- in the
+ * case of Bitcoin the unit is 'bitcoin' NOT satoshis.
+ */
+ if (tok_pull(buffer, &tok_copy, "lnbcrt")
+ || tok_pull(buffer, &tok_copy, "lnbc")
+ || tok_pull(buffer, &tok_copy, "lntbs")
+ || tok_pull(buffer, &tok_copy, "lntb")) {
+ /* Now find last '1', which separates hrp from data */
+ const char *delim = memrchr(buffer + tok_copy.start, '1',
+ tok_copy.end - tok_copy.start);
+ if (!delim)
+ return LIKELY_OTHER;
+
+ /* BOLT #11:
+ * The following `multiplier` letters are defined:
+ *
+ * * `m` (milli): multiply by 0.001
+ * * `u` (micro): multiply by 0.000001
+ * * `n` (nano): multiply by 0.000000001
+ * * `p` (pico): multiply by 0.000000000001
+ */
+ delim--;
+ if (delim > buffer + tok_copy.start
+ && (tolower(*delim) == 'm'
+ || tolower(*delim) == 'u'
+ || tolower(*delim) == 'n'
+ || tolower(*delim) == 'p')) {
+ delim--;
+ }
+
+ while (delim >= buffer + tok_copy.start) {
+ if (!cisdigit(*delim))
+ return LIKELY_OTHER;
+ delim--;
+ }
+ return LIKELY_BOLT11;
+ }
+
+ return LIKELY_OTHER;
+}
+
static struct command_result *param_decodable(struct command *cmd,
const char *name,
const char *buffer,
@@ -500,6 +586,7 @@ static struct command_result *param_decodable(struct command *cmd,
{
char *likely_fail = NULL, *fail;
jsmntok_t tok;
+ enum likely_type type;
/* BOLT #11:
*
@@ -507,14 +594,14 @@ static struct command_result *param_decodable(struct command *cmd,
* use 'lightning:' as a prefix before the BOLT-11 encoding
*/
tok = *token;
- if (json_tok_startswith(buffer, &tok, "lightning:")
- || json_tok_startswith(buffer, &tok, "LIGHTNING:"))
- tok.start += strlen("lightning:");
+ /* Note: either case! */
+ tok_pull(buffer, &tok, "lightning:");
+ type = guess_type(buffer, &tok);
decodable->offer = offer_decode(cmd, buffer + tok.start,
tok.end - tok.start,
plugin_feature_set(cmd->plugin), NULL,
- json_tok_startswith(buffer, &tok, "lno1")
+ type == LIKELY_BOLT12_OFFER
? &likely_fail : &fail);
if (decodable->offer) {
decodable->type = "bolt12 offer";
@@ -525,8 +612,7 @@ static struct command_result *param_decodable(struct command *cmd,
tok.end - tok.start,
plugin_feature_set(cmd->plugin),
NULL,
- json_tok_startswith(buffer, &tok,
- "lni1")
+ type == LIKELY_BOLT12_INV
? &likely_fail : &fail);
if (decodable->invoice) {
decodable->type = "bolt12 invoice";
@@ -537,8 +623,7 @@ static struct command_result *param_decodable(struct command *cmd,
tok.end - tok.start,
plugin_feature_set(cmd->plugin),
NULL,
- json_tok_startswith(buffer, &tok,
- "lnr1")
+ type == LIKELY_BOLT12_INVREQ
? &likely_fail : &fail);
if (decodable->invreq) {
decodable->type = "bolt12 invoice_request";
@@ -547,8 +632,7 @@ static struct command_result *param_decodable(struct command *cmd,
decodable->emergency_recover = encrypted_decode(cmd, tal_strndup(tmpctx, buffer + tok.start,
tok.end - tok.start),
- json_tok_startswith(buffer, &tok,
- "clnemerg1")
+ type == LIKELY_EMERGENCY_RECOVER
? &likely_fail : &fail);
if (decodable->emergency_recover) {
@@ -556,13 +640,12 @@ static struct command_result *param_decodable(struct command *cmd,
return NULL;
}
- /* If no other was likely, bolt11 decoder gives us failure string. */
decodable->b11 = bolt11_decode(cmd,
tal_strndup(tmpctx, buffer + tok.start,
tok.end - tok.start),
plugin_feature_set(cmd->plugin),
NULL, NULL,
- likely_fail ? &fail : &likely_fail);
+ type == LIKELY_BOLT11 ? &likely_fail : &fail);
if (decodable->b11) {
decodable->type = "bolt11 invoice";
return NULL;
@@ -571,10 +654,19 @@ static struct command_result *param_decodable(struct command *cmd,
decodable->rune = rune_from_base64n(decodable, buffer + tok.start,
tok.end - tok.start);
if (decodable->rune) {
- decodable->type = "rune";
- return NULL;
+ /* Any bech32 string will "parse" as a rune, but that's not
+ * helpful. If it isn't all valid UTF8, and it looks like a
+ * different type, reject it as that one. */
+ const char *string = rune_to_string(tmpctx, decodable->rune);
+ if (utf8_check(string, strlen(string)) || type == LIKELY_OTHER) {
+ decodable->type = "rune";
+ return NULL;
+ }
}
+ if (!likely_fail)
+ likely_fail = "Unparsable string";
+
/* Return failure message from most likely parsing candidate */
return command_fail_badparam(cmd, name, buffer, &tok, likely_fail);
}
diff --git a/plugins/test/run-decode_guess_type.c b/plugins/test/run-decode_guess_type.c
new file mode 100644
index 00000000..25000cc4
--- /dev/null
+++ b/plugins/test/run-decode_guess_type.c
@@ -0,0 +1,275 @@
+#include "config.h"
+#define main unused_main
+int main(int argc, char *argv[]);
+#include "../offers.c"
+#undef main
+#include <ccan/array_size/array_size.h>
+#include <common/json_command.h>
+#include <common/setup.h>
+#include <stdio.h>
+
+/* 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_filter_ptr */
+struct json_filter **command_filter_ptr(struct command *cmd UNNEEDED)
+{ fprintf(stderr, "command_filter_ptr called!\n"); abort(); }
+/* Generated stub for command_finished */
+struct command_result *command_finished(struct command *cmd UNNEEDED, struct json_stream *response)
+
+{ fprintf(stderr, "command_finished called!\n"); abort(); }
+/* Generated stub for command_hook_success */
+struct command_result *command_hook_success(struct command *cmd)
+
+{ fprintf(stderr, "command_hook_success called!\n"); abort(); }
+/* Generated stub for command_log */
+void command_log(struct command *cmd UNNEEDED, enum log_level level UNNEEDED,
+ const char *fmt UNNEEDED, ...)
+
+{ fprintf(stderr, "command_log called!\n"); abort(); }
+/* Generated stub for command_param_failed */
+struct command_result *command_param_failed(void)
+{ fprintf(stderr, "command_param_failed 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 establish_onion_path_ */
+struct command_result *establish_onion_path_(struct command *cmd UNNEEDED,
+ struct gossmap *gossmap UNNEEDED,
+ const struct pubkey *local_id UNNEEDED,
+ const struct pubkey *dst UNNEEDED,
+ bool connect_disable UNNEEDED,
+ struct command_result *(*success)(struct command * UNNEEDED,
+ const struct pubkey * UNNEEDED,
+ void *arg) UNNEEDED,
+ struct command_result *(*fail)(struct command * UNNEEDED,
+ const char *why UNNEEDED,
+ void *arg) UNNEEDED,
+ void *arg UNNEEDED)
+{ fprintf(stderr, "establish_onion_path_ called!\n"); abort(); }
+/* Generated stub for flag_jsonfmt */
+bool flag_jsonfmt(struct plugin *plugin UNNEEDED, struct json_stream *js UNNEEDED, const char *fieldname UNNEEDED,
+ bool *i UNNEEDED)
+{ fprintf(stderr, "flag_jsonfmt called!\n"); abort(); }
+/* Generated stub for flag_option */
+char *flag_option(struct plugin *plugin UNNEEDED, const char *arg UNNEEDED, bool check_only UNNEEDED, bool *i UNNEEDED)
+{ fprintf(stderr, "flag_option called!\n"); abort(); }
+/* Generated stub for forward_error */
+struct command_result *forward_error(struct command *cmd UNNEEDED,
+ const char *method UNNEEDED,
+ const char *buf UNNEEDED,
+ const jsmntok_t *error UNNEEDED,
+ void *arg)
+
+{ fprintf(stderr, "forward_error called!\n"); abort(); }
+/* Generated stub for handle_invoice */
+struct command_result *handle_invoice(struct command *cmd UNNEEDED,
+ const u8 *invbin UNNEEDED,
+ struct blinded_path *reply_path STEALS UNNEEDED,
+ const struct secret *secret UNNEEDED)
+{ fprintf(stderr, "handle_invoice called!\n"); abort(); }
+/* Generated stub for handle_invoice_onion_message */
+struct command_result *handle_invoice_onion_message(struct command *cmd UNNEEDED,
+ const char *buf UNNEEDED,
+ const jsmntok_t *om UNNEEDED,
+ const struct secret *pathsecret UNNEEDED)
+{ fprintf(stderr, "handle_invoice_onion_message called!\n"); abort(); }
+/* Generated stub for handle_invoice_request */
+struct command_result *handle_invoice_request(struct command *cmd UNNEEDED,
+ const u8 *invreqbin UNNEEDED,
+ struct blinded_path *reply_path STEALS UNNEEDED,
+ const struct secret *secret TAKES UNNEEDED)
+{ fprintf(stderr, "handle_invoice_request called!\n"); abort(); }
+/* Generated stub for invoice_payment */
+struct command_result *invoice_payment(struct command *cmd UNNEEDED,
+ const char *buf UNNEEDED,
+ const jsmntok_t *params UNNEEDED)
+{ fprintf(stderr, "invoice_payment called!\n"); abort(); }
+/* Generated stub for json_cancelrecurringinvoice */
+struct command_result *json_cancelrecurringinvoice(struct command *cmd UNNEEDED,
+ const char *buffer UNNEEDED,
+ const jsmntok_t *params UNNEEDED)
+{ fprintf(stderr, "json_cancelrecurringinvoice called!\n"); abort(); }
+/* Generated stub for json_dev_rawrequest */
+struct command_result *json_dev_rawrequest(struct command *cmd UNNEEDED,
+ const char *buffer UNNEEDED,
+ const jsmntok_t *params UNNEEDED)
+{ fprintf(stderr, "json_dev_rawrequest called!\n"); abort(); }
+/* Generated stub for json_fetchinvoice */
+struct command_result *json_fetchinvoice(struct command *cmd UNNEEDED,
+ const char *buffer UNNEEDED,
+ const jsmntok_t *params UNNEEDED)
+{ fprintf(stderr, "json_fetchinvoice called!\n"); abort(); }
+/* Generated stub for json_invoicerequest */
+struct command_result *json_invoicerequest(struct command *cmd UNNEEDED,
+ const char *buffer UNNEEDED,
+ const jsmntok_t *params UNNEEDED)
+{ fprintf(stderr, "json_invoicerequest called!\n"); abort(); }
+/* Generated stub for json_offer */
+struct command_result *json_offer(struct command *cmd UNNEEDED,
+ const char *buffer UNNEEDED,
+ const jsmntok_t *params UNNEEDED)
+{ fprintf(stderr, "json_offer called!\n"); abort(); }
+/* Generated stub for json_out_obj */
+struct json_out *json_out_obj(const tal_t *ctx UNNEEDED,
+ const char *fieldname UNNEEDED,
+ const char *str TAKES UNNEEDED)
+{ fprintf(stderr, "json_out_obj called!\n"); abort(); }
+/* Generated stub for json_sendinvoice */
+struct command_result *json_sendinvoice(struct command *cmd UNNEEDED,
+ const char *buffer UNNEEDED,
+ const jsmntok_t *params UNNEEDED)
+{ fprintf(stderr, "json_sendinvoice called!\n"); abort(); }
+/* Generated stub for jsonrpc_request_start_ */
+struct out_req *jsonrpc_request_start_(struct command *cmd UNNEEDED,
+ const char *method UNNEEDED,
+ const char *id_prefix UNNEEDED,
+ const char *filter UNNEEDED,
+ struct command_result *(*cb)(struct command *command UNNEEDED,
+ const char *methodname UNNEEDED,
+ const char *buf UNNEEDED,
+ const jsmntok_t *result UNNEEDED,
+ void *arg) UNNEEDED,
+ struct command_result *(*errcb)(struct command *command UNNEEDED,
+ const char *methodname UNNEEDED,
+ const char *buf UNNEEDED,
+ const jsmntok_t *result UNNEEDED,
+ void *arg) UNNEEDED,
+ void *arg)
+
+{ fprintf(stderr, "jsonrpc_request_start_ called!\n"); abort(); }
+/* Generated stub for jsonrpc_stream_success */
+struct json_stream *jsonrpc_stream_success(struct command *cmd)
+
+{ fprintf(stderr, "jsonrpc_stream_success called!\n"); abort(); }
+/* Generated stub for notification_handled */
+struct command_result *notification_handled(struct command *cmd)
+
+{ fprintf(stderr, "notification_handled called!\n"); abort(); }
+/* Generated stub for plugin_err */
+void plugin_err(struct plugin *p UNNEEDED, const char *fmt UNNEEDED, ...)
+{ fprintf(stderr, "plugin_err called!\n"); abort(); }
+/* Generated stub for plugin_feature_set */
+const struct feature_set *plugin_feature_set(const struct plugin *p UNNEEDED)
+{ fprintf(stderr, "plugin_feature_set called!\n"); abort(); }
+/* Generated stub for plugin_gossmap_logcb */
+void plugin_gossmap_logcb(struct plugin *plugin UNNEEDED,
+ enum log_level level UNNEEDED,
+ const char *fmt UNNEEDED,
+ ...)
+{ fprintf(stderr, "plugin_gossmap_logcb called!\n"); abort(); }
+/* Generated stub for plugin_log */
+void plugin_log(struct plugin *p UNNEEDED, enum log_level l UNNEEDED, const char *fmt UNNEEDED, ...)
+{ fprintf(stderr, "plugin_log called!\n"); abort(); }
+/* Generated stub for plugin_main */
+void plugin_main(char *argv[] UNNEEDED,
+ const char *(*init)(struct command *init_cmd UNNEEDED,
+ const char *buf UNNEEDED,
+ const jsmntok_t *) UNNEEDED,
+ void *data TAKES UNNEEDED,
+ const enum plugin_restartability restartability UNNEEDED,
+ bool init_rpc UNNEEDED,
+ struct feature_set *features STEALS UNNEEDED,
+ const struct plugin_command *commands TAKES UNNEEDED,
+ size_t num_commands UNNEEDED,
+ const struct plugin_notification *notif_subs TAKES UNNEEDED,
+ size_t num_notif_subs UNNEEDED,
+ const struct plugin_hook *hook_subs TAKES UNNEEDED,
+ size_t num_hook_subs UNNEEDED,
+ const char **notif_topics TAKES UNNEEDED,
+ size_t num_notif_topics UNNEEDED,
+ ...)
+{ fprintf(stderr, "plugin_main called!\n"); abort(); }
+/* Generated stub for rpc_scan */
+void rpc_scan(struct command *cmd UNNEEDED,
+ const char *method UNNEEDED,
+ const struct json_out *params TAKES UNNEEDED,
+ const char *guide UNNEEDED,
+ ...)
+{ fprintf(stderr, "rpc_scan called!\n"); abort(); }
+/* Generated stub for send_outreq */
+struct command_result *send_outreq(const struct out_req *req UNNEEDED)
+{ fprintf(stderr, "send_outreq called!\n"); abort(); }
+/* AUTOGENERATED MOCKS END */
+
+struct likely_test {
+ const char *input;
+ enum likely_type expected;
+};
+
+static void run_likely_tests(const struct likely_test *tests)
+{
+ for (size_t i = 0; tests[i].input; i++) {
+ const char *s = tests[i].input;
+ /* Dynamic allocation makes it easier for valgrind to
+ * see uninit mem */
+ jsmntok_t *tok = tal(tmpctx, jsmntok_t);
+ tok->start = 0;
+ tok->end = strlen(s);
+ tok->type = JSMN_STRING;
+
+ assert(guess_type(s, tok) == tests[i].expected);
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ common_setup(argv[0]);
+
+ static const struct likely_test tests[] = {
+ /* BOLT12 */
+ { "lno1qqqqqq", LIKELY_BOLT12_OFFER },
+ { "LNO1QQQQQQ", LIKELY_BOLT12_OFFER },
+ { "lni1abcd", LIKELY_BOLT12_INV },
+ { "LNI1ABCD", LIKELY_BOLT12_INV },
+ { "lnr1xyz", LIKELY_BOLT12_INVREQ },
+ { "LNR1XYZ", LIKELY_BOLT12_INVREQ },
+
+ /* Emergency recover */
+ { "clnemerg1foo", LIKELY_EMERGENCY_RECOVER },
+ { "CLNEMERG1FOO", LIKELY_EMERGENCY_RECOVER },
+
+ /* BOLT11 (lower + upper, amount + no amount) */
+ { "lnbc1qqqqqq", LIKELY_BOLT11 },
+ { "LNBC1QQQQQQ", LIKELY_BOLT11 },
+ { "lnbc10u1qqqqqq", LIKELY_BOLT11 },
+ { "LNTB2500N1ABCDEF", LIKELY_BOLT11 },
+ { "lnbcrt1deadbeef", LIKELY_BOLT11 },
+
+ /* Invalid / other */
+ { "lnbcx1qqqq", LIKELY_OTHER }, /* bad amount */
+ { "lnbc10x1qqqq", LIKELY_OTHER }, /* bad multiplier */
+ { "lnbc2000qqqq", LIKELY_OTHER }, /* missing separator */
+ { "notlightning", LIKELY_OTHER },
+ { "", LIKELY_OTHER },
+
+ { NULL, 0 }
+ };
+
+ run_likely_tests(tests);
+
+ common_shutdown();
+ return 0;
+}
diff --git a/tests/test_pay.py b/tests/test_pay.py
index 215050bc..c8633acc 100644
--- a/tests/test_pay.py
+++ b/tests/test_pay.py
@@ -7174,5 +7174,5 @@ def test_invoice_amount_override(node_factory):
def test_offer_currency_no_amount(node_factory):
l1 = node_factory.get_node()
- ret = l1.rpc.decode("lno1qcp4256ypgx9getnwss8vetrw3hhyuckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg")
- assert ret['valid'] is False
+ with pytest.raises(RpcError, match="currency with no amount"):
+ l1.rpc.decode("lno1qcp4256ypgx9getnwss8vetrw3hhyuckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg")
Why this scored 19/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.