Skip to content

Commit 63bf809

Browse files
contrib: add libbolt12 standalone decode/verify library
Add a standalone C library (libbolt12) that wraps CLN's existing bolt12 implementation behind a clean public API with no CLN-internal types exposed (no tal, no TLV structs). The library provides: - Decode BOLT12 offers (lno1...) and invoices (lni1...) - Access all offer/invoice properties via accessor functions - Verify invoice BIP-340 schnorr signatures - Verify proof of payment (preimage vs payment_hash) - Compare offer_id and signing pubkeys between offer and invoice - All-in-one bolt12_verify_offer_payment() convenience function The motivation is enabling external C programs (e.g. mining pool payout verification tools) to decode and verify BOLT12 offers/invoices without depending on Rust or running a full CLN node. The library links against libcommon.a and libccan.a, with a fat archive option (libbolt12-fat.a) for external consumers. libbolt12 deliberately avoids common_setup() because common_setup() mutates process-global state (locale, env, progname) and calls errx(1) on init failures -- both unacceptable for a reusable library. Instead, bolt12_init() does the minimum: secp256k1_context_create() plus a tmpctx. bolt12_cleanup() reverses this. Includes a test suite (contrib/libbolt12/run-bolt12.c) using real-world test vectors from Phoenix and CLN-generated offers/invoices, exercising the happy path and all documented error branches. Changelog-Added: contrib: new libbolt12 library exposing BOLT12 decode and verification as a standalone C API. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f2b7017 commit 63bf809

9 files changed

Lines changed: 1603 additions & 0 deletions

File tree

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ include doc/Makefile
408408
include contrib/msggen/Makefile
409409
include devtools/Makefile
410410
include tools/Makefile
411+
include contrib/libbolt12/Makefile
411412
ifneq ($(RUST),0)
412413
include cln-rpc/Makefile
413414
include cln-grpc/Makefile

contrib/libbolt12/Makefile

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# libbolt12 - Makefile fragment included by CLN's top-level Makefile.
2+
#
3+
# Produces:
4+
# libbolt12.a thin wrapper; consumers link libcommon.a +
5+
# libccan.a + libsecp256k1 alongside it
6+
# libbolt12-fat.a self-contained archive (only needs
7+
# -lsecp256k1 at link time)
8+
# contrib/libbolt12/run-bolt12 unit test binary
9+
10+
LIBBOLT12_SRC := contrib/libbolt12/bolt12_api.c
11+
LIBBOLT12_OBJS := $(LIBBOLT12_SRC:.c=.o)
12+
LIBBOLT12_HEADERS := \
13+
contrib/libbolt12/bolt12.h \
14+
contrib/libbolt12/bolt12_internal.h
15+
16+
LIBBOLT12_TEST_SRC := contrib/libbolt12/run-bolt12.c
17+
LIBBOLT12_TEST_OBJS := $(LIBBOLT12_TEST_SRC:.c=.o)
18+
19+
# Thin wrapper library (just our wrapper code).
20+
libbolt12.a: $(LIBBOLT12_OBJS)
21+
@$(call VERBOSE, "ar libbolt12.a", $(AR) rcs $@ $(LIBBOLT12_OBJS))
22+
23+
# Fat archive: bundles libbolt12 + libcommon + libccan. External
24+
# consumers still need -lsecp256k1 -lsodium -lwallycore -lm at link time
25+
# because libcommon.a transitively references sodium/wally symbols from
26+
# code paths libbolt12 itself doesn't use (psbt.c, script.c, randbytes.c
27+
# etc.). A future refactor could extract a bolt12-only subset to drop
28+
# those transitive deps.
29+
libbolt12-fat.a: libbolt12.a libcommon.a libccan.a
30+
@$(call VERBOSE, "ar libbolt12-fat.a", \
31+
rm -rf .libbolt12-tmp && mkdir .libbolt12-tmp && \
32+
cd .libbolt12-tmp && \
33+
$(AR) x ../libbolt12.a && \
34+
$(AR) x ../libcommon.a && \
35+
$(AR) x ../libccan.a && \
36+
$(AR) rcs ../libbolt12-fat.a *.o && \
37+
cd .. && rm -rf .libbolt12-tmp)
38+
39+
# Test binary.
40+
contrib/libbolt12/run-bolt12: $(LIBBOLT12_TEST_OBJS) libbolt12.a libcommon.a libccan.a
41+
@$(call VERBOSE, "ld $@", \
42+
$(CC) $(CFLAGS) -o $@ $(LIBBOLT12_TEST_OBJS) \
43+
libbolt12.a libcommon.a libccan.a \
44+
$(EXTERNAL_LDLIBS) $(LDLIBS))
45+
46+
check-libbolt12: contrib/libbolt12/run-bolt12
47+
contrib/libbolt12/run-bolt12
48+
49+
ALL_C_SOURCES += $(LIBBOLT12_SRC) $(LIBBOLT12_TEST_SRC)
50+
ALL_C_HEADERS += $(LIBBOLT12_HEADERS)
51+
52+
clean: libbolt12-clean
53+
libbolt12-clean:
54+
$(RM) libbolt12.a libbolt12-fat.a
55+
$(RM) $(LIBBOLT12_OBJS) $(LIBBOLT12_TEST_OBJS)
56+
$(RM) contrib/libbolt12/run-bolt12
57+
$(RM) -rf .libbolt12-tmp

