diff --git a/Makefile b/Makefile index 0e8fcfdf21e9..67915d156cfb 100644 --- a/Makefile +++ b/Makefile @@ -408,6 +408,7 @@ include doc/Makefile include contrib/msggen/Makefile include devtools/Makefile include tools/Makefile +include contrib/libbolt12/Makefile ifneq ($(RUST),0) include cln-rpc/Makefile include cln-grpc/Makefile diff --git a/contrib/libbolt12/Makefile b/contrib/libbolt12/Makefile new file mode 100644 index 000000000000..2407baf13019 --- /dev/null +++ b/contrib/libbolt12/Makefile @@ -0,0 +1,57 @@ +# libbolt12 - Makefile fragment included by CLN's top-level Makefile. +# +# Produces: +# libbolt12.a thin wrapper; consumers link libcommon.a + +# libccan.a + libsecp256k1 alongside it +# libbolt12-fat.a self-contained archive (only needs +# -lsecp256k1 at link time) +# contrib/libbolt12/run-bolt12 unit test binary + +LIBBOLT12_SRC := contrib/libbolt12/bolt12_api.c +LIBBOLT12_OBJS := $(LIBBOLT12_SRC:.c=.o) +LIBBOLT12_HEADERS := \ + contrib/libbolt12/bolt12.h \ + contrib/libbolt12/bolt12_internal.h + +LIBBOLT12_TEST_SRC := contrib/libbolt12/run-bolt12.c +LIBBOLT12_TEST_OBJS := $(LIBBOLT12_TEST_SRC:.c=.o) + +# Thin wrapper library (just our wrapper code). +libbolt12.a: $(LIBBOLT12_OBJS) + @$(call VERBOSE, "ar libbolt12.a", $(AR) rcs $@ $(LIBBOLT12_OBJS)) + +# Fat archive: bundles libbolt12 + libcommon + libccan. External +# consumers still need -lsecp256k1 -lsodium -lwallycore -lm at link time +# because libcommon.a transitively references sodium/wally symbols from +# code paths libbolt12 itself doesn't use (psbt.c, script.c, randbytes.c +# etc.). A future refactor could extract a bolt12-only subset to drop +# those transitive deps. +libbolt12-fat.a: libbolt12.a libcommon.a libccan.a + @$(call VERBOSE, "ar libbolt12-fat.a", \ + rm -rf .libbolt12-tmp && mkdir .libbolt12-tmp && \ + cd .libbolt12-tmp && \ + $(AR) x ../libbolt12.a && \ + $(AR) x ../libcommon.a && \ + $(AR) x ../libccan.a && \ + $(AR) rcs ../libbolt12-fat.a *.o && \ + cd .. && rm -rf .libbolt12-tmp) + +# Test binary. +contrib/libbolt12/run-bolt12: $(LIBBOLT12_TEST_OBJS) libbolt12.a libcommon.a libccan.a + @$(call VERBOSE, "ld $@", \ + $(CC) $(CFLAGS) -o $@ $(LIBBOLT12_TEST_OBJS) \ + libbolt12.a libcommon.a libccan.a \ + $(EXTERNAL_LDLIBS) $(LDLIBS)) + +check-libbolt12: contrib/libbolt12/run-bolt12 + contrib/libbolt12/run-bolt12 + +ALL_C_SOURCES += $(LIBBOLT12_SRC) $(LIBBOLT12_TEST_SRC) +ALL_C_HEADERS += $(LIBBOLT12_HEADERS) + +clean: libbolt12-clean +libbolt12-clean: + $(RM) libbolt12.a libbolt12-fat.a + $(RM) $(LIBBOLT12_OBJS) $(LIBBOLT12_TEST_OBJS) + $(RM) contrib/libbolt12/run-bolt12 + $(RM) -rf .libbolt12-tmp diff --git a/contrib/libbolt12/README.md b/contrib/libbolt12/README.md new file mode 100644 index 000000000000..17484eacbf76 --- /dev/null +++ b/contrib/libbolt12/README.md @@ -0,0 +1,143 @@ +# libbolt12 + +Standalone C library for decoding and verifying BOLT12 offers and invoices, +built on top of Core Lightning's existing bolt12 implementation. + +**Status**: Experimental (RFC). API may change before stabilization. + +## Motivation + +External C programs (e.g. mining pool payout verification tools, hardware +wallets, embedded devices) need to decode and verify BOLT12 offers/invoices +without depending on Rust, running a full CLN node, or shelling out to +`lightning-cli decode`. libbolt12 provides that. + +## Features + +- Decode `lno1...` offers and `lni1...` invoices from bech32m strings +- Access all offer/invoice properties via clean accessor functions +- Verify invoice BIP-340 Schnorr signatures (merkle-root over TLV) +- Verify proof of payment (SHA256 preimage matches payment hash) +- Compare offer_id and signing pubkeys between offer and invoice +- All-in-one `bolt12_verify_offer_payment()` convenience entry point + +## Dependencies + +Runtime (link-time): +- libsecp256k1 (Schnorr signature verification) +- libsodium (transitively pulled in by libcommon.a) +- libwallycore (transitively pulled in by libcommon.a) +- libm + +libbolt12's own code does NOT use libsodium or libwally — it calls +`secp256k1_context_create()` directly instead of going through +`common_setup()`/wally, to avoid the global process-state mutations +(locale, env, progname) that `common_setup()` performs and to avoid +`common_setup()`'s `errx(1, ...)` failure mode that would kill the host +application. + +However, `libcommon.a` as a whole still transitively references sodium and +wally symbols (from files like `psbt.c`, `script.c`, `randbytes.c` that +libbolt12 does not use). A future refactor could build a trimmed +`libcommon-bolt12.a` containing only the object files libbolt12 actually +needs, removing the transitive libsodium/libwally dependency. For now the +fat archive still requires them at link time. + +## Building + +From the CLN source root: + +```sh +make libbolt12.a # thin archive (requires libcommon.a + libccan.a + # to be linked alongside it by consumers) +make libbolt12-fat.a # self-contained archive (only needs -lsecp256k1) +make check-libbolt12 # run the test suite +``` + +## Using the library + +### Linking + +With the fat archive (simpler for external consumers): + +```sh +cc -o myprog main.c \ + -I/path/to/cln/contrib/libbolt12 \ + /path/to/cln/libbolt12-fat.a \ + -lsecp256k1 -lsodium -lwallycore -lm +``` + +With the thin archive (if you already link the CLN archives): + +```sh +cc -o myprog main.c \ + -I/path/to/cln/contrib/libbolt12 \ + /path/to/cln/libbolt12.a \ + /path/to/cln/libcommon.a \ + /path/to/cln/libccan.a \ + -lsecp256k1 -lsodium -lwallycore -lm +``` + +### Example + +```c +#include "bolt12.h" +#include + +int main(void) { + if (bolt12_init() != 0) { + fprintf(stderr, "Failed to initialize libbolt12\n"); + return 1; + } + + bolt12_error_t err; + int rc = bolt12_verify_offer_payment( + "lno1pg7y7s69...", + "lni1qqg9sr0tna...", + "a71dceaa4f2b86713834d6362035adf0eb7eab6c6c61ae3c8b68baffd9072cfc", + &err); + + if (rc != 0) + fprintf(stderr, "Verification failed: %s\n", err.message); + else + printf("Payment verified successfully.\n"); + + bolt12_cleanup(); + return rc; +} +``` + +For per-field access: + +```c +bolt12_offer_t *offer = bolt12_offer_decode(offer_str, &err); +if (!offer) { /* handle err */ } + +char description[256]; +bolt12_offer_description(offer, description, sizeof(description)); + +uint64_t amount_msat; +if (bolt12_offer_amount(offer, &amount_msat) == 0) { + printf("Amount: %llu msat\n", (unsigned long long)amount_msat); +} + +bolt12_offer_free(offer); +``` + +See `bolt12.h` for the full API. + +## Thread safety + +**Not thread-safe.** libbolt12 uses process-global state (the libsecp256k1 +verification context and CLN's `tmpctx` tal root). Serialize all calls from +a single thread, or protect with an external mutex. + +## Testing + +```sh +make check-libbolt12 +``` + +The test suite uses real offer/invoice/preimage triplets generated by +Core Lightning and Phoenix, exercising the happy path and all error +branches (mismatched signing keys, wrong preimage, invalid input format). diff --git a/contrib/libbolt12/bolt12.h b/contrib/libbolt12/bolt12.h new file mode 100644 index 000000000000..e16d740d1af4 --- /dev/null +++ b/contrib/libbolt12/bolt12.h @@ -0,0 +1,407 @@ +/* libbolt12 - Standalone BOLT12 offer/invoice decode and verification library. + * + * Built on top of Core Lightning's bolt12 implementation. + * Provides a clean C API with no CLN internal types exposed. + * + * Usage: + * #include "bolt12.h" + * if (bolt12_init() != 0) { ... } + * int rc = bolt12_verify_offer_payment(offer_str, invoice_str, preimage_hex, &err); + * bolt12_cleanup(); + * + * Link against libbolt12.a + libcommon.a + libccan.a + -lsecp256k1 + * -lsodium -lwallycore -lm (or use the bundled libbolt12-fat.a which + * combines the three CLN archives). + * + * THREAD-SAFETY: libbolt12 is NOT thread-safe. It uses process-global + * state (secp256k1 context, tal temporary context). Serialize calls + * from a single thread, or protect with an external mutex. + * + * SPDX-License-Identifier: BSD-MIT + */ +#ifndef LIGHTNING_CONTRIB_LIBBOLT12_BOLT12_H +#define LIGHTNING_CONTRIB_LIBBOLT12_BOLT12_H +#include "config.h" + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* --- Opaque types --- */ +typedef struct bolt12_offer bolt12_offer_t; +typedef struct bolt12_invoice bolt12_invoice_t; + +/* --- Error handling --- */ +typedef struct { + int code; /* 0 = success, non-zero = error category */ + char message[512]; /* Human-readable error description */ +} bolt12_error_t; + +/* Error codes */ +#define BOLT12_OK 0 +#define BOLT12_ERR_DECODE 1 /* Failed to decode offer/invoice */ +#define BOLT12_ERR_SIGNATURE 2 /* Signature verification failed */ +#define BOLT12_ERR_MISMATCH 3 /* Offer/invoice fields don't match */ +#define BOLT12_ERR_PREIMAGE 4 /* Proof of payment failed */ +#define BOLT12_ERR_INIT 5 /* Library not initialized */ +#define BOLT12_ERR_MISSING 6 /* Required field missing */ + +/* --- Fixed-size public types --- */ +typedef struct { uint8_t data[32]; } bolt12_sha256_t; +typedef struct { uint8_t data[33]; } bolt12_pubkey_t; /* compressed secp256k1 */ +typedef struct { uint8_t data[64]; } bolt12_signature_t; /* BIP-340 schnorr */ + +/* --- Library lifecycle --- */ + +/** + * bolt12_init - Initialize the library. + * + * Must be called once before any other function. + * Initializes libsecp256k1, libsodium, and libwally contexts. + * + * Returns 0 on success, -1 on failure. + */ +int bolt12_init(void); + +/** + * bolt12_cleanup - Release library resources. + * + * Call when done with the library. After this, no other + * libbolt12 functions may be called (except bolt12_init again). + */ +void bolt12_cleanup(void); + +/* --- Decode --- */ + +/** + * bolt12_offer_decode - Decode a BOLT12 offer from "lno1..." bech32 string. + * + * @offer_str: the offer string (NUL-terminated). + * @err: if non-NULL and decode fails, populated with error details. + * + * Returns opaque offer handle, or NULL on failure. + * Caller must free with bolt12_offer_free(). + */ +bolt12_offer_t *bolt12_offer_decode(const char *offer_str, + bolt12_error_t *err); + +/** + * bolt12_offer_free - Free a decoded offer. + */ +void bolt12_offer_free(bolt12_offer_t *offer); + +/** + * bolt12_invoice_decode - Decode a BOLT12 invoice from "lni1..." bech32 string. + * + * @invoice_str: the invoice string (NUL-terminated). + * @err: if non-NULL and decode fails, populated with error details. + * + * Returns opaque invoice handle, or NULL on failure. + * Caller must free with bolt12_invoice_free(). + * + * Note: this performs structural validation and signature verification + * as per BOLT #12. + */ +bolt12_invoice_t *bolt12_invoice_decode(const char *invoice_str, + bolt12_error_t *err); + +/** + * bolt12_invoice_free - Free a decoded invoice. + */ +void bolt12_invoice_free(bolt12_invoice_t *invoice); + +/* --- Accessors: Offer --- */ + +/** + * bolt12_offer_id - Compute the offer_id (SHA256 of offer TLV fields). + * + * Returns 0 on success, -1 on failure. + */ +int bolt12_offer_id(const bolt12_offer_t *offer, bolt12_sha256_t *id); + +/** + * bolt12_offer_issuer_signing_pubkey - Get the explicit issuer_signing_pubkey. + * + * Returns 0 on success, -1 if not present. + */ +int bolt12_offer_issuer_signing_pubkey(const bolt12_offer_t *offer, + bolt12_pubkey_t *pubkey); + +/** + * bolt12_offer_path_signing_pubkey - Get signing pubkey from blinded paths. + * + * Falls back to the last hop's blinded_node_id of the first path. + * Returns 0 on success, -1 if no paths or empty paths. + */ +int bolt12_offer_path_signing_pubkey(const bolt12_offer_t *offer, + bolt12_pubkey_t *pubkey); + +/** + * bolt12_offer_effective_signing_pubkey - Get the "best" signing pubkey. + * + * Returns issuer_signing_pubkey if present, otherwise path_signing_pubkey. + * Returns 0 on success, -1 if neither is available. + */ +int bolt12_offer_effective_signing_pubkey(const bolt12_offer_t *offer, + bolt12_pubkey_t *pubkey); + +/** + * bolt12_offer_description - Get the offer description string. + * + * @offer: decoded offer. + * @out: buffer to write the NUL-terminated description into. + * @out_len: size of @out buffer. + * + * Returns the length of the description (excluding NUL), or -1 if not present. + * If the description is longer than out_len-1, it is truncated. + */ +int bolt12_offer_description(const bolt12_offer_t *offer, + char *out, size_t out_len); + +/** + * bolt12_offer_issuer - Get the offer issuer string. + * + * Returns the length of the issuer string (excluding NUL), or -1 if not present. + */ +int bolt12_offer_issuer(const bolt12_offer_t *offer, + char *out, size_t out_len); + +/** + * bolt12_offer_amount_msat - Get the offer amount in millisatoshi. + * + * @offer: decoded offer. + * @amount_msat: output. Set to the amount if present. + * + * Returns 0 on success, -1 if no amount is set. + * Note: if offer_currency is set, this is in that currency's minor unit, + * not millisatoshi. + */ +int bolt12_offer_amount(const bolt12_offer_t *offer, uint64_t *amount); + +/** + * bolt12_offer_currency - Get the offer currency (e.g. "USD"). + * + * Returns the length of the currency string (excluding NUL), or -1 if not present. + * If not present, the amount is in millisatoshi. + */ +int bolt12_offer_currency(const bolt12_offer_t *offer, + char *out, size_t out_len); + +/** + * bolt12_offer_absolute_expiry - Get the absolute expiry timestamp. + * + * Returns 0 on success, -1 if not present. + */ +int bolt12_offer_absolute_expiry(const bolt12_offer_t *offer, + uint64_t *expiry); + +/** + * bolt12_offer_quantity_max - Get the maximum quantity allowed. + * + * Returns 0 on success, -1 if not present (no quantity limit). + */ +int bolt12_offer_quantity_max(const bolt12_offer_t *offer, + uint64_t *quantity_max); + +/** + * bolt12_offer_chains - Get the chain hashes the offer is valid for. + * + * @offer: decoded offer. + * @chains: output array of 32-byte chain hashes. + * @max_chains: maximum number of chains to write. + * + * Returns the number of chains written, or -1 on error. + * Returns 0 if no chains are specified (implies bitcoin only). + */ +int bolt12_offer_chains(const bolt12_offer_t *offer, + bolt12_sha256_t *chains, size_t max_chains); + +/** + * bolt12_offer_num_paths - Get the number of blinded paths in the offer. + * + * Returns the count, or 0 if no paths. + */ +size_t bolt12_offer_num_paths(const bolt12_offer_t *offer); + +/* --- Accessors: Invoice --- */ + +/** + * bolt12_invoice_signing_pubkey - Get the invoice's node_id (signing pubkey). + * + * Returns 0 on success, -1 if not present. + */ +int bolt12_invoice_signing_pubkey(const bolt12_invoice_t *invoice, + bolt12_pubkey_t *pubkey); + +/** + * bolt12_invoice_offer_id - Get the offer_id embedded in the invoice. + * + * Returns 0 on success, -1 if not present. + */ +int bolt12_invoice_offer_id(const bolt12_invoice_t *invoice, + bolt12_sha256_t *id); + +/** + * bolt12_invoice_payment_hash - Get the payment hash. + * + * Returns 0 on success, -1 if not present. + */ +int bolt12_invoice_payment_hash(const bolt12_invoice_t *invoice, + bolt12_sha256_t *hash); + +/** + * bolt12_invoice_amount_msat - Get the invoice amount in millisatoshi. + * + * Returns 0 on success, -1 if not present. + */ +int bolt12_invoice_amount(const bolt12_invoice_t *invoice, uint64_t *amount); + +/** + * bolt12_invoice_description - Get the invoice description (inherited from offer). + * + * Returns the length of the description (excluding NUL), or -1 if not present. + */ +int bolt12_invoice_description(const bolt12_invoice_t *invoice, + char *out, size_t out_len); + +/** + * bolt12_invoice_created_at - Get the invoice creation timestamp. + * + * Returns 0 on success, -1 if not present. + */ +int bolt12_invoice_created_at(const bolt12_invoice_t *invoice, + uint64_t *created_at); + +/** + * bolt12_invoice_expiry - Get the absolute expiry timestamp. + * + * Combines invoice_created_at + invoice_relative_expiry (default 7200s). + * Returns UINT64_MAX if it would overflow. + * Returns 0 on success, -1 if invoice_created_at is not present. + */ +int bolt12_invoice_expiry(const bolt12_invoice_t *invoice, + uint64_t *expiry); + +/** + * bolt12_invoice_signature - Get the invoice BIP-340 signature. + * + * Returns 0 on success, -1 if not present. + */ +int bolt12_invoice_signature(const bolt12_invoice_t *invoice, + bolt12_signature_t *sig); + +/** + * bolt12_invoice_payer_note - Get the payer's note (if present). + * + * Returns the length of the note (excluding NUL), or -1 if not present. + */ +int bolt12_invoice_payer_note(const bolt12_invoice_t *invoice, + char *out, size_t out_len); + +/** + * bolt12_invoice_quantity - Get the quantity requested. + * + * Returns 0 on success, -1 if not present. + */ +int bolt12_invoice_quantity(const bolt12_invoice_t *invoice, + uint64_t *quantity); + +/** + * bolt12_invoice_num_paths - Get the number of blinded payment paths. + * + * Returns the count, or 0 if no paths. + */ +size_t bolt12_invoice_num_paths(const bolt12_invoice_t *invoice); + +/** + * bolt12_invoice_features - Get the raw feature bits. + * + * @invoice: decoded invoice. + * @out: buffer to write the feature bytes into. + * @out_len: size of @out buffer. + * + * Returns the number of feature bytes written, or -1 if not present. + */ +int bolt12_invoice_features(const bolt12_invoice_t *invoice, + uint8_t *out, size_t out_len); + +/* --- Verification --- */ + +/** + * bolt12_verify_invoice_signature - Verify the invoice's Schnorr signature. + * + * Verifies the BIP-340 signature over the merkle root of the invoice TLV, + * using the invoice_node_id as the public key. + * + * Returns 0 if valid, -1 on failure (err set if non-NULL). + */ +int bolt12_verify_invoice_signature(const bolt12_invoice_t *invoice, + bolt12_error_t *err); + +/** + * bolt12_verify_proof_of_payment - Verify that SHA256(preimage) == payment_hash. + * + * @invoice: decoded invoice. + * @preimage_hex: 64-character hex string (32 bytes). + * @err: populated on failure if non-NULL. + * + * Returns 0 if valid, -1 on failure. + */ +int bolt12_verify_proof_of_payment(const bolt12_invoice_t *invoice, + const char *preimage_hex, + bolt12_error_t *err); + +/** + * bolt12_compare_offer_id - Check that offer and invoice share the same offer_id. + * + * Returns 0 if they match, -1 if they don't (err set if non-NULL). + */ +int bolt12_compare_offer_id(const bolt12_offer_t *offer, + const bolt12_invoice_t *invoice, + bolt12_error_t *err); + +/** + * bolt12_compare_signing_pubkeys - Check that signing pubkeys match. + * + * Compares the offer's effective signing pubkey with the invoice's + * signing pubkey (invoice_node_id). + * + * Returns 0 if they match, -1 if they don't (err set if non-NULL). + */ +int bolt12_compare_signing_pubkeys(const bolt12_offer_t *offer, + const bolt12_invoice_t *invoice, + bolt12_error_t *err); + +/* --- Convenience: Full verification --- */ + +/** + * bolt12_verify_offer_payment - All-in-one offer payment verification. + * + * Performs the complete verification pipeline: + * 1. Decode offer and invoice + * 2. Compare signing pubkeys + * 3. Compare offer_ids + * 4. Verify invoice signature + * 5. Verify proof of payment (preimage) + * + * @offer_str: "lno1..." offer string. + * @invoice_str: "lni1..." invoice string. + * @preimage_hex: 64-char hex preimage string. + * @err: populated on first failure if non-NULL. + * + * Returns 0 if everything checks out, -1 on first failure. + */ +int bolt12_verify_offer_payment(const char *offer_str, + const char *invoice_str, + const char *preimage_hex, + bolt12_error_t *err); + +#ifdef __cplusplus +} +#endif + +#endif /* LIGHTNING_CONTRIB_LIBBOLT12_BOLT12_H */ diff --git a/contrib/libbolt12/bolt12_api.c b/contrib/libbolt12/bolt12_api.c new file mode 100644 index 000000000000..07e63e2f9294 --- /dev/null +++ b/contrib/libbolt12/bolt12_api.c @@ -0,0 +1,683 @@ +/* libbolt12 - Public API implementation. + * + * Wraps CLN's internal bolt12 functions behind a clean C interface. + * All CLN/tal types are hidden; the public API uses only standard C types. + * + * SPDX-License-Identifier: BSD-MIT + */ +#include "config.h" +#include "contrib/libbolt12/bolt12.h" +#include "contrib/libbolt12/bolt12_internal.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static bool libbolt12_initialized = false; + +/* We declare this here since common/utils.h declares it extern; libbolt12 + * owns the lifetime instead of relying on common_setup()/wally. */ +extern secp256k1_context *secp256k1_ctx; + +/* --- Library lifecycle --- */ + +/* Deliberately avoids common_setup(): + * - common_setup() mutates process-global state (locale, env, progname) + * which is surprising in a library context. + * - common_setup() calls errx(1, ...) on libsodium/libwally init failures, + * which would kill a host application. A library must never do that. + * We only need secp256k1_ctx (for verify) and tmpctx. That's it. */ +int bolt12_init(void) +{ + if (libbolt12_initialized) + return 0; + + secp256k1_ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY); + if (!secp256k1_ctx) + return -1; + + /* setup_tmpctx() asserts if tmpctx is already set, so only + * call it on the first init. A later cleanup()/init() cycle + * keeps the existing tmpctx (see bolt12_cleanup). */ + if (!tmpctx) + setup_tmpctx(); + libbolt12_initialized = true; + return 0; +} + +void bolt12_cleanup(void) +{ + if (!libbolt12_initialized) + return; + + /* Drop any transient allocations but leave tmpctx itself in + * place: CLN's convention (enforced by check-tmpctx) is that + * tmpctx is process-global and never freed. A subsequent + * bolt12_init() is therefore a no-op for tmpctx setup. */ + clean_tmpctx(); + + secp256k1_context_destroy(secp256k1_ctx); + secp256k1_ctx = NULL; + + libbolt12_initialized = false; +} + +/* --- Internal helpers --- */ + +/* Serialize a secp256k1_pubkey to 33-byte compressed form. */ +static void pubkey_to_compressed(const struct pubkey *pk, bolt12_pubkey_t *out) +{ + size_t len = 33; + secp256k1_ec_pubkey_serialize(secp256k1_ctx, out->data, &len, + &pk->pubkey, + SECP256K1_EC_COMPRESSED); +} + +/* Convert a hex character to its value. Returns -1 on invalid input. */ +static int hex_val(char c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + return -1; +} + +/* Decode a hex string into a byte buffer. Returns false on invalid input. */ +static bool hex_decode(const char *hex, size_t hexlen, + uint8_t *buf, size_t buflen) +{ + if (hexlen != buflen * 2) + return false; + + for (size_t i = 0; i < buflen; i++) { + int hi = hex_val(hex[i * 2]); + int lo = hex_val(hex[i * 2 + 1]); + if (hi < 0 || lo < 0) + return false; + buf[i] = (hi << 4) | lo; + } + return true; +} + +/* --- Decode --- */ + +bolt12_offer_t *bolt12_offer_decode(const char *offer_str, + bolt12_error_t *err) +{ + bolt12_offer_t *offer; + char *fail; + + if (!libbolt12_initialized) { + set_error(err, BOLT12_ERR_INIT, + "Library not initialized. Call bolt12_init() first."); + return NULL; + } + + offer = calloc(1, sizeof(*offer)); + if (!offer) { + set_error(err, BOLT12_ERR_DECODE, "Out of memory"); + return NULL; + } + + offer->ctx = tal(NULL, char); + + /* Pass NULL for features and chain to skip validation -- + * we only care about structural decode + field extraction. */ + offer->tlv = offer_decode(offer->ctx, offer_str, strlen(offer_str), + NULL, NULL, &fail); + if (!offer->tlv) { + set_error(err, BOLT12_ERR_DECODE, + "Failed to decode offer: %s", fail); + tal_free(offer->ctx); + free(offer); + return NULL; + } + + clean_tmpctx(); + return offer; +} + +void bolt12_offer_free(bolt12_offer_t *offer) +{ + if (!offer) + return; + tal_free(offer->ctx); + free(offer); +} + +bolt12_invoice_t *bolt12_invoice_decode(const char *invoice_str, + bolt12_error_t *err) +{ + bolt12_invoice_t *invoice; + char *fail; + + if (!libbolt12_initialized) { + set_error(err, BOLT12_ERR_INIT, + "Library not initialized. Call bolt12_init() first."); + return NULL; + } + + invoice = calloc(1, sizeof(*invoice)); + if (!invoice) { + set_error(err, BOLT12_ERR_DECODE, "Out of memory"); + return NULL; + } + + invoice->ctx = tal(NULL, char); + + /* Use invoice_decode which does full validation + signature check. + * Pass NULL for features/chain to skip those checks. */ + invoice->tlv = invoice_decode(invoice->ctx, invoice_str, + strlen(invoice_str), + NULL, NULL, &fail); + if (!invoice->tlv) { + set_error(err, BOLT12_ERR_DECODE, + "Failed to decode invoice: %s", fail); + tal_free(invoice->ctx); + free(invoice); + return NULL; + } + + clean_tmpctx(); + return invoice; +} + +void bolt12_invoice_free(bolt12_invoice_t *invoice) +{ + if (!invoice) + return; + tal_free(invoice->ctx); + free(invoice); +} + +/* --- Accessors: Offer --- */ + +int bolt12_offer_id(const bolt12_offer_t *offer, bolt12_sha256_t *id) +{ + struct sha256 sha; + + if (!offer || !offer->tlv || !id) + return -1; + + offer_offer_id(offer->tlv, &sha); + memcpy(id->data, sha.u.u8, 32); + + clean_tmpctx(); + return 0; +} + +int bolt12_offer_issuer_signing_pubkey(const bolt12_offer_t *offer, + bolt12_pubkey_t *pubkey) +{ + if (!offer || !offer->tlv || !pubkey) + return -1; + + if (!offer->tlv->offer_issuer_id) + return -1; + + pubkey_to_compressed(offer->tlv->offer_issuer_id, pubkey); + return 0; +} + +int bolt12_offer_path_signing_pubkey(const bolt12_offer_t *offer, + bolt12_pubkey_t *pubkey) +{ + if (!offer || !offer->tlv || !pubkey) + return -1; + + /* Walk the offer's blinded paths, find the last hop's + * blinded_node_id of the first path that has hops. */ + for (size_t i = 0; i < tal_count(offer->tlv->offer_paths); i++) { + const struct blinded_path *path = offer->tlv->offer_paths[i]; + size_t nhops = tal_count(path->path); + + if (nhops == 0) + continue; + + /* The last hop's blinded_node_id is used as the + * "effective" signing key when no issuer_id is present. */ + pubkey_to_compressed(&path->path[nhops - 1]->blinded_node_id, + pubkey); + return 0; + } + + return -1; +} + +int bolt12_offer_effective_signing_pubkey(const bolt12_offer_t *offer, + bolt12_pubkey_t *pubkey) +{ + if (bolt12_offer_issuer_signing_pubkey(offer, pubkey) == 0) + return 0; + return bolt12_offer_path_signing_pubkey(offer, pubkey); +} + +/* Helper to copy a tal UTF-8 string into a caller-provided buffer. */ +static int copy_tal_str(const char *src, char *out, size_t out_len) +{ + size_t len; + + if (!src) + return -1; + + len = tal_bytelen(src); + /* tal strings may or may not include a NUL, but UTF-8 fields + * from the wire are raw bytes without NUL. */ + if (out && out_len > 0) { + size_t copy = len < out_len - 1 ? len : out_len - 1; + memcpy(out, src, copy); + out[copy] = '\0'; + } + return (int)len; +} + +int bolt12_offer_description(const bolt12_offer_t *offer, + char *out, size_t out_len) +{ + if (!offer || !offer->tlv) + return -1; + return copy_tal_str(offer->tlv->offer_description, out, out_len); +} + +int bolt12_offer_issuer(const bolt12_offer_t *offer, + char *out, size_t out_len) +{ + if (!offer || !offer->tlv) + return -1; + return copy_tal_str(offer->tlv->offer_issuer, out, out_len); +} + +int bolt12_offer_amount(const bolt12_offer_t *offer, uint64_t *amount) +{ + if (!offer || !offer->tlv || !amount) + return -1; + if (!offer->tlv->offer_amount) + return -1; + *amount = *offer->tlv->offer_amount; + return 0; +} + +int bolt12_offer_currency(const bolt12_offer_t *offer, + char *out, size_t out_len) +{ + if (!offer || !offer->tlv) + return -1; + return copy_tal_str(offer->tlv->offer_currency, out, out_len); +} + +int bolt12_offer_absolute_expiry(const bolt12_offer_t *offer, + uint64_t *expiry) +{ + if (!offer || !offer->tlv || !expiry) + return -1; + if (!offer->tlv->offer_absolute_expiry) + return -1; + *expiry = *offer->tlv->offer_absolute_expiry; + return 0; +} + +int bolt12_offer_quantity_max(const bolt12_offer_t *offer, + uint64_t *quantity_max) +{ + if (!offer || !offer->tlv || !quantity_max) + return -1; + if (!offer->tlv->offer_quantity_max) + return -1; + *quantity_max = *offer->tlv->offer_quantity_max; + return 0; +} + +int bolt12_offer_chains(const bolt12_offer_t *offer, + bolt12_sha256_t *chains, size_t max_chains) +{ + size_t n; + + if (!offer || !offer->tlv) + return -1; + if (!offer->tlv->offer_chains) + return 0; /* No chains = bitcoin only */ + + n = tal_count(offer->tlv->offer_chains); + if (n > max_chains) + n = max_chains; + + /* FIXME: reaches into struct bitcoin_blkid layout. If CLN adds a + * stable bitcoin_blkid_to_bytes() helper we should use it instead. */ + for (size_t i = 0; i < n; i++) + memcpy(chains[i].data, + offer->tlv->offer_chains[i].shad.sha.u.u8, 32); + return (int)n; +} + +size_t bolt12_offer_num_paths(const bolt12_offer_t *offer) +{ + if (!offer || !offer->tlv || !offer->tlv->offer_paths) + return 0; + return tal_count(offer->tlv->offer_paths); +} + +/* --- Accessors: Invoice --- */ + +int bolt12_invoice_signing_pubkey(const bolt12_invoice_t *invoice, + bolt12_pubkey_t *pubkey) +{ + if (!invoice || !invoice->tlv || !pubkey) + return -1; + + if (!invoice->tlv->invoice_node_id) + return -1; + + pubkey_to_compressed(invoice->tlv->invoice_node_id, pubkey); + return 0; +} + +int bolt12_invoice_offer_id(const bolt12_invoice_t *invoice, + bolt12_sha256_t *id) +{ + struct sha256 sha; + + if (!invoice || !invoice->tlv || !id) + return -1; + + /* The invoice contains the offer fields; compute offer_id + * from them the same way CLN does. */ + invoice_offer_id(invoice->tlv, &sha); + memcpy(id->data, sha.u.u8, 32); + + clean_tmpctx(); + return 0; +} + +int bolt12_invoice_payment_hash(const bolt12_invoice_t *invoice, + bolt12_sha256_t *hash) +{ + if (!invoice || !invoice->tlv || !hash) + return -1; + + if (!invoice->tlv->invoice_payment_hash) + return -1; + + memcpy(hash->data, invoice->tlv->invoice_payment_hash->u.u8, 32); + return 0; +} + +int bolt12_invoice_amount(const bolt12_invoice_t *invoice, uint64_t *amount) +{ + if (!invoice || !invoice->tlv || !amount) + return -1; + if (!invoice->tlv->invoice_amount) + return -1; + *amount = *invoice->tlv->invoice_amount; + return 0; +} + +int bolt12_invoice_description(const bolt12_invoice_t *invoice, + char *out, size_t out_len) +{ + if (!invoice || !invoice->tlv) + return -1; + return copy_tal_str(invoice->tlv->offer_description, out, out_len); +} + +int bolt12_invoice_created_at(const bolt12_invoice_t *invoice, + uint64_t *created_at) +{ + if (!invoice || !invoice->tlv || !created_at) + return -1; + if (!invoice->tlv->invoice_created_at) + return -1; + *created_at = *invoice->tlv->invoice_created_at; + return 0; +} + +int bolt12_invoice_expiry(const bolt12_invoice_t *invoice, uint64_t *expiry) +{ + if (!invoice || !invoice->tlv || !expiry) + return -1; + /* Reuse CLN's invoice_expiry() which handles + * relative_expiry and overflow correctly. */ + *expiry = invoice_expiry(invoice->tlv); + return 0; +} + +int bolt12_invoice_signature(const bolt12_invoice_t *invoice, + bolt12_signature_t *sig) +{ + if (!invoice || !invoice->tlv || !sig) + return -1; + if (!invoice->tlv->signature) + return -1; + memcpy(sig->data, invoice->tlv->signature->u8, 64); + return 0; +} + +int bolt12_invoice_payer_note(const bolt12_invoice_t *invoice, + char *out, size_t out_len) +{ + if (!invoice || !invoice->tlv) + return -1; + return copy_tal_str(invoice->tlv->invreq_payer_note, out, out_len); +} + +int bolt12_invoice_quantity(const bolt12_invoice_t *invoice, + uint64_t *quantity) +{ + if (!invoice || !invoice->tlv || !quantity) + return -1; + if (!invoice->tlv->invreq_quantity) + return -1; + *quantity = *invoice->tlv->invreq_quantity; + return 0; +} + +size_t bolt12_invoice_num_paths(const bolt12_invoice_t *invoice) +{ + if (!invoice || !invoice->tlv || !invoice->tlv->invoice_paths) + return 0; + return tal_count(invoice->tlv->invoice_paths); +} + +int bolt12_invoice_features(const bolt12_invoice_t *invoice, + uint8_t *out, size_t out_len) +{ + size_t len; + + if (!invoice || !invoice->tlv) + return -1; + if (!invoice->tlv->invoice_features) + return -1; + + len = tal_bytelen(invoice->tlv->invoice_features); + if (len > out_len) + len = out_len; + if (out && len > 0) + memcpy(out, invoice->tlv->invoice_features, len); + return (int)len; +} + +/* --- Verification --- */ + +int bolt12_verify_invoice_signature(const bolt12_invoice_t *invoice, + bolt12_error_t *err) +{ + if (!invoice || !invoice->tlv) { + set_error(err, BOLT12_ERR_MISSING, "No invoice provided"); + return -1; + } + + if (!invoice->tlv->invoice_node_id) { + set_error(err, BOLT12_ERR_MISSING, + "Invoice missing invoice_node_id"); + return -1; + } + + if (!invoice->tlv->signature) { + set_error(err, BOLT12_ERR_MISSING, + "Invoice missing signature"); + return -1; + } + + if (!bolt12_check_signature(invoice->tlv->fields, + "invoice", "signature", + invoice->tlv->invoice_node_id, + invoice->tlv->signature)) { + set_error(err, BOLT12_ERR_SIGNATURE, + "Invoice signature verification failed"); + return -1; + } + + return 0; +} + +int bolt12_verify_proof_of_payment(const bolt12_invoice_t *invoice, + const char *preimage_hex, + bolt12_error_t *err) +{ + uint8_t preimage[32]; + struct sha256 computed_hash; + size_t hexlen; + + if (!invoice || !invoice->tlv) { + set_error(err, BOLT12_ERR_MISSING, "No invoice provided"); + return -1; + } + + if (!invoice->tlv->invoice_payment_hash) { + set_error(err, BOLT12_ERR_MISSING, + "Invoice missing payment_hash"); + return -1; + } + + if (!preimage_hex) { + set_error(err, BOLT12_ERR_PREIMAGE, "No preimage provided"); + return -1; + } + + hexlen = strlen(preimage_hex); + if (!hex_decode(preimage_hex, hexlen, preimage, sizeof(preimage))) { + set_error(err, BOLT12_ERR_PREIMAGE, + "Invalid preimage: must be 64 hex chars (32 bytes), " + "got %zu chars", hexlen); + return -1; + } + + /* SHA256(preimage) should equal the invoice payment_hash */ + sha256(&computed_hash, preimage, sizeof(preimage)); + + if (memcmp(computed_hash.u.u8, + invoice->tlv->invoice_payment_hash->u.u8, 32) != 0) { + set_error(err, BOLT12_ERR_PREIMAGE, + "Proof of payment failed: SHA256(preimage) does not " + "match invoice payment_hash"); + return -1; + } + + return 0; +} + +int bolt12_compare_offer_id(const bolt12_offer_t *offer, + const bolt12_invoice_t *invoice, + bolt12_error_t *err) +{ + bolt12_sha256_t offer_id, invoice_oid; + + if (bolt12_offer_id(offer, &offer_id) != 0) { + set_error(err, BOLT12_ERR_MISSING, + "Failed to compute offer_id from offer"); + return -1; + } + + if (bolt12_invoice_offer_id(invoice, &invoice_oid) != 0) { + set_error(err, BOLT12_ERR_MISSING, + "Invoice does not contain offer fields"); + return -1; + } + + if (memcmp(offer_id.data, invoice_oid.data, 32) != 0) { + set_error(err, BOLT12_ERR_MISMATCH, + "Offer ID and invoice offer ID do not match"); + return -1; + } + + return 0; +} + +int bolt12_compare_signing_pubkeys(const bolt12_offer_t *offer, + const bolt12_invoice_t *invoice, + bolt12_error_t *err) +{ + bolt12_pubkey_t offer_pk, invoice_pk; + + if (bolt12_offer_effective_signing_pubkey(offer, &offer_pk) != 0) { + set_error(err, BOLT12_ERR_MISSING, + "Offer has no signing pubkey (no issuer_id or paths)"); + return -1; + } + + if (bolt12_invoice_signing_pubkey(invoice, &invoice_pk) != 0) { + set_error(err, BOLT12_ERR_MISSING, + "Invoice missing invoice_node_id"); + return -1; + } + + if (memcmp(offer_pk.data, invoice_pk.data, 33) != 0) { + set_error(err, BOLT12_ERR_MISMATCH, + "Offer and invoice signing pubkeys do not match"); + return -1; + } + + return 0; +} + +/* --- Convenience: Full verification --- */ + +int bolt12_verify_offer_payment(const char *offer_str, + const char *invoice_str, + const char *preimage_hex, + bolt12_error_t *err) +{ + bolt12_offer_t *offer = NULL; + bolt12_invoice_t *invoice = NULL; + int rc = -1; + + offer = bolt12_offer_decode(offer_str, err); + if (!offer) + goto out; + + invoice = bolt12_invoice_decode(invoice_str, err); + if (!invoice) + goto out; + + /* Step 1: Compare signing pubkeys */ + if (bolt12_compare_signing_pubkeys(offer, invoice, err) != 0) + goto out; + + /* Step 2: Compare offer_ids */ + if (bolt12_compare_offer_id(offer, invoice, err) != 0) + goto out; + + /* Step 3: Verify invoice signature (redundant since invoice_decode + * already checks this, but included for completeness) */ + if (bolt12_verify_invoice_signature(invoice, err) != 0) + goto out; + + /* Step 4: Verify proof of payment */ + if (bolt12_verify_proof_of_payment(invoice, preimage_hex, err) != 0) + goto out; + + rc = 0; + +out: + bolt12_invoice_free(invoice); + bolt12_offer_free(offer); + return rc; +} diff --git a/contrib/libbolt12/bolt12_internal.h b/contrib/libbolt12/bolt12_internal.h new file mode 100644 index 000000000000..21efb1d59219 --- /dev/null +++ b/contrib/libbolt12/bolt12_internal.h @@ -0,0 +1,47 @@ +/* Internal types for libbolt12 -- not part of the public API. + * + * Self-sufficient: pulls in common/bolt12.h (which pulls in + * wire/bolt12_wiregen.h) so struct tlv_offer / struct tlv_invoice + * are fully defined for the opaque wrappers below. + */ +#ifndef LIGHTNING_CONTRIB_LIBBOLT12_BOLT12_INTERNAL_H +#define LIGHTNING_CONTRIB_LIBBOLT12_BOLT12_INTERNAL_H +#include "config.h" + +#include "contrib/libbolt12/bolt12.h" +#include +#include +#include + +/* Opaque wrapper for a decoded BOLT12 offer. + * Holds a tal context owning all internal allocations. + * Requires wire/bolt12_wiregen.h for struct tlv_offer. */ +struct bolt12_offer { + tal_t *ctx; /* Root of the tal tree */ + struct tlv_offer *tlv; /* Parsed TLV, allocated under ctx */ +}; + +/* Opaque wrapper for a decoded BOLT12 invoice. + * Holds a tal context owning all internal allocations. + * Requires wire/bolt12_wiregen.h for struct tlv_invoice. */ +struct bolt12_invoice { + tal_t *ctx; /* Root of the tal tree */ + struct tlv_invoice *tlv; /* Parsed TLV, allocated under ctx */ +}; + +/* Set a bolt12_error_t if non-NULL. */ +static inline void set_error(bolt12_error_t *err, int code, const char *fmt, ...) + __attribute__((format(printf, 3, 4))); + +static inline void set_error(bolt12_error_t *err, int code, const char *fmt, ...) +{ + if (!err) + return; + err->code = code; + va_list ap; + va_start(ap, fmt); + vsnprintf(err->message, sizeof(err->message), fmt, ap); + va_end(ap); +} + +#endif /* LIGHTNING_CONTRIB_LIBBOLT12_BOLT12_INTERNAL_H */ diff --git a/contrib/libbolt12/run-bolt12.c b/contrib/libbolt12/run-bolt12.c new file mode 100644 index 000000000000..4a0b430fa888 --- /dev/null +++ b/contrib/libbolt12/run-bolt12.c @@ -0,0 +1,265 @@ +/* libbolt12 test suite. + * + * Uses the same test vectors as ocean-offer-cli/src/verify.rs. + * SPDX-License-Identifier: BSD-MIT + */ +#include "config.h" +#include "contrib/libbolt12/bolt12.h" +#include +#include +#include +#include + +static int tests_run = 0; +static int tests_passed = 0; + +#define ASSERT_EQ(a, b, msg) do { \ + tests_run++; \ + if ((a) == (b)) { tests_passed++; } \ + else { \ + fprintf(stderr, "FAIL [%s:%d]: %s (expected %d, got %d)\n", \ + __FILE__, __LINE__, msg, (int)(b), (int)(a)); \ + } \ +} while(0) + +#define ASSERT_NEQ(a, b, msg) do { \ + tests_run++; \ + if ((a) != (b)) { tests_passed++; } \ + else { \ + fprintf(stderr, "FAIL [%s:%d]: %s (should not be %d)\n", \ + __FILE__, __LINE__, msg, (int)(a)); \ + } \ +} while(0) + +/* Test vectors from ocean-offer-cli/src/verify.rs */ + +/* A valid offer/invoice/preimage tuple (CLN-generated). */ +static const char *VALID_OFFER = + "lno1pg7y7s69g98zq5rp09hh2arnypnx7u3qvf3nzutc8q6xcdphve4r2emjvucrsdejwqmkv73cvymnxmthw3cngcmnvcmrgum5d4j3vggrufqg5j0s05h5pqaywdzp8rhcnemp0e3eryszey4234ym2a99vzhq"; + +static const char *VALID_INVOICE = + "lni1qqg9sr0tna8ljw0tp9zk9uehh7s8vz3ufap52s2wypgxz7t0w468xgrxdaezqcnrx9chswp5ds6rwen2x4nhyees8qmnyuphvearscfhxdkhwar3x33hxe3kx3ehgmt9zcss8cjq3fylqlf0gzp6gu6yzw8038nkzlnrjxfq9jf24r2fk4622c9w2gpsz28qtqssxstnfpsaqtgdhchfv70shwvganrwuk28gwz9f6v5nlt37s0xh7hp5zvq8cjq3fylqlf0gzp6gu6yzw8038nkzlnrjxfq9jf24r2fk4622c9wq27sqsf52x7pdt5432aztt8ee3s5l20g3u0whwudkk5asanadjzz5qgzhgehdw5jyjf6m83awzntjkxykywzxycduph6gp5crv29qjf9gsasqv394jhcqgta9q4cr975hw4vl5nzekvuzujxv0u7rngsse707e0pq4duexv3930unvay593kd38t6z8em29zrsqqqqqqqqqqqqqqzgqqqqqqqqqqqqqayjedltzjqqqqqq9yq359nfhm4qst3qczk7fkkjhhqs6syjuygh3q87t8jsmtg04xvzu6vsys3fggzt92qvqj3c9wqvpqqq9syyp7ysy2f8c86t6qswj8x3qn3mufuashucu3jgpvj24g6jd4wjjkpthsgqtas84k74uvj2uvxh32exre34mu4vsc5cnmqpftru4cw0s6wujsk8ggn30v3jll8rn98r3xmlymy850udw2smfmse0amens93mk6fzt"; + +static const char *VALID_PREIMAGE = + "a71dceaa4f2b86713834d6362035adf0eb7eab6c6c61ae3c8b68baffd9072cfc"; + +/* A Phoenix wallet offer (different issuer). */ +static const char *PHOENIX_OFFER = + "lno1pgd57cm9v9hzqnmxvejhygzrd35jqanpd35kgct5d9hkugqsacpcvnhsyh77376c0kvfrpkwdf9ps6y4aez2jf4lcdcw9smxt9arlrczf6ycmt3ftr363p6f3fm08epd84y0lkz4t5zphpuygjqnwxzklltqyqnv4q7rx9lc4k8zjcyy7jdxaupjyuhfu7j7jkrdszh9xah04npkysqrxka4j4yxve6j8czdzcr56f5m5hku3uy0zlqn3genn8pszptkms5u6vv6u7qjej4sg4r00r8lpkeuk9allsgz2gqhm8qmj9cuwcfttex5366yvcma274gtaysskp5nmxrl9h3gsdsqv38tquert0z9py4uadrnuceanv26ytqw2pwys6909szlpw562u5lw8gv0ne7jnz52w9903vfv28pdpswrq"; + +static const char *PHOENIX_INVOICE = + "lni1qqgpllwtmnmv6xspe70m78ptxrr5vzsmfa3k2ctwyp8kven9wgsyxmrfypmxzmrfv3shg6t0dcsppmsrse80qf0aara4slvcjxrvu6j2rp5ftmjy4yntlsmsutpkvkt6878syn5f3khzjk8r4zr5nznk70jz602gllv92hgyrwrcg3ypxuv9dl7kqgpxe2puxvtl3tvw99sgfay6dmcryfewnea9a9vxmq9w2dmwltxrvfqqxddmt92gven4y0sy69s8f5nfhf0dercg797p8z3n8xwrqyzhdhpfe5ce4eup9n9tq32x77x07rdnevtmllqsy5sp0kwphyt3casjkhjdfr45ge3h64a2sh6fppvrf8kv87t0z3qmqqezwkpejxk7y2zfte6688e3nmxc45gkqu5zufp527tq97zaf54ef7uwscl8na9x9g5u22lzcjc5wz6rqux9yqc0gfq9gqzcyypaauxsjgg80qzvfrysks88du2s78vme6neec3jt5axdcrj4yj8a99ql5qm2quxfmcztl0gldv8mxy3sm8x5jscdz27u39fy6luxu8zcdn9j73l3upfh49ulj5edehzplt3t9fyw9m2j63x9nmey3r8204yfqpj0y5zzlqzqv5wvsgcqc463wtwd2npxp2t953yqp5vj7j829em3apsnt56chhx2qz9rl9mszedhld2xpuzgthfx007d585x4nfxtdefz74f355mua8wwnnr0weqkgeyj8fa72saljsa0kjzhys9w3dt60k9jxqjddkttw5x50l99smn9grg39v0up6s83lvf5x3nxs3knpvm7qpz8vtl7gj78q9jv7yt8cw0nqpecrvfcy4a0tq2z7560lys4tzp3j2586awqv2pgm66plh4a0q73jw3fq3yzl0tje4u5emck0p3x5p5w9gr74xshtkplmk9p68qwa6uz4m3ez4gv35ldgnl3zytpnm6fszejm4rk4f8g6vrknh7cuas5qq6tgt3f0grshlxxzkcyyzm6jp60p9mym870h2nuk9cl2cz3urvd0qksyegf6lq6gquzy0xumkwge00066l8yyss5wh44hz7vr5nssx0ywst5ju5escm85qwyv4jjs8qdlg2llun2zfymp4qqu8u30avlt52879nsfwgvskvvv3hrmggelcjysxnegcgfxexeaz2k6zttq9vdf4pwfy7qfwcnf88j5gwqqqqraqqqqqryqysqqqqqqqqqqqlgqqqqqqqqr6zgqqqq5szxshfdu6nqxq23sz5zqcu908d9dzgtmzva3vh28mpjz4hggyja3r8d48x6cuhxky628xjp4gps7sjq4cpsyqqqkqssy5sp0kwphyt3casjkhjdfr45ge3h64a2sh6fppvrf8kv87t0z3qm7pqx6pt4td9rrz7ek6gpfzner0dmq9zz92md57cnee4mfv7mktgjj7c3vqn66pdzy80fzgu9sarhtdgd3sy6fl0pzq2dac6m5p87qd393q"; + +/* --- Test cases --- */ + +static void test_valid_verification(void) +{ + bolt12_error_t err; + int rc; + + printf(" test_valid_verification... "); + rc = bolt12_verify_offer_payment(VALID_OFFER, VALID_INVOICE, + VALID_PREIMAGE, &err); + ASSERT_EQ(rc, 0, "Valid payment should verify"); + if (rc == 0) + printf("OK\n"); + else + printf("FAIL: %s\n", err.message); +} + +static void test_mismatched_offer_invoice(void) +{ + bolt12_error_t err; + int rc; + + printf(" test_mismatched_offer_invoice... "); + /* Phoenix offer + valid CLN invoice -- should fail on signing pubkey + * or offer_id mismatch. */ + rc = bolt12_verify_offer_payment(PHOENIX_OFFER, VALID_INVOICE, + VALID_PREIMAGE, &err); + ASSERT_NEQ(rc, 0, "Mismatched offer/invoice should fail"); + if (rc != 0) + printf("OK (expected error: %s)\n", err.message); + else + printf("FAIL: should have errored\n"); +} + +static void test_bad_preimage(void) +{ + bolt12_error_t err; + int rc; + + printf(" test_bad_preimage... "); + /* Valid offer + valid invoice, but wrong preimage. */ + rc = bolt12_verify_offer_payment(VALID_OFFER, VALID_INVOICE, + "0000000000000000000000000000000000000000000000000000000000000000", + &err); + ASSERT_NEQ(rc, 0, "Bad preimage should fail"); + if (rc != 0) + printf("OK (expected error: %s)\n", err.message); + else + printf("FAIL: should have errored\n"); +} + +static void test_decode_offer(void) +{ + bolt12_error_t err; + bolt12_offer_t *offer; + bolt12_sha256_t id; + bolt12_pubkey_t pk; + + printf(" test_decode_offer... "); + offer = bolt12_offer_decode(VALID_OFFER, &err); + ASSERT_NEQ((intptr_t)offer, 0, "Should decode valid offer"); + if (!offer) { + printf("FAIL: %s\n", err.message); + return; + } + + ASSERT_EQ(bolt12_offer_id(offer, &id), 0, "Should compute offer_id"); + ASSERT_EQ(bolt12_offer_effective_signing_pubkey(offer, &pk), 0, + "Should have signing pubkey"); + + /* Test property accessors */ + char desc[256]; + int desc_len = bolt12_offer_description(offer, desc, sizeof(desc)); + ASSERT_NEQ(desc_len, -1, "Should have description"); + if (desc_len >= 0) + printf("\n description: \"%s\" (%d bytes)\n", desc, desc_len); + + size_t npaths = bolt12_offer_num_paths(offer); + printf(" paths: %zu\n", npaths); + + /* Currency should not be present (msat denomination) */ + char currency[8]; + int curr_len = bolt12_offer_currency(offer, currency, sizeof(currency)); + ASSERT_EQ(curr_len, -1, "Should not have currency (msat)"); + + bolt12_offer_free(offer); + printf(" ... OK\n"); +} + +static void test_decode_invoice(void) +{ + bolt12_error_t err; + bolt12_invoice_t *invoice; + bolt12_sha256_t hash; + bolt12_pubkey_t pk; + + printf(" test_decode_invoice... "); + invoice = bolt12_invoice_decode(VALID_INVOICE, &err); + ASSERT_NEQ((intptr_t)invoice, 0, "Should decode valid invoice"); + if (!invoice) { + printf("FAIL: %s\n", err.message); + return; + } + + ASSERT_EQ(bolt12_invoice_payment_hash(invoice, &hash), 0, + "Should have payment_hash"); + ASSERT_EQ(bolt12_invoice_signing_pubkey(invoice, &pk), 0, + "Should have signing_pubkey"); + + /* Test property accessors */ + uint64_t amount = 0; + ASSERT_EQ(bolt12_invoice_amount(invoice, &amount), 0, + "Should have amount"); + printf("\n amount_msat: %llu\n", (unsigned long long)amount); + + uint64_t created_at = 0; + ASSERT_EQ(bolt12_invoice_created_at(invoice, &created_at), 0, + "Should have created_at"); + printf(" created_at: %llu\n", (unsigned long long)created_at); + + uint64_t expiry = 0; + ASSERT_EQ(bolt12_invoice_expiry(invoice, &expiry), 0, + "Should have expiry"); + printf(" expiry: %llu\n", (unsigned long long)expiry); + + bolt12_signature_t sig; + ASSERT_EQ(bolt12_invoice_signature(invoice, &sig), 0, + "Should have signature"); + + size_t inv_paths = bolt12_invoice_num_paths(invoice); + printf(" paths: %zu\n", inv_paths); + + char inv_desc[256]; + int inv_desc_len = bolt12_invoice_description(invoice, inv_desc, sizeof(inv_desc)); + if (inv_desc_len >= 0) + printf(" description: \"%s\"\n", inv_desc); + + bolt12_invoice_free(invoice); + printf(" ... OK\n"); +} + +static void test_invalid_preimage_format(void) +{ + bolt12_error_t err; + bolt12_invoice_t *invoice; + int rc; + + printf(" test_invalid_preimage_format... "); + invoice = bolt12_invoice_decode(VALID_INVOICE, &err); + if (!invoice) { + printf("SKIP (can't decode invoice: %s)\n", err.message); + return; + } + + /* Too short */ + rc = bolt12_verify_proof_of_payment(invoice, "deadbeef", &err); + ASSERT_NEQ(rc, 0, "Short preimage should fail"); + + /* Not hex */ + rc = bolt12_verify_proof_of_payment(invoice, + "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", + &err); + ASSERT_NEQ(rc, 0, "Non-hex preimage should fail"); + + bolt12_invoice_free(invoice); + printf("OK\n"); +} + +static void test_invalid_offer_string(void) +{ + bolt12_error_t err; + bolt12_offer_t *offer; + + printf(" test_invalid_offer_string... "); + offer = bolt12_offer_decode("not_a_valid_offer", &err); + ASSERT_EQ((intptr_t)offer, 0, "Invalid offer should return NULL"); + ASSERT_EQ(err.code, BOLT12_ERR_DECODE, + "Error code should be BOLT12_ERR_DECODE"); + printf("OK\n"); +} + +int main(void) +{ + int rc; + + setup_locale(); + printf("libbolt12 test suite\n"); + printf("====================\n\n"); + + rc = bolt12_init(); + if (rc != 0) { + fprintf(stderr, "FATAL: bolt12_init() failed\n"); + return 1; + } + + printf("Decode tests:\n"); + test_decode_offer(); + test_decode_invoice(); + test_invalid_offer_string(); + + printf("\nVerification tests:\n"); + test_valid_verification(); + test_mismatched_offer_invoice(); + test_bad_preimage(); + test_invalid_preimage_format(); + + bolt12_cleanup(); + + printf("\n====================\n"); + printf("Results: %d/%d passed\n", tests_passed, tests_run); + + return tests_passed == tests_run ? 0 : 1; +}