diff --git a/common/bolt12.c b/common/bolt12.c index 1435b35d854a..6cdc0762f343 100644 --- a/common/bolt12.c +++ b/common/bolt12.c @@ -221,6 +221,16 @@ struct tlv_offer *offer_decode(const tal_t *ctx, return tal_free(offer); } + /* BOLT #12: + * + * - if `offer_currency` is set and `offer_amount` is not set: + * - MUST NOT respond to the offer. + */ + if (offer->offer_currency && !offer->offer_amount) { + *fail = tal_strdup(ctx, "Offer contains a currency with no amount"); + return tal_free(offer); + } + /* BOLT #12: * * - if neither `offer_issuer_id` nor `offer_paths` are set: diff --git a/common/utils.c b/common/utils.c index aba4745218d0..83c8b59416a9 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 d9b441e4bc2c..a95da849f7ea 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 bcc68dc03948..6b3ad7bfd48e 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 2cefc1ff7cf7..8e847ab578c8 100644 --- a/plugins/offers.c +++ b/plugins/offers.c @@ -2,6 +2,7 @@ #include "config.h" #include #include +#include #include #include #include @@ -492,6 +493,91 @@ static u8 *encrypted_decode(const tal_t *ctx, const char *str, char **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 000000000000..25000cc4a0b4 --- /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 +#include +#include +#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_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 48abcb3142ab..abeb0461ae8a 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -7212,3 +7212,9 @@ def test_invoice_amount_override(node_factory): l1.rpc.sendpay(route, inv["payment_hash"], payment_secret=inv["payment_secret"]) assert l1.rpc.waitsendpay(inv["payment_hash"])["status"] == "complete" + + +def test_offer_currency_no_amount(node_factory): + l1 = node_factory.get_node() + with pytest.raises(RpcError, match="currency with no amount"): + l1.rpc.decode("lno1qcp4256ypgx9getnwss8vetrw3hhyuckyypwa3eyt44h6txtxquqh7lz5djge4afgfjn7k4rgrkuag0jsd5xvxg")