contrib/libbolt12/README.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# libbolt12
2+
3+
Standalone C library for decoding and verifying BOLT12 offers and invoices,
4+
built on top of Core Lightning's existing bolt12 implementation.
5+
6+
**Status**: Experimental (RFC). API may change before stabilization.
7+
8+
## Motivation
9+
10+
External C programs (e.g. mining pool payout verification tools, hardware
11+
wallets, embedded devices) need to decode and verify BOLT12 offers/invoices
12+
without depending on Rust, running a full CLN node, or shelling out to
13+
`lightning-cli decode`. libbolt12 provides that.
14+
15+
## Features
16+
17+
- Decode `lno1...` offers and `lni1...` invoices from bech32m strings
18+
- Access all offer/invoice properties via clean accessor functions
19+
- Verify invoice BIP-340 Schnorr signatures (merkle-root over TLV)
20+
- Verify proof of payment (SHA256 preimage matches payment hash)
21+
- Compare offer_id and signing pubkeys between offer and invoice
22+
- All-in-one `bolt12_verify_offer_payment()` convenience entry point
23+
24+
## Dependencies
25+
26+
Runtime (link-time):
27+
- libsecp256k1 (Schnorr signature verification)
28+
- libsodium (transitively pulled in by libcommon.a)
29+
- libwallycore (transitively pulled in by libcommon.a)
30+
- libm
31+
32+
libbolt12's own code does NOT use libsodium or libwally — it calls
33+
`secp256k1_context_create()` directly instead of going through
34+
`common_setup()`/wally, to avoid the global process-state mutations
35+
(locale, env, progname) that `common_setup()` performs and to avoid
36+
`common_setup()`'s `errx(1, ...)` failure mode that would kill the host
37+
application.
38+
39+
However, `libcommon.a` as a whole still transitively references sodium and
40+
wally symbols (from files like `psbt.c`, `script.c`, `randbytes.c` that
41+
libbolt12 does not use). A future refactor could build a trimmed
42+
`libcommon-bolt12.a` containing only the object files libbolt12 actually
43+
needs, removing the transitive libsodium/libwally dependency. For now the
44+
fat archive still requires them at link time.
45+
46+
## Building
47+
48+
From the CLN source root:
49+
50+
```sh
51+
make libbolt12.a # thin archive (requires libcommon.a + libccan.a
52+
# to be linked alongside it by consumers)
53+
make libbolt12-fat.a # self-contained archive (only needs -lsecp256k1)
54+
make check-libbolt12 # run the test suite
55+
```
56+
57+
## Using the library
58+
59+
### Linking
60+
61+
With the fat archive (simpler for external consumers):
62+
63+
```sh
64+
cc -o myprog main.c \
65+
-I/path/to/cln/contrib/libbolt12 \
66+
/path/to/cln/libbolt12-fat.a \
67+
-lsecp256k1 -lsodium -lwallycore -lm
68+
```
69+
70+
With the thin archive (if you already link the CLN archives):
71+
72+
```sh
73+
cc -o myprog main.c \
74+
-I/path/to/cln/contrib/libbolt12 \
75+
/path/to/cln/libbolt12.a \
76+
/path/to/cln/libcommon.a \
77+
/path/to/cln/libccan.a \
78+
-lsecp256k1 -lsodium -lwallycore -lm
79+
```
80+
81+
### Example
82+
83+
```c
84+
#include "bolt12.h"
85+
#include <stdio.h>
86+
87+
int main(void) {
88+
if (bolt12_init() != 0) {
89+
fprintf(stderr, "Failed to initialize libbolt12\n");
90+
return 1;
91+
}
92+
93+
bolt12_error_t err;
94+
int rc = bolt12_verify_offer_payment(
95+
"lno1pg7y7s69...",
96+
"lni1qqg9sr0tna...",
97+
"a71dceaa4f2b86713834d6362035adf0eb7eab6c6c61ae3c8b68baffd9072cfc",
98+
&err);
99+
100+
if (rc != 0)
101+
fprintf(stderr, "Verification failed: %s\n", err.message);
102+
else
103+
printf("Payment verified successfully.\n");
104+
105+
bolt12_cleanup();
106+
return rc;
107+
}
108+
```
109+
110+
For per-field access:
111+
112+
```c
113+
bolt12_offer_t *offer = bolt12_offer_decode(offer_str, &err);
114+
if (!offer) { /* handle err */ }
115+
116+
char description[256];
117+
bolt12_offer_description(offer, description, sizeof(description));
118+
119+
uint64_t amount_msat;
120+
if (bolt12_offer_amount(offer, &amount_msat) == 0) {
121+
printf("Amount: %llu msat\n", (unsigned long long)amount_msat);
122+
}
123+
124+
bolt12_offer_free(offer);
125+
```
126+
127+
See `bolt12.h` for the full API.
128+
129+
## Thread safety
130+
131+
**Not thread-safe.** libbolt12 uses process-global state (the libsecp256k1
132+
verification context and CLN's `tmpctx` tal root). Serialize all calls from
133+
a single thread, or protect with an external mutex.
134+
135+
## Testing
136+
137+
```sh
138+
make check-libbolt12
139+
```
140+
141+
The test suite uses real offer/invoice/preimage triplets generated by
142+
Core Lightning and Phoenix, exercising the happy path and all error
143+
branches (mismatched signing keys, wrong preimage, invalid input format).

0 commit comments

Comments
 (0)