From c08ff167b224472f6e063af2118922377f33a049 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Fri, 8 Jan 2021 05:18:47 +1030 Subject: [PATCH] decode: new generic API to decode bolt11 and bolt12. This is experimental for now, but can eventually deprecated 'decodepay' and even decode other kinds of messages. Signed-off-by: Rusty Russell --- common/bolt12.c | 26 +- common/bolt12.h | 8 + common/json_helpers.c | 17 ++ common/json_helpers.h | 12 + doc/Makefile | 1 + doc/index.rst | 1 + doc/lightning-decode.7 | 172 ++++++++++++ doc/lightning-decode.7.md | 135 +++++++++ plugins/Makefile | 2 +- plugins/offers.c | 561 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 926 insertions(+), 9 deletions(-) create mode 100644 doc/lightning-decode.7 create mode 100644 doc/lightning-decode.7.md diff --git a/common/bolt12.c b/common/bolt12.c index 4e1120ce3..9cff0ab88 100644 --- a/common/bolt12.c +++ b/common/bolt12.c @@ -71,6 +71,22 @@ static char *check_features_and_chain(const tal_t *ctx, return NULL; } +bool bolt12_check_signature(const struct tlv_field *fields, + const char *messagename, + const char *fieldname, + const struct pubkey32 *key, + const struct bip340sig *sig) +{ + struct sha256 m, shash; + + merkle_tlv(fields, &m); + sighash_from_merkle(messagename, fieldname, &m, &shash); + return secp256k1_schnorrsig_verify(secp256k1_ctx, + sig->u8, + shash.u.u8, + &key->pubkey) == 1; +} + static char *check_signature(const tal_t *ctx, const struct tlv_field *fields, const char *messagename, @@ -78,19 +94,13 @@ static char *check_signature(const tal_t *ctx, const struct pubkey32 *node_id, const struct bip340sig *sig) { - struct sha256 m, shash; - if (!node_id) return tal_fmt(ctx, "Missing node_id"); if (!sig) return tal_fmt(ctx, "Missing signature"); - merkle_tlv(fields, &m); - sighash_from_merkle(messagename, fieldname, &m, &shash); - if (secp256k1_schnorrsig_verify(secp256k1_ctx, - sig->u8, - shash.u.u8, - &node_id->pubkey) != 1) + if (!bolt12_check_signature(fields, + messagename, fieldname, node_id, sig)) return tal_fmt(ctx, "Invalid signature"); return NULL; } diff --git a/common/bolt12.h b/common/bolt12.h index 1f36d5216..15d8e52f0 100644 --- a/common/bolt12.h +++ b/common/bolt12.h @@ -95,6 +95,14 @@ struct tlv_invoice *invoice_decode_nosig(const tal_t *ctx, const struct chainparams *must_be_chain, char **fail); +/* Check a bolt12-style signature. */ +bool bolt12_check_signature(const struct tlv_field *fields, + const char *messagename, + const char *fieldname, + const struct pubkey32 *key, + const struct bip340sig *sig) + NO_NULL_ARGS; + /* Given a tal_arr of chains, does it contain this chain? */ bool bolt12_chains_match(const struct bitcoin_blkid *chains, const struct chainparams *must_be_chain); diff --git a/common/json_helpers.c b/common/json_helpers.c index c3dfdaf38..61450a302 100644 --- a/common/json_helpers.c +++ b/common/json_helpers.c @@ -170,6 +170,23 @@ void json_add_pubkey(struct json_stream *response, json_add_hex(response, fieldname, der, sizeof(der)); } +void json_add_pubkey32(struct json_stream *response, + const char *fieldname, + const struct pubkey32 *key) +{ + u8 output[32]; + + secp256k1_xonly_pubkey_serialize(secp256k1_ctx, output, &key->pubkey); + json_add_hex(response, fieldname, output, sizeof(output)); +} + +void json_add_bip340sig(struct json_stream *response, + const char *fieldname, + const struct bip340sig *sig) +{ + json_add_hex(response, fieldname, sig->u8, sizeof(sig->u8)); +} + void json_add_txid(struct json_stream *result, const char *fieldname, const struct bitcoin_txid *txid) { diff --git a/common/json_helpers.h b/common/json_helpers.h index f72f3a30e..5d0b568f4 100644 --- a/common/json_helpers.h +++ b/common/json_helpers.h @@ -9,10 +9,12 @@ struct amount_msat; struct amount_sat; +struct bip340sig; struct channel_id; struct node_id; struct preimage; struct pubkey; +struct pubkey32; struct secret; struct short_channel_id; struct wireaddr; @@ -83,6 +85,16 @@ void json_add_pubkey(struct json_stream *response, const char *fieldname, const struct pubkey *key); +/* '"fieldname" : "89abcdef..."' or "89abcdef..." if fieldname is NULL */ +void json_add_pubkey32(struct json_stream *response, + const char *fieldname, + const struct pubkey32 *key); + +/* '"fieldname" : "89abcdef..."' or "89abcdef..." if fieldname is NULL */ +void json_add_bip340sig(struct json_stream *response, + const char *fieldname, + const struct bip340sig *sig); + /* '"fieldname" : "89abcdef..."' or "89abcdef..." if fieldname is NULL */ void json_add_secret(struct json_stream *response, const char *fieldname, diff --git a/doc/Makefile b/doc/Makefile index bf11cee1e..288d26c1e 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -15,6 +15,7 @@ MANPAGES := doc/lightning-cli.1 \ doc/lightning-createonion.7 \ doc/lightning-createinvoice.7 \ doc/lightning-decodepay.7 \ + doc/lightning-decode.7 \ doc/lightning-delexpiredinvoice.7 \ doc/lightning-delinvoice.7 \ doc/lightning-delpay.7 \ diff --git a/doc/index.rst b/doc/index.rst index 98d19a5e5..3fe53324d 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -37,6 +37,7 @@ c-lightning Documentation lightning-connect lightning-createinvoice lightning-createonion + lightning-decode lightning-decodepay lightning-delexpiredinvoice lightning-delinvoice diff --git a/doc/lightning-decode.7 b/doc/lightning-decode.7 new file mode 100644 index 000000000..3ebe2977d --- /dev/null +++ b/doc/lightning-decode.7 @@ -0,0 +1,172 @@ +.TH "LIGHTNING-DECODE" "7" "" "" "lightning-decode" +.SH NAME +lightning-decode - Command for decoding an invoice string (low-level) +.SH SYNOPSIS + +\fIEXPERIMENTAL_FEATURES only\fR + + +\fBdecode\fR \fIstring\fR + +.SH DESCRIPTION + +The \fBdecode\fR RPC command checks and parses a \fIbolt11\fR or \fIbolt12\fR +string (optionally prefixed by \fBlightning:\fR or \fBLIGHTNING:\fR) as +specified by the BOLT 11 and BOLT 12 specifications\. It may decode +other formats in future\. + +.SH RETURN VALUE + +On success, an object is returned with a \fItype\fR member indicating the +type of the decoding: + + +\fItype\fR: "bolt12 offer" + +.nf +.RS +- *offer_id*: the id of this offer (merkle hash of non-signature fields) +- *chains* (optional): if set, an array of genesis hashes of supported chains. (Unset implies bitcoin mainnet). +- *currency* (optional): ISO 4217 code of the currency. +- *minor_unit* (optional): the number of decimal places to apply to amount (if currency known) +- *amount* (optional): the amount in the *currency* adjusted by *minor_unit*, if any. +- *amount_msat* (optional): the amount (with "msat" appended) if there is no *currency*. +- *send_invoice* (optional): `true` if this is a send_invoice offer. +- *refund_for* (optional): the sha256 payment_preimage of invoice this is a refund for. +- *description* (optional): the UTF-8 description of the purpose of the offer. +- *vendor* (optional): the UTF-8 name of the vendor for this offer. +- *features* (optional): hex array of feature bits. +- *absolute_expiry* (optional): UNIX timestamp of when this offer expires. +- *paths* (optional): Array of objects containing *blinding*, *path* array; each *path* entry contains an object with *node_id* and *enctlv*. +- *quantity_min* (optional): minimum valid quantity for offer responses +- *quantity_max* (optional): maximum valid quantity for offer responses +- *recurrence* (optional): an object containing *time_unit*, *time_unit_name* (optional, a string), *period*, *basetime* (optional), *start_any_period* (optional), *limit* (optional), and *paywindow* (optional) object containing *seconds_before*, *seconds_after* and *proportional_amount* (optional). +- *node_id*: 32-byte (x-only) public key of the offering node. +- *signature*: BIP-340 signature of the *node_id* on this offer. + + +.RE + +.fi + +\fItype\fR: "bolt12 invoice" + +.nf +.RS +- *chains* (optional): if set, an array of genesis hashes of supported chains. (Unset implies bitcoin mainnet). +- *offer_id* (optional): id of the offer this invoice is for. +- *amount_msat* (optional): the amount (with "msat" appended). +- *description* (optional): the UTF-8 description of the purpose of the offer. +- *vendor* (optional): the UTF-8 name of the vendor for this offer. +- *features* (optional): hex array of feature bits. +- *paths* (optional): Array of objects containing *blinding*, *path* array; each *path* entry contains an object with *node_id*, *enctlv*, *fee_base_msat* (optional), *fee_proportional_millionths* (optional), *cltv_expiry_delta* (optional), and *features* (optional). +- *quantity* (optional): quantity of items. +- *send_invoice* (optional): `true` if this is a response to a send_invoice offer. +- *refund_for* (optional): the sha256 payment_preimage of invoice this is a refund for. +- *recurrence_counter* (optional): the zero-based number of the invoice for a recurring offer. +- *recurrence_start* (optional): the zero-based offet of the first invoice for the recurring offer. +- *recurrence_basetime* (optional): the UNIX timestamp of the first period of the offer. +- *payer_key* (optional): the 32-byte (x-only) id of the payer. +- *payer_info* (optional): a variable-length blob for the payer to derive their key. +- *timestamp* (optional): the UNIX timestamp of the invoice. +- *payment_hash* (optional): the hex SHA256 of the payment_preimage. +- *expiry* (optional): seconds from *timestamp* when invoice expires. +- *min_final_cltv_expiry*: required CLTV for final hop. +- *fallbacks* (optional): an array containing objects with *version*, and *hex* fields for each fallback address, and *address* (optional) if it's parsable. +- *refund_signature* (optional): BIP-340 signature of the *payer_key* on this offer. +- *node_id*: 32-byte (x-only) public key of the invoicing node. +- *signature*: BIP-340 signature of the *node_id* on this invoice. + + +.RE + +.fi + +\fItype\fR: "bolt12 invoice_request" + +.nf +.RS +- *chains* (optional): if set, an array of genesis hashes of supported chains. (Unset implies bitcoin mainnet). +- *offer_id* (optional): id of the offer this invoice is for. +- *amount_msat* (optional): the amount (with "msat" appended). +- *features* (optional): hex array of feature bits. +- *quantity* (optional): quantity of items. +- *recurrence_counter* (optional): the zero-based number of the invoice for a recurring offer. +- *recurrence_start* (optional): the zero-based offet of the first invoice for the recurring offer. +- *payer_key* (optional): the 32-byte (x-only) id of the payer. +- *payer_info* (optional): a variable-length blob for the payer to derive their key. +- *recurrence_signature* (optional): BIP-340 signature of the *payer_key* on this offer. + + +.RE + +.fi + +\fItype\fR: "bolt11 invoice" + +.nf +.RS +- *currency*: the BIP173 name for the currency. +- *timestamp*: the UNIX-style timestamp of the invoice. +- *expiry*: the number of seconds this is valid after *timestamp*. +- *payee*: the public key of the recipient. +- *payment_hash*: the payment hash of the request. +- *signature*: the DER-encoded signature. +- *description*: the UTF-8 description of the purpose of the purchase. +- *msatoshi* (optional): the number of millisatoshi requested (if any). +- *amount_msat* (optional): the same as above, with *msat* appended (if any). +- *fallbacks* (optional): array of fallback address object containing a *hex* string, and both *type* and *addr* if it is recognized as one of *P2PKH*, *P2SH*, *P2WPKH*, or *P2WSH*. +- *routes* (optional): an array of routes. Each route is an arrays of objects, each containing *pubkey*, *short_channel_id*, *fee_base_msat*, *fee_proportional_millionths* and *cltv_expiry_delta*. +- *extra* (optional): an array of objects representing unknown fields, each with one-character *tag* and a *data* bech32 string. + + +.RE + +.fi + +Some invalid strings can still be parsed, and warnings will be given: + +.nf +.RS +- "warning_offer_unknown_currency": unknown or invalid *currency* code. +- "warning_offer_missing_description": invalid due to missing description. +- "warning_invoice_invalid_blinded_payinfo": blinded_payinfo does not match paths. +- "warning_invoice_fallbacks_version_invalid": a fallback version is not a valid segwit version +- "warning_invoice_fallbacks_address_invalid": a fallback address is not a valid segwit address (within an object in the *fallback* array) +- "warning_invoice_missing_amount": amount field is missing. +- "warning_invoice_missing_description": description field is missing. +- "warning_invoice_missing_blinded_payinfo": blindedpay is missing. +- "warning_invoice_missing_recurrence_basetime: recurrence_basetime is missing. +- "warning_invoice_missing_timestamp": timestamp is missing. +- "warning_invoice_missing_payment_hash": payment hash is missing. +- "warning_invoice_refund_signature_missing_payer_key": payer_key is missing for refund_signature. +- "warning_invoice_refund_signature_invalid": refund_signature does not match. +- "warning_invoice_refund_missing_signature": refund_signature is missing. +- "warning_invoice_request_missing_offer_id": offer_id is missing. +- "warning_invoice_request_missing_payer_key": payer_key is missing. +- "warning_invoice_request_invalid_recurrence_signature": recurrence_signature does not match. +- "warning_invoice_request_missing_recurrence_signature": recurrence_signature is missing. + + +.RE + +.fi +.SH AUTHOR + +Rusty Russell \fI is mainly responsible\. + +.SH SEE ALSO + +\fBlightning-pay\fR(7), \fBlightning-offer\fR(7), \fBlightning-offerout\fR(7), \fBlightning-fetchinvoice\fR(7), \fBlightning-sendinvoice\fR(7) + + +\fBBOLT #11\fR (\fIhttps://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md\fR)\. + + +\fBBOLT #12\fR (\fIhttps://github.com/lightningnetwork/lightning-rfc/blob/master/12-offer-encoding.md\fR)\. + +.SH RESOURCES + +Main web site: \fIhttps://github.com/ElementsProject/lightning\fR + +\" SHA256STAMP:6920ea3b5e3fe8c193ce149b813496370fbc249649911595ea857f5cfb7d6e89 diff --git a/doc/lightning-decode.7.md b/doc/lightning-decode.7.md new file mode 100644 index 000000000..4dca34cda --- /dev/null +++ b/doc/lightning-decode.7.md @@ -0,0 +1,135 @@ +lightning-decode -- Command for decoding an invoice string (low-level) +======================================================================= + +SYNOPSIS +-------- + +*EXPERIMENTAL_FEATURES only* + +**decode** *string* + +DESCRIPTION +----------- + +The **decode** RPC command checks and parses a *bolt11* or *bolt12* +string (optionally prefixed by `lightning:` or `LIGHTNING:`) as +specified by the BOLT 11 and BOLT 12 specifications. It may decode +other formats in future. + +RETURN VALUE +------------ + +On success, an object is returned with a *type* member indicating the +type of the decoding: + +*type*: "bolt12 offer" + - *offer_id*: the id of this offer (merkle hash of non-signature fields) + - *chains* (optional): if set, an array of genesis hashes of supported chains. (Unset implies bitcoin mainnet). + - *currency* (optional): ISO 4217 code of the currency. + - *minor_unit* (optional): the number of decimal places to apply to amount (if currency known) + - *amount* (optional): the amount in the *currency* adjusted by *minor_unit*, if any. + - *amount_msat* (optional): the amount (with "msat" appended) if there is no *currency*. + - *send_invoice* (optional): `true` if this is a send_invoice offer. + - *refund_for* (optional): the sha256 payment_preimage of invoice this is a refund for. + - *description* (optional): the UTF-8 description of the purpose of the offer. + - *vendor* (optional): the UTF-8 name of the vendor for this offer. + - *features* (optional): hex array of feature bits. + - *absolute_expiry* (optional): UNIX timestamp of when this offer expires. + - *paths* (optional): Array of objects containing *blinding*, *path* array; each *path* entry contains an object with *node_id* and *enctlv*. + - *quantity_min* (optional): minimum valid quantity for offer responses + - *quantity_max* (optional): maximum valid quantity for offer responses + - *recurrence* (optional): an object containing *time_unit*, *time_unit_name* (optional, a string), *period*, *basetime* (optional), *start_any_period* (optional), *limit* (optional), and *paywindow* (optional) object containing *seconds_before*, *seconds_after* and *proportional_amount* (optional). + - *node_id*: 32-byte (x-only) public key of the offering node. + - *signature*: BIP-340 signature of the *node_id* on this offer. + +*type*: "bolt12 invoice" + - *chains* (optional): if set, an array of genesis hashes of supported chains. (Unset implies bitcoin mainnet). + - *offer_id* (optional): id of the offer this invoice is for. + - *amount_msat* (optional): the amount (with "msat" appended). + - *description* (optional): the UTF-8 description of the purpose of the offer. + - *vendor* (optional): the UTF-8 name of the vendor for this offer. + - *features* (optional): hex array of feature bits. + - *paths* (optional): Array of objects containing *blinding*, *path* array; each *path* entry contains an object with *node_id*, *enctlv*, *fee_base_msat* (optional), *fee_proportional_millionths* (optional), *cltv_expiry_delta* (optional), and *features* (optional). + - *quantity* (optional): quantity of items. + - *send_invoice* (optional): `true` if this is a response to a send_invoice offer. + - *refund_for* (optional): the sha256 payment_preimage of invoice this is a refund for. + - *recurrence_counter* (optional): the zero-based number of the invoice for a recurring offer. + - *recurrence_start* (optional): the zero-based offet of the first invoice for the recurring offer. + - *recurrence_basetime* (optional): the UNIX timestamp of the first period of the offer. + - *payer_key* (optional): the 32-byte (x-only) id of the payer. + - *payer_info* (optional): a variable-length blob for the payer to derive their key. + - *timestamp* (optional): the UNIX timestamp of the invoice. + - *payment_hash* (optional): the hex SHA256 of the payment_preimage. + - *expiry* (optional): seconds from *timestamp* when invoice expires. + - *min_final_cltv_expiry*: required CLTV for final hop. + - *fallbacks* (optional): an array containing objects with *version*, and *hex* fields for each fallback address, and *address* (optional) if it's parsable. + - *refund_signature* (optional): BIP-340 signature of the *payer_key* on this offer. + - *node_id*: 32-byte (x-only) public key of the invoicing node. + - *signature*: BIP-340 signature of the *node_id* on this invoice. + +*type*: "bolt12 invoice_request" + - *chains* (optional): if set, an array of genesis hashes of supported chains. (Unset implies bitcoin mainnet). + - *offer_id* (optional): id of the offer this invoice is for. + - *amount_msat* (optional): the amount (with "msat" appended). + - *features* (optional): hex array of feature bits. + - *quantity* (optional): quantity of items. + - *recurrence_counter* (optional): the zero-based number of the invoice for a recurring offer. + - *recurrence_start* (optional): the zero-based offet of the first invoice for the recurring offer. + - *payer_key* (optional): the 32-byte (x-only) id of the payer. + - *payer_info* (optional): a variable-length blob for the payer to derive their key. + - *recurrence_signature* (optional): BIP-340 signature of the *payer_key* on this offer. + +*type*: "bolt11 invoice" + - *currency*: the BIP173 name for the currency. + - *timestamp*: the UNIX-style timestamp of the invoice. + - *expiry*: the number of seconds this is valid after *timestamp*. + - *payee*: the public key of the recipient. + - *payment_hash*: the payment hash of the request. + - *signature*: the DER-encoded signature. + - *description*: the UTF-8 description of the purpose of the purchase. + - *msatoshi* (optional): the number of millisatoshi requested (if any). + - *amount_msat* (optional): the same as above, with *msat* appended (if any). + - *fallbacks* (optional): array of fallback address object containing a *hex* string, and both *type* and *addr* if it is recognized as one of *P2PKH*, *P2SH*, *P2WPKH*, or *P2WSH*. + - *routes* (optional): an array of routes. Each route is an arrays of objects, each containing *pubkey*, *short_channel_id*, *fee_base_msat*, *fee_proportional_millionths* and *cltv_expiry_delta*. + - *extra* (optional): an array of objects representing unknown fields, each with one-character *tag* and a *data* bech32 string. + +Some invalid strings can still be parsed, and warnings will be given: + - "warning_offer_unknown_currency": unknown or invalid *currency* code. + - "warning_offer_missing_description": invalid due to missing description. + - "warning_invoice_invalid_blinded_payinfo": blinded_payinfo does not match paths. + - "warning_invoice_fallbacks_version_invalid": a fallback version is not a valid segwit version + - "warning_invoice_fallbacks_address_invalid": a fallback address is not a valid segwit address (within an object in the *fallback* array) + - "warning_invoice_missing_amount": amount field is missing. + - "warning_invoice_missing_description": description field is missing. + - "warning_invoice_missing_blinded_payinfo": blindedpay is missing. + - "warning_invoice_missing_recurrence_basetime: recurrence_basetime is missing. + - "warning_invoice_missing_timestamp": timestamp is missing. + - "warning_invoice_missing_payment_hash": payment hash is missing. + - "warning_invoice_refund_signature_missing_payer_key": payer_key is missing for refund_signature. + - "warning_invoice_refund_signature_invalid": refund_signature does not match. + - "warning_invoice_refund_missing_signature": refund_signature is missing. + - "warning_invoice_request_missing_offer_id": offer_id is missing. + - "warning_invoice_request_missing_payer_key": payer_key is missing. + - "warning_invoice_request_invalid_recurrence_signature": recurrence_signature does not match. + - "warning_invoice_request_missing_recurrence_signature": recurrence_signature is missing. + +AUTHOR +------ + +Rusty Russell <> is mainly responsible. + +SEE ALSO +-------- + +lightning-pay(7), lightning-offer(7), lightning-offerout(7), lightning-fetchinvoice(7), lightning-sendinvoice(7) + +[BOLT \#11](https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md). + +[BOLT \#12](https://github.com/lightningnetwork/lightning-rfc/blob/master/12-offer-encoding.md). + + +RESOURCES +--------- + +Main web site: + diff --git a/plugins/Makefile b/plugins/Makefile index b6f2471b5..63c152e3f 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -141,7 +141,7 @@ $(PLUGIN_KEYSEND_OBJS): $(PLUGIN_PAY_LIB_HEADER) plugins/spenderp: bitcoin/chainparams.o bitcoin/psbt.o common/psbt_open.o $(PLUGIN_SPENDER_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) -plugins/offers: bitcoin/chainparams.o $(PLUGIN_OFFERS_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) common/bolt12.o common/bolt12_merkle.o common/iso4217.o wire/bolt12_exp_wiregen.o $(WIRE_OBJS) bitcoin/block.o common/channel_id.o bitcoin/preimage.o $(JSMN_OBJS) $(CCAN_OBJS) +plugins/offers: bitcoin/chainparams.o $(PLUGIN_OFFERS_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) common/bolt12.o common/bolt12_merkle.o common/bolt11_json.o common/iso4217.o wire/bolt12_exp_wiregen.o $(WIRE_OBJS) bitcoin/block.o common/channel_id.o bitcoin/preimage.o $(JSMN_OBJS) $(CCAN_OBJS) plugins/fetchinvoice: bitcoin/chainparams.o $(PLUGIN_FETCHINVOICE_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) common/bolt12.o common/bolt12_merkle.o common/iso4217.o wire/bolt12_exp_wiregen.o $(WIRE_OBJS) bitcoin/block.o common/channel_id.o bitcoin/preimage.o $(JSMN_OBJS) $(CCAN_OBJS) common/gossmap.o common/dijkstra.o common/route.o common/blindedpath.o common/hmac.o common/blinding.o diff --git a/plugins/offers.c b/plugins/offers.c index 9bc44bca2..0a68cff22 100644 --- a/plugins/offers.c +++ b/plugins/offers.c @@ -1,5 +1,13 @@ /* This plugin covers both sending and receiving offers */ +#include #include +#include +#include +#include +#include +#include +#include +#include #include #include #include @@ -111,6 +119,552 @@ static const struct plugin_hook hooks[] = { }, }; +struct decodable { + const char *type; + struct bolt11 *b11; + struct tlv_offer *offer; + struct tlv_invoice *invoice; + struct tlv_invoice_request *invreq; +}; + +static struct command_result *param_decodable(struct command *cmd, + const char *name, + const char *buffer, + const jsmntok_t *token, + struct decodable *decodable) +{ + char *likely_fail = NULL, *fail; + jsmntok_t tok; + + /* BOLT #11: + * + * If a URI scheme is desired, the current recommendation is to either + * 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:"); + + decodable->offer = offer_decode(cmd, buffer + tok.start, + tok.end - tok.start, + plugin_feature_set(cmd->plugin), NULL, + json_tok_startswith(buffer, &tok, "lno1") + ? &likely_fail : &fail); + if (decodable->offer) { + decodable->type = "bolt12 offer"; + return NULL; + } + + decodable->invoice = invoice_decode(cmd, buffer + tok.start, + tok.end - tok.start, + plugin_feature_set(cmd->plugin), + NULL, + json_tok_startswith(buffer, &tok, + "lni1") + ? &likely_fail : &fail); + if (decodable->invoice) { + decodable->type = "bolt12 invoice"; + return NULL; + } + + decodable->invreq = invrequest_decode(cmd, buffer + tok.start, + tok.end - tok.start, + plugin_feature_set(cmd->plugin), + NULL, + json_tok_startswith(buffer, &tok, + "lnr1") + ? &likely_fail : &fail); + if (decodable->invreq) { + decodable->type = "bolt12 invoice_request"; + 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); + if (decodable->b11) { + decodable->type = "bolt11 invoice"; + return NULL; + } + + /* Return failure message from most likely parsing candidate */ + return command_fail_badparam(cmd, name, buffer, &tok, likely_fail); +} + +static void json_add_chains(struct json_stream *js, + const struct bitcoin_blkid *chains) +{ + json_array_start(js, "chains"); + for (size_t i = 0; i < tal_count(chains); i++) + json_add_sha256(js, NULL, &chains[i].shad.sha); + json_array_end(js); +} + +static void json_add_onionmsg_path(struct json_stream *js, + const char *fieldname, + const struct onionmsg_path *path, + const struct blinded_payinfo *payinfo) +{ + json_object_start(js, fieldname); + json_add_pubkey(js, "node_id", &path->node_id); + json_add_hex_talarr(js, "enctlv", path->enctlv); + if (payinfo) { + json_add_u32(js, "fee_base_msat", payinfo->fee_base_msat); + json_add_u32(js, "fee_proportional_millionths", + payinfo->fee_proportional_millionths); + json_add_u32(js, "cltv_expiry_delta", + payinfo->cltv_expiry_delta); + json_add_hex_talarr(js, "features", payinfo->features); + } + json_object_end(js); +} + +static void json_add_blinded_paths(struct json_stream *js, + struct blinded_path **paths, + struct blinded_payinfo **blindedpay) +{ + size_t n = 0; + json_array_start(js, "paths"); + for (size_t i = 0; i < tal_count(paths); i++) { + json_object_start(js, NULL); + json_add_pubkey(js, "blinding", &paths[i]->blinding); + json_array_start(js, "path"); + for (size_t j = 0; j < tal_count(paths[i]->path); j++) { + json_add_onionmsg_path(js, NULL, paths[i]->path[j], + n < tal_count(blindedpay) + ? blindedpay[n] : NULL); + n++; + } + json_array_end(js); + json_object_end(js); + } + json_array_end(js); + + /* BOLT-offers #12: + * - MUST reject the invoice if `blinded_payinfo` does not contain + * exactly as many `payinfo` as total `onionmsg_path` in + * `blinded_path`. + */ + if (blindedpay && n != tal_count(blindedpay)) + json_add_string(js, "warning_invoice_invalid_blinded_payinfo", + "invoice does not have correct number of blinded_payinfo"); +} + +static const char *recurrence_time_unit_name(u8 time_unit) +{ + /* BOLT-offers #12: + * `time_unit` defining 0 (seconds), 1 (days), 2 (months), 3 (years). + */ + switch (time_unit) { + case 0: + return "seconds"; + case 1: + return "days"; + case 2: + return "months"; + case 3: + return "years"; + } + return NULL; +} + +static void json_add_offer(struct json_stream *js, const struct tlv_offer *offer) +{ + struct sha256 offer_id; + + merkle_tlv(offer->fields, &offer_id); + json_add_sha256(js, "offer_id", &offer_id); + if (offer->chains) + json_add_chains(js, offer->chains); + if (offer->currency) { + const struct iso4217_name_and_divisor *iso4217; + json_add_stringn(js, "currency", + offer->currency, tal_bytelen(offer->currency)); + if (offer->amount) + json_add_u64(js, "amount", *offer->amount); + iso4217 = find_iso4217(offer->currency, + tal_bytelen(offer->currency)); + if (iso4217) + json_add_num(js, "minor_unit", iso4217->minor_unit); + else + json_add_string(js, "warning_offer_unknown_currency", + "unknown currency code"); + } else if (offer->amount) + json_add_amount_msat_only(js, "amount_msat", + amount_msat(*offer->amount)); + if (offer->send_invoice) + json_add_bool(js, "send_invoice", true); + if (offer->refund_for) + json_add_sha256(js, "refund_for", offer->refund_for); + + /* BOLT-offers #12: + * A reader of an offer: + *... + * - if `node_id`, `description` or `signature` is not set: + * - MUST NOT respond to the offer. + */ + if (offer->description) + json_add_stringn(js, "description", + offer->description, + tal_bytelen(offer->description)); + else + json_add_string(js, "warning_offer_missing_description", + "offers without a description are invalid"); + + if (offer->vendor) + json_add_stringn(js, "vendor", offer->vendor, + tal_bytelen(offer->vendor)); + if (offer->features) + json_add_hex_talarr(js, "features", offer->features); + if (offer->absolute_expiry) + json_add_u64(js, "absolute_expiry", + *offer->absolute_expiry); + if (offer->paths) + json_add_blinded_paths(js, offer->paths, NULL); + + if (offer->quantity_min) + json_add_u64(js, "quantity_min", *offer->quantity_min); + if (offer->quantity_max) + json_add_u64(js, "quantity_max", *offer->quantity_max); + if (offer->recurrence) { + const char *name; + json_object_start(js, "recurrence"); + json_add_num(js, "time_unit", offer->recurrence->time_unit); + name = recurrence_time_unit_name(offer->recurrence->time_unit); + if (name) + json_add_string(js, "time_unit_name", name); + json_add_num(js, "period", offer->recurrence->period); + if (offer->recurrence_base) { + json_add_u64(js, "basetime", + offer->recurrence_base->basetime); + if (offer->recurrence_base->start_any_period) + json_add_bool(js, "start_any_period", true); + } + if (offer->recurrence_limit) + json_add_u32(js, "limit", *offer->recurrence_limit); + if (offer->recurrence_paywindow) { + json_object_start(js, "paywindow"); + json_add_u32(js, "seconds_before", + offer->recurrence_paywindow->seconds_before); + json_add_u32(js, "seconds_after", + offer->recurrence_paywindow->seconds_after); + if (offer->recurrence_paywindow->proportional_amount) + json_add_bool(js, "proportional_amount", true); + json_object_end(js); + } + json_object_end(js); + } + + /* offer_decode fails if node_id or signature not set */ + json_add_pubkey32(js, "node_id", offer->node_id); + json_add_bip340sig(js, "signature", offer->signature); +} + +static void json_add_fallback_address(struct json_stream *js, + const struct chainparams *chain, + u8 version, const u8 *address) +{ + char out[73 + strlen(chain->bip173_name)]; + + /* Does extra checks, in particular checks v0 sizes */ + if (segwit_addr_encode(out, chain->bip173_name, version, + address, tal_bytelen(address))) + json_add_string(js, "address", out); + else + json_add_string(js, + "warning_invoice_fallbacks_address_invalid", + "invalid fallback address for this version"); +} + +static void json_add_fallbacks(struct json_stream *js, + const struct bitcoin_blkid *chains, + struct fallback_address **fallbacks) +{ + const struct chainparams *chain; + + /* Present address as first chain mentioned. */ + if (tal_count(chains) != 0) + chain = chainparams_by_chainhash(&chains[0]); + else + chain = chainparams_for_network("bitcoin"); + + json_array_start(js, "fallbacks"); + for (size_t i = 0; i < tal_count(fallbacks); i++) { + size_t addrlen = tal_bytelen(fallbacks[i]->address); + + json_object_start(js, NULL); + json_add_u32(js, "version", fallbacks[i]->version); + json_add_hex_talarr(js, "hex", fallbacks[i]->address); + + /* BOLT-offers #12: + * - for the bitcoin chain, if the invoice specifies `fallbacks`: + * - MUST ignore any `fallback_address` for which `version` is + * greater than 16. + * - MUST ignore any `fallback_address` for which `address` is + * less than 2 or greater than 40 bytes. + * - MUST ignore any `fallback_address` for which `address` does + * not meet known requirements for the given `version` + */ + if (fallbacks[i]->version > 16) { + json_add_string(js, + "warning_invoice_fallbacks_version_invalid", + "invoice fallback version > 16"); + } else if (addrlen < 2 || addrlen > 40) { + json_add_string(js, + "warning_invoice_fallbacks_address_invalid", + "invoice fallback address bad length"); + } else if (chain) { + json_add_fallback_address(js, chain, + fallbacks[i]->version, + fallbacks[i]->address); + } + json_object_end(js); + } + json_array_end(js); +} + +static void json_add_b12_invoice(struct json_stream *js, + const struct tlv_invoice *invoice) +{ + if (invoice->chains) + json_add_chains(js, invoice->chains); + if (invoice->offer_id) + json_add_sha256(js, "offer_id", invoice->offer_id); + + /* BOLT-offers #12: + * - MUST reject the invoice if `msat` is not present. + */ + if (invoice->amount) + json_add_amount_msat_only(js, "amount_msat", + amount_msat(*invoice->amount)); + else + json_add_string(js, "warning_invoice_missing_amount", + "invoices without an amount are invalid"); + + /* BOLT-offers #12: + * - MUST reject the invoice if `description` is not present. + */ + if (invoice->description) + json_add_stringn(js, "description", invoice->description, + tal_bytelen(invoice->description)); + else + json_add_string(js, "warning_invoice_missing_description", + "invoices without a description are invalid"); + if (invoice->vendor) + json_add_stringn(js, "vendor", invoice->vendor, + tal_bytelen(invoice->vendor)); + if (invoice->features) + json_add_hex_talarr(js, "features", invoice->features); + if (invoice->paths) { + /* BOLT-offers #12: + * - if `blinded_path` is present: + * - MUST reject the invoice if `blinded_payinfo` is not + * present. + * - MUST reject the invoice if `blinded_payinfo` does not + * contain exactly as many `payinfo` as total `onionmsg_path` + * in `blinded_path`. + */ + if (!invoice->blindedpay) + json_add_string(js, "warning_invoice_missing_blinded_payinfo", + "invoices with blinded_path without blinded_payindo are invalid"); + json_add_blinded_paths(js, invoice->paths, invoice->blindedpay); + } + if (invoice->quantity) + json_add_u64(js, "quantity", *invoice->quantity); + if (invoice->send_invoice) + json_add_bool(js, "send_invoice", true); + if (invoice->refund_for) + json_add_sha256(js, "refund_for", invoice->refund_for); + if (invoice->recurrence_counter) { + json_add_u32(js, "recurrence_counter", + *invoice->recurrence_counter); + if (invoice->recurrence_start) + json_add_u32(js, "recurrence_start", + *invoice->recurrence_start); + /* BOLT-offers #12: + * - if the offer contained `recurrence`: + * - MUST reject the invoice if `recurrence_basetime` is not + * set. + */ + if (invoice->recurrence_basetime) + json_add_u64(js, "recurrence_basetime", + *invoice->recurrence_basetime); + else + json_add_string(js, "warning_invoice_missing_recurrence_basetime", + "recurring invoices without a recurrence_basetime are invalid"); + } + + if (invoice->payer_key) + json_add_pubkey32(js, "payer_key", invoice->payer_key); + if (invoice->payer_info) + json_add_hex_talarr(js, "payer_info", invoice->payer_info); + + /* BOLT-offers #12: + * - MUST reject the invoice if `timestamp` is not present. + */ + if (invoice->timestamp) + json_add_u64(js, "timestamp", *invoice->timestamp); + else + json_add_string(js, "warning_invoice_missing_timestamp", + "invoices without a timestamp are invalid"); + + /* BOLT-offers #12: + * - MUST reject the invoice if `payment_hash` is not present. + */ + if (invoice->payment_hash) + json_add_sha256(js, "payment_hash", invoice->payment_hash); + else + json_add_string(js, "warning_invoice_missing_payment_hash", + "invoices without a payment_hash are invalid"); + + /* BOLT-offers #12: + * + * - if the expiry for accepting payment is not 7200 seconds after + * `timestamp`: + * - MUST set `relative_expiry` + */ + if (invoice->relative_expiry) + json_add_u32(js, "relative_expiry", *invoice->relative_expiry); + else + json_add_u32(js, "relative_expiry", 7200); + + /* BOLT-offers #12: + * - if the `min_final_cltv_expiry` for the last HTLC in the route is + * not 18: + * - MUST set `min_final_cltv_expiry`. + */ + if (invoice->cltv) + json_add_u32(js, "min_final_cltv_expiry", *invoice->cltv); + else + json_add_u32(js, "min_final_cltv_expiry", 18); + + if (invoice->fallbacks) + json_add_fallbacks(js, invoice->chains, + invoice->fallbacks->fallbacks); + + /* BOLT-offers #12: + * - if the offer contained `refund_for`: + * - MUST reject the invoice if `payer_key` does not match the invoice + * whose `payment_hash` is equal to `refund_for` + * `refunded_payment_hash` + * - MUST reject the invoice if `refund_signature` is not set. + * - MUST reject the invoice if `refund_signature` is not a valid + * signature using `payer_key` as described in + * [Signature Calculation](#signature-calculation). + */ + if (invoice->refund_signature) { + json_add_bip340sig(js, "refund_signature", + invoice->refund_signature); + if (!invoice->payer_key) + json_add_string(js, "warning_invoice_refund_signature_missing_payer_key", + "Can't have refund_signature without payer key"); + else if (!bolt12_check_signature(invoice->fields, + "invoice", + "refund_signature", + invoice->payer_key, + invoice->refund_signature)) + json_add_string(js, "warning_invoice_refund_signature_invalid", + "refund_signature does not match"); + } else if (invoice->refund_for) + json_add_string(js, "warning_invoice_refund_missing_signature", + "refund_for requires refund_signature"); + + /* invoice_decode checked these */ + json_add_pubkey32(js, "node_id", invoice->node_id); + json_add_bip340sig(js, "signature", invoice->signature); +} + +static void json_add_invoice_request(struct json_stream *js, + const struct tlv_invoice_request *invreq) +{ + if (invreq->chains) + json_add_chains(js, invreq->chains); + /* BOLT-offers #12: + * - MUST fail the request if `payer_key` is not present. + * - MUST fail the request if `chains` does not include (or imply) a supported chain. + * - MUST fail the request if `features` contains unknown even bits. + * - MUST fail the request if `offer_id` is not present. + */ + if (invreq->offer_id) + json_add_sha256(js, "offer_id", invreq->offer_id); + else + json_add_string(js, "warning_invoice_request_missing_offer_id", + "invoice_request requires offer_id"); + if (invreq->amount) + json_add_amount_msat_only(js, "amount_msat", + amount_msat(*invreq->amount)); + if (invreq->features) + json_add_hex_talarr(js, "features", invreq->features); + if (invreq->quantity) + json_add_u64(js, "quantity", *invreq->quantity); + + if (invreq->recurrence_counter) + json_add_u32(js, "recurrence_counter", + *invreq->recurrence_counter); + if (invreq->recurrence_start) + json_add_u32(js, "recurrence_start", + *invreq->recurrence_start); + if (invreq->payer_key) + json_add_pubkey32(js, "payer_key", invreq->payer_key); + else + json_add_string(js, "warning_invoice_request_missing_payer_key", + "invoice_request requires payer_key"); + if (invreq->payer_info) + json_add_hex_talarr(js, "payer_info", invreq->payer_info); + + /* BOLT-offers #12: + * - if the offer had a `recurrence`: + * - MUST fail the request if there is no `recurrence_counter` field. + * - MUST fail the request if there is no `recurrence_signature` field. + * - MUST fail the request if `recurrence_signature` is not correct. + */ + if (invreq->recurrence_signature) { + json_add_bip340sig(js, "recurrence_signature", + invreq->recurrence_signature); + if (invreq->payer_key + && !bolt12_check_signature(invreq->fields, + "invoice_request", + "recurrence_signature", + invreq->payer_key, + invreq->recurrence_signature)) + json_add_string(js, "warning_invoice_request_invalid_recurrence_signature", + "Bad recurrence_signature"); + } else if (invreq->recurrence_counter) { + json_add_string(js, "warning_invoice_request_missing_recurrence_signature", + "invoice_request requires recurrence_signature"); + } +} + +static struct command_result *json_decode(struct command *cmd, + const char *buffer, + const jsmntok_t *params) +{ + struct decodable *decodable = talz(cmd, struct decodable); + struct json_stream *response; + + if (!param(cmd, buffer, params, + p_req("string", param_decodable, decodable), + NULL)) + return command_param_failed(); + + response = jsonrpc_stream_success(cmd); + json_add_string(response, "type", decodable->type); + if (decodable->offer) + json_add_offer(response, decodable->offer); + if (decodable->invreq) + json_add_invoice_request(response, decodable->invreq); + if (decodable->invoice) + json_add_b12_invoice(response, decodable->invoice); + if (decodable->b11) + json_add_bolt11(response, decodable->b11); + return command_finished(cmd, response); +} + static void init(struct plugin *p, const char *buf UNUSED, const jsmntok_t *config UNUSED) @@ -144,6 +698,13 @@ static const struct plugin_command commands[] = { "Create an offer to pay invoices of {amount} with {description}, optional {vendor}, internal {label}, {absolute_expiry} and {refund_for}", json_offerout }, + { + "decode", + "utility", + "Decode {string} message, returning {type} and information.", + NULL, + json_decode, + }, }; int main(int argc, char *argv[])