diff --git a/devtools/Makefile b/devtools/Makefile index 17c75cd84..28bc6ab17 100644 --- a/devtools/Makefile +++ b/devtools/Makefile @@ -3,7 +3,7 @@ ifeq ($(HAVE_SQLITE3),1) DEVTOOLS += devtools/checkchannels endif ifeq ($(EXPERIMENTAL_FEATURES),1) -DEVTOOLS += devtools/blindedpath +DEVTOOLS += devtools/blindedpath devtools/bolt12-cli endif DEVTOOLS_TOOL_SRC := $(DEVTOOLS:=.c) devtools/print_wire.c devtools/clean_topo.c @@ -49,6 +49,8 @@ DEVTOOLS_COMMON_OBJS := \ devtools/bolt11-cli: $(DEVTOOLS_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) $(BITCOIN_OBJS) wire/fromwire.o wire/towire.o devtools/bolt11-cli.o +devtools/bolt12-cli: $(DEVTOOLS_COMMON_OBJS) $(CCAN_OBJS) $(BITCOIN_OBJS) wire/bolt12_exp_wiregen.o wire/fromwire.o wire/towire.o common/bolt12.o common/bolt12_merkle.o devtools/bolt12-cli.o common/setup.o common/iso4217.o + devtools/decodemsg: $(DEVTOOLS_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) $(BITCOIN_OBJS) $(WIRE_PRINT_OBJS) wire/fromwire.o wire/towire.o devtools/print_wire.o devtools/decodemsg.o devtools/dump-gossipstore: $(DEVTOOLS_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) $(BITCOIN_OBJS) wire/fromwire.o wire/towire.o devtools/dump-gossipstore.o gossipd/gossip_store_wiregen.o diff --git a/devtools/bolt12-cli.c b/devtools/bolt12-cli.c new file mode 100644 index 000000000..8c336c235 --- /dev/null +++ b/devtools/bolt12-cli.c @@ -0,0 +1,645 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NO_ERROR 0 +#define ERROR_BAD_DECODE 1 +#define ERROR_USAGE 3 + +static bool well_formed = true; + +/* Tal wrappers for opt. */ +static void *opt_allocfn(size_t size) +{ + return tal_arr_label(NULL, char, size, TAL_LABEL("opt_allocfn", "")); +} + +static void *tal_reallocfn(void *ptr, size_t size) +{ + if (!ptr) + return opt_allocfn(size); + tal_resize_(&ptr, 1, size, false); + return ptr; +} + +static void tal_freefn(void *ptr) +{ + tal_free(ptr); +} + +static char *fmt_time(const tal_t *ctx, u64 time) +{ + /* ctime is not sane. Take pointer, returns \n in string. */ + time_t t = time; + const char *p = ctime(&t); + + return tal_fmt(ctx, "%.*s", (int)strcspn(p, "\n"), p); +} + +static bool must_str(bool expected, const char *complaint, const char *fieldname) +{ + if (!expected) { + fprintf(stderr, "%s %s\n", complaint, fieldname); + well_formed = false; + return false; + } + return true; +} + +#define must_have(obj, field) \ + must_str((obj)->field != NULL, "Missing", stringify(field)) +#define must_not_have(obj, field) \ + must_str((obj)->field == NULL, "Unnecessary", stringify(field)) + +static void print_chains(const struct bitcoin_blkid *chains) +{ + printf("chains:"); + for (size_t i = 0; i < tal_count(chains); i++) { + printf(" %s", type_to_string(tmpctx, struct bitcoin_blkid, &chains[i])); + } + printf("\n"); +} + +static bool print_amount(const struct bitcoin_blkid *chains, + const char *iso4217, u64 amount) +{ + const char *currency; + unsigned int minor_unit; + bool ok = true; + + /* BOLT-offers #12: + * - if the currency for `amount` is that of the first entry in `chains`: + * - MUST specify `amount` in multiples of the minimum + * lightning-payable unit (e.g. milli-satoshis for bitcoin). + * - otherwise: + * - MUST specify `iso4217` as an ISO 4712 three-letter code. + * - MUST specify `amount` in the currency unit adjusted by the + * ISO 4712 exponent (e.g. USD cents). + */ + if (!iso4217) { + if (tal_count(chains) == 0) + currency = "bc"; + else { + const struct chainparams *ch; + + ch = chainparams_by_chainhash(&chains[0]); + if (!ch) { + currency = tal_fmt(tmpctx, "UNKNOWN CHAINHASH %s", + type_to_string(tmpctx, + struct bitcoin_blkid, + &chains[0])); + ok = false; + } else + currency = ch->bip173_name; + } + minor_unit = 11; + } else { + const struct iso4217_name_and_divisor *iso; + currency = tal_strndup(tmpctx, iso4217, tal_bytelen(iso4217)); + iso = find_iso4217(currency); + if (iso) + minor_unit = iso->minor_unit; + else { + minor_unit = 0; + currency = tal_fmt(tmpctx, "%s (UNKNOWN CURRENCY)", + currency); + ok = false; + } + } + + if (!minor_unit) + printf("amount: %"PRIu64"%s\n", amount, currency); + else { + u64 minor_div = 1; + for (size_t i = 0; i < minor_unit; i++) + minor_div *= 10; + printf("amount: %"PRIu64".%.*"PRIu64"%s\n", + amount / minor_div, minor_unit, amount % minor_div, + currency); + } + + return ok; +} + +static void print_description(const char *description) +{ + printf("description: %.*s\n", + (int)tal_bytelen(description), description); +} + +static void print_vendor(const char *vendor) +{ + printf("vendor: %.*s\n", (int)tal_bytelen(vendor), vendor); +} + +static void print_node_id(const struct pubkey32 *node_id) +{ + printf("node_id: %s\n", type_to_string(tmpctx, struct pubkey32, node_id)); +} + +static void print_quantity_min(u64 min) +{ + printf("quantity_min: %"PRIu64"\n", min); +} + +static void print_quantity_max(u64 max) +{ + printf("quantity_max: %"PRIu64"\n", max); +} + +static bool print_recurrance(const struct tlv_offer_recurrence *recurrence, + const struct tlv_offer_recurrence_paywindow *paywindow, + const u32 *limit, + const struct tlv_offer_recurrence_base *base) +{ + const char *unit; + bool ok = true; + + /* BOLT-offers #12: + * Thus, each payment has: + * 1. A `time_unit` defining 0 (seconds), 1 (days), 2 (months), + * 3 (years). + * 2. A `period`, defining how often (in `time_unit`) it has to be paid. + * 3. An optional `recurrence_limit` of total payments to be paid. + * 4. An optional `recurrence_base`: + * * `basetime`, defining when the first period starts + * in seconds since 1970-01-01 UTC. + * * `start_any_period` if non-zero, meaning you don't have to start + * paying at the period indicated by `basetime`, but can use + * `recurrence_start` to indicate what period you are starting at. + * 5. An optional `recurrence_paywindow`: + * * `seconds_before`, defining how many seconds prior to the start of + * the period a payment will be accepted. + * * `proportional_amount`, if set indicating that a payment made + * during the period itself will be charged proportionally to the + * remaining time in the period (e.g. 150 seconds into a 1500 + * second period gives a 10% discount). + * * `seconds_after`, defining how many seconds after the start of the + * period a payment will be accepted. + * If this field is missing, payment will be accepted during the prior + * period and the paid-for period. + */ + switch (recurrence->time_unit) { + case 0: + unit = "seconds"; + break; + case 1: + unit = "days"; + break; + case 2: + unit = "months"; + break; + case 3: + unit = "years"; + break; + default: + fprintf(stderr, "recurrence: unknown time_unit %u", recurrence->time_unit); + unit = ""; + ok = false; + } + printf("recurrence: every %u %s", recurrence->period, unit); + if (limit) + printf(" limit %u", *limit); + if (base) { + printf(" start %"PRIu64" (%s)", + base->basetime, + fmt_time(tmpctx, base->basetime)); + if (base->start_any_period) + printf(" (can start any period)"); + } + if (paywindow) { + printf(" paywindow -%u to +%u", + paywindow->seconds_before, paywindow->seconds_after); + if (paywindow->proportional_amount) + printf(" (pay proportional)"); + } + printf("\n"); + + return ok; +} + +static void print_absolute_expiry(u64 expiry) +{ + printf("absolute_expiry: %"PRIu64" (%s)\n", + expiry, fmt_time(tmpctx, expiry)); +} + +static void print_features(const u8 *features) +{ + printf("features:"); + for (size_t i = 0; i < tal_bytelen(features) * CHAR_BIT; i++) { + if (feature_is_set(features, i)) + printf(" %zu", i); + } + printf("\n"); +} + +static bool print_blindedpaths(struct blinded_path **paths, + struct blinded_payinfo **blindedpay) +{ + size_t bp_idx = 0; + + for (size_t i = 0; i < tal_count(paths); i++) { + struct onionmsg_path **p = paths[i]->path; + printf("blindedpath %zu/%zu: blinding %s", + i, tal_count(paths), + type_to_string(tmpctx, struct pubkey, + &paths[i]->blinding)); + printf("blindedpath %zu/%zu: path ", + i, tal_count(paths)); + for (size_t j = 0; j < tal_count(p); j++) { + printf(" %s:%s", + type_to_string(tmpctx, struct pubkey, + &p[j]->node_id), + tal_hex(tmpctx, p[j]->enctlv)); + if (blindedpay) { + if (bp_idx < tal_count(blindedpay)) + printf("fee=%u/%u,cltv=%u,features=%s", + blindedpay[bp_idx]->fee_base_msat, + blindedpay[bp_idx]->fee_proportional_millionths, + blindedpay[bp_idx]->cltv_expiry_delta, + tal_hex(tmpctx, + blindedpay[bp_idx]->features)); + bp_idx++; + } + } + printf("\n"); + } + if (blindedpay && tal_count(blindedpay) != bp_idx) { + fprintf(stderr, "Expected %zu blindedpay fields, got %zu\n", + bp_idx, tal_count(blindedpay)); + return false; + } + return true; +} + +static void print_send_invoice(void) +{ + printf("send_invoice\n"); +} + +static void print_refund_for(const struct sha256 *payment_hash) +{ + printf("refund_for: %s\n", + type_to_string(tmpctx, struct sha256, payment_hash)); +} + +static bool print_signature(const char *messagename, + const char *fieldname, + const struct tlv_field *fields, + const struct pubkey32 *node_id, + const struct bip340sig *sig) +{ + struct sha256 m, shash; + + /* No key, it's already invalid */ + if (!node_id) + return false; + + 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) { + fprintf(stderr, "%s: INVALID\n", fieldname); + return false; + } + printf("%s: %s\n", + fieldname, + type_to_string(tmpctx, struct bip340sig, sig)); + return true; +} + +static void print_offer_id(const struct sha256 *offer_id) +{ + printf("offer_id: %s\n", + type_to_string(tmpctx, struct sha256, offer_id)); +} + +static void print_quantity(u64 q) +{ + printf("quantity: %"PRIu64"\n", q); +} + +static void print_recurrence_counter(const u32 *recurrence_counter, + const u32 *recurrence_start) +{ + printf("recurrence_counter: %u", *recurrence_counter); + if (recurrence_start) + printf(" (start +%u)", *recurrence_start); + printf("\n"); +} + +static bool print_recurrence_counter_with_base(const u32 *recurrence_counter, + const u32 *recurrence_start, + const u64 *recurrence_base) +{ + if (!recurrence_base) { + fprintf(stderr, "Missing recurrence_base\n"); + return false; + } + printf("recurrence_counter: %u", *recurrence_counter); + if (recurrence_start) + printf(" (start +%u)", *recurrence_start); + printf(" (base %"PRIu64")\n", *recurrence_base); + return true; +} + +static void print_payer_key(const struct pubkey32 *payer_key, + const u8 *payer_info) +{ + printf("payer_key: %s", + type_to_string(tmpctx, struct pubkey32, payer_key)); + if (payer_info) + printf(" (payer_info %s)", tal_hex(tmpctx, payer_info)); + printf("\n"); +} + +static void print_timestamp(u64 timestamp) +{ + printf("timestamp: %"PRIu64" (%s)\n", + timestamp, fmt_time(tmpctx, timestamp)); +} + +static void print_payment_hash(const struct sha256 *payment_hash) +{ + printf("payment_hash: %s\n", + type_to_string(tmpctx, struct sha256, payment_hash)); +} + +static void print_cltv(u32 cltv) +{ + printf("min_final_cltv_expiry: %u\n", cltv); +} + +static void print_relative_expiry(u64 *timestamp, u32 *relative) +{ + /* Ignore if already malformed */ + if (!timestamp) + return; + + /* BOLT-offers #12: + * - if `relative_expiry` is present: + * - MUST reject the invoice if the current time since 1970-01-01 UTC + * is greater than `timestamp` plus `seconds_from_timestamp`. + * - otherwise: + * - MUST reject the invoice if the current time since 1970-01-01 UTC + * is greater than `timestamp` plus 7200. + */ + if (!relative) + printf("relative_expiry: %u (%s) (default)\n", 7200, + fmt_time(tmpctx, *timestamp + 7200)); + else + printf("relative_expiry: %u (%s)\n", *relative, + fmt_time(tmpctx, *timestamp + *relative)); +} + +static void print_fallbacks(const struct tlv_invoice_fallbacks *fallbacks) +{ + for (size_t i = 0; i < tal_count(fallbacks->fallbacks); i++) { + /* FIXME: format properly! */ + printf("fallback: %u %s\n", + fallbacks->fallbacks[i]->version, + tal_hex(tmpctx, fallbacks->fallbacks[i]->address)); + } +} + +static bool print_extra_fields(const struct tlv_field *fields) +{ + bool ok = true; + + for (size_t i = 0; i < tal_count(fields); i++) { + if (fields[i].meta) + continue; + if (fields[i].numtype % 2) { + printf("UNKNOWN EVEN field %"PRIu64": %s\n", + fields[i].numtype, + tal_hexstr(tmpctx, fields[i].value, fields[i].length)); + ok = false; + } else { + printf("Unknown field %"PRIu64": %s\n", + fields[i].numtype, + tal_hexstr(tmpctx, fields[i].value, fields[i].length)); + } + } + return ok; +} + +int main(int argc, char *argv[]) +{ + const tal_t *ctx = tal(NULL, char); + const char *method; + char *hrp; + u8 *data; + char *fail; + + common_setup(argv[0]); + + opt_set_alloc(opt_allocfn, tal_reallocfn, tal_freefn); + opt_register_noarg("--help|-h", opt_usage_and_exit, + " ", "Show this message"); + opt_register_version(); + + opt_early_parse(argc, argv, opt_log_stderr_exit); + opt_parse(&argc, argv, opt_log_stderr_exit); + + method = argv[1]; + if (!method) + errx(ERROR_USAGE, "Need at least one argument\n%s", + opt_usage(argv[0], NULL)); + + if (!streq(method, "decode")) + errx(ERROR_USAGE, "Need decode argument\n%s", + opt_usage(argv[0], NULL)); + + if (!argv[2]) + errx(ERROR_USAGE, "Need argument\n%s", + opt_usage(argv[0], NULL)); + + if (!from_bech32_charset(ctx, argv[2], strlen(argv[2]), &hrp, &data)) + errx(ERROR_USAGE, "Bad bech32 string\n%s", + opt_usage(argv[0], NULL)); + + if (streq(hrp, "lno")) { + const struct tlv_offer *offer + = offer_decode_nosig(ctx, argv[2], strlen(argv[2]), + NULL, NULL, &fail); + if (!offer) + errx(ERROR_BAD_DECODE, "Bad offer: %s", fail); + + if (offer->send_invoice) + print_send_invoice(); + if (offer->chains) + print_chains(offer->chains); + if (offer->refund_for) + print_refund_for(offer->refund_for); + if (offer->amount) + well_formed &= print_amount(offer->chains, + offer->currency, + *offer->amount); + if (must_have(offer, description)) + print_description(offer->description); + if (offer->vendor) + print_vendor(offer->vendor); + if (must_have(offer, node_id)) + print_node_id(offer->node_id); + if (offer->quantity_min) + print_quantity_min(*offer->quantity_min); + if (offer->quantity_max) + print_quantity_max(*offer->quantity_max); + if (offer->recurrence) + well_formed &= print_recurrance(offer->recurrence, + offer->recurrence_paywindow, + offer->recurrence_limit, + offer->recurrence_base); + if (offer->absolute_expiry) + print_absolute_expiry(*offer->absolute_expiry); + if (offer->features) + print_features(offer->features); + if (offer->paths) + print_blindedpaths(offer->paths, NULL); + if (must_have(offer, signature) && offer->node_id) + well_formed &= print_signature("offer", "signature", + offer->fields, + offer->node_id, + offer->signature); + if (!print_extra_fields(offer->fields)) + well_formed = false; + } else if (streq(hrp, "lnr")) { + const struct tlv_invoice_request *invreq + = invrequest_decode(ctx, argv[2], strlen(argv[2]), + NULL, NULL, &fail); + if (!invreq) + errx(ERROR_BAD_DECODE, "Bad invoice_request: %s", fail); + + if (invreq->chains) + print_chains(invreq->chains); + if (must_have(invreq, payer_key)) + print_payer_key(invreq->payer_key, invreq->payer_info); + if (must_have(invreq, offer_id)) + print_offer_id(invreq->offer_id); + if (must_have(invreq, amount)) + well_formed &= print_amount(invreq->chains, + NULL, + *invreq->amount); + if (invreq->features) + print_features(invreq->features); + if (invreq->quantity) + print_quantity(*invreq->quantity); + if (invreq->recurrence_counter) { + print_recurrence_counter(invreq->recurrence_counter, + invreq->recurrence_start); + if (must_have(invreq, recurrence_signature)) { + well_formed &= print_signature("invoice_request", + "recurrence_signature", + invreq->fields, + invreq->payer_key, + invreq->recurrence_signature); + } + } else { + must_not_have(invreq, recurrence_start); + must_not_have(invreq, recurrence_signature); + } + if (!print_extra_fields(invreq->fields)) + well_formed = false; + } else if (streq(hrp, "lni")) { + const struct tlv_invoice *invoice + = invoice_decode(ctx, argv[2], strlen(argv[2]), + NULL, NULL, &fail); + if (!invoice) + errx(ERROR_BAD_DECODE, "Bad invoice: %s", fail); + + if (invoice->chains) + print_chains(invoice->chains); + + if (invoice->offer_id) { + print_offer_id(invoice->offer_id); + } + if (must_have(invoice, amount)) + well_formed &= print_amount(invoice->chains, + NULL, + *invoice->amount); + if (must_have(invoice, description)) + print_description(invoice->description); + if (invoice->features) + print_features(invoice->features); + if (invoice->paths) { + must_have(invoice, blindedpay); + well_formed &= print_blindedpaths(invoice->paths, + invoice->blindedpay); + } else + must_not_have(invoice, blindedpay); + if (invoice->vendor) + print_vendor(invoice->vendor); + if (must_have(invoice, node_id)) + print_node_id(invoice->node_id); + if (invoice->quantity) + print_quantity(*invoice->quantity); + if (invoice->refund_for) { + print_refund_for(invoice->refund_for); + if (must_have(invoice, refund_signature)) + well_formed &= print_signature("invoice", + "refund_signature", + invoice->fields, + invoice->payer_key, + invoice->refund_signature); + } else { + must_not_have(invoice, refund_signature); + } + if (invoice->recurrence_counter) { + well_formed &= + print_recurrence_counter_with_base(invoice->recurrence_counter, + invoice->recurrence_start, + invoice->recurrence_basetime); + } else { + must_not_have(invoice, recurrence_start); + must_not_have(invoice, recurrence_basetime); + } + if (must_have(invoice, payer_key)) + print_payer_key(invoice->payer_key, invoice->payer_info); + if (must_have(invoice, timestamp)) + print_timestamp(*invoice->timestamp); + print_relative_expiry(invoice->timestamp, + invoice->relative_expiry); + if (must_have(invoice, payment_hash)) + print_payment_hash(invoice->payment_hash); + if (must_have(invoice, cltv)) + print_cltv(*invoice->cltv); + if (invoice->fallbacks) + print_fallbacks(invoice->fallbacks); + if (must_have(invoice, signature)) + well_formed &= print_signature("invoice", "signature", + invoice->fields, + invoice->node_id, + invoice->signature); + if (!print_extra_fields(invoice->fields)) + well_formed = false; + } else + errx(ERROR_BAD_DECODE, "Unknown prefix %s", hrp); + + tal_free(ctx); + common_shutdown(); + + if (well_formed) + return NO_ERROR; + else + return ERROR_BAD_DECODE; +}