nova-carnivore / bolt12-php
Modern PHP 8.3+ BOLT 12 Lightning Network offer/invoice encoder/decoder with BIP-340 Schnorr signatures
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/nova-carnivore/bolt12-php
Requires
- php: ^8.3
- ext-gmp: *
- paragonie/ecc: ^2.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2026-02-15 17:42:28 UTC
README
Modern PHP 8.3+ BOLT 12 Lightning Network offer/invoice encoder/decoder with BIP-340 Schnorr signatures. Full spec compliance, production-ready.
Features
- ⚡ Full BOLT 12 spec compliance — Offers, Invoice Requests, Invoices, Invoice Errors
- 🔐 BIP-340 Schnorr signatures — Pure PHP implementation with Merkle tree construction
- 🏗️ Modern PHP 8.3+ — Enums, readonly classes, named arguments, match expressions
- 🔍 PHPStan level 9 — Maximum static analysis strictness
- 🌐 All message types — Offer (lno), Invoice Request (lnr), Invoice (lni), Invoice Error
- 🏷️ All TLV fields — chains, metadata, currency, amounts, paths, features, BIP-353, and more
- 🔄 Round-trip safe — Encode → decode preserves all data
- 📏 PSR-12 code style — Enforced with PHP-CS-Fixer
- 🔒 Constant-time crypto — Uses GMP for big integer operations
- 🔗 Blinded paths — Full support for multi-hop blinded payment paths
Installation
composer require nova-carnivore/bolt12-php
Requirements
- PHP 8.3 or higher
- ext-gmp (required for big integers and BIP-340 crypto)
Quick Start
Decode (Auto-detect Type)
use Nova\Bitcoin\Bolt12\Decoder; $offer = Decoder::decode('lno1...'); // Returns Offer object $invReq = Decoder::decode('lnr1...'); // Returns InvoiceRequest object $invoice = Decoder::decode('lni1...'); // Returns Invoice object
Encode an Offer
use Nova\Bitcoin\Bolt12\Encoder; use Nova\Bitcoin\Bolt12\Signer; $encoded = Encoder::encodeOffer( issuerId: $myPublicKeyHex, description: 'Buy a coffee', amountMsat: gmp_init(100000), issuer: 'CoffeeShop', ); // Returns: 'lno1...'
Encode an Invoice Request (auto-signed)
$invReq = Encoder::encodeInvoiceRequest( invreqMetadata: bin2hex(random_bytes(32)), payerId: $myPublicKeyHex, payerPrivateKey: $myPrivateKeyHex, offerDescription: 'Buy a coffee', offerIssuerId: $merchantPubkey, offerAmountMsat: gmp_init(100000), ); // Returns: 'lnr1...' (signed with BIP-340 Schnorr)
Encode an Invoice (auto-signed)
use Nova\Bitcoin\Bolt12\BlindedPath; use Nova\Bitcoin\Bolt12\BlindedPayInfo; use Nova\Bitcoin\Bolt12\OnionMessageHop; $invoice = Encoder::encodeInvoice( nodeId: $myNodePubkey, nodePrivateKey: $myNodePrivkey, createdAt: gmp_init(time()), paymentHash: hash('sha256', $preimage), amountMsat: gmp_init(100000), invoicePaths: [ new BlindedPath($blindingKey, [ new OnionMessageHop($nodeId, $encryptedData), ]), ], blindedPayInfo: [ new BlindedPayInfo( feeBaseMsat: 1000, feeProportionalMillionths: 100, cltvExpiryDelta: 144, htlcMinimumMsat: gmp_init(1000), htlcMaximumMsat: gmp_init(1000000000), ), ], ); // Returns: 'lni1...' (signed with BIP-340 Schnorr)
Verify Signatures
use Nova\Bitcoin\Bolt12\Signer; $invReq = Decoder::decode('lnr1...'); $valid = Signer::verifyInvoiceRequest($invReq); // Returns bool $invoice = Decoder::decode('lni1...'); $valid = Signer::verifyInvoice($invoice); // Returns bool
Invoice Errors
// Encode $errorBytes = Encoder::encodeInvoiceError( error: 'Amount too low', erroneousField: gmp_init(82), // invreq_amount suggestedValue: '0186a0', // 100000 as tu64 hex ); // Decode $invoiceError = Decoder::decodeInvoiceError($errorBytes); echo $invoiceError->error; // 'Amount too low'
Full Payment Flow Example
use Nova\Bitcoin\Bolt12\{Decoder, Encoder, Signer, BlindedPath, BlindedPayInfo, OnionMessageHop}; // 1. Merchant creates an offer $offer = Encoder::encodeOffer( issuerId: $merchantPubkey, description: 'Buy a coffee', amountMsat: gmp_init(100000), ); $decoded = Decoder::decode($offer); // 2. Payer creates invoice request (mirrors offer fields) $invReq = Encoder::encodeInvoiceRequest( invreqMetadata: bin2hex(random_bytes(32)), payerId: $payerPubkey, payerPrivateKey: $payerPrivkey, offerDescription: $decoded->description, offerIssuerId: $decoded->issuerId, offerAmountMsat: $decoded->amountMsat, ); // 3. Merchant creates and signs invoice $invoice = Encoder::encodeInvoice( nodeId: $merchantPubkey, nodePrivateKey: $merchantPrivkey, createdAt: gmp_init(time()), paymentHash: hash('sha256', $preimage), amountMsat: gmp_init(100000), invoicePaths: [$blindedPath], blindedPayInfo: [$payInfo], ); // 4. Payer verifies invoice signature $inv = Decoder::decode($invoice); assert(Signer::verifyInvoice($inv));
API Reference
Decoder::decode(string $bolt12String): Offer|InvoiceRequest|Invoice
Decodes any BOLT 12 bech32 string (auto-detects type by prefix).
Decoder::decodeInvoiceError(array $bytes): InvoiceError
Decodes a BOLT 12 Invoice Error from raw TLV bytes.
Encoder::encodeOffer(...): string
Encodes a BOLT 12 Offer (lno1...). Not signed per spec.
Encoder::encodeInvoiceRequest(...): string
Encodes and signs a BOLT 12 Invoice Request (lnr1...).
Encoder::encodeInvoice(...): string
Encodes and signs a BOLT 12 Invoice (lni1...).
Encoder::encodeInvoiceError(...): array
Encodes a BOLT 12 Invoice Error as raw TLV bytes.
Signer::verifyInvoiceRequest(InvoiceRequest $invReq): bool
Verifies a BIP-340 Schnorr signature on an invoice request.
Signer::verifyInvoice(Invoice $invoice): bool
Verifies a BIP-340 Schnorr signature on an invoice.
Signer::getPublicKey(string $privateKeyHex): string
Derives a compressed public key from a private key.
Data Classes
| Class | Description |
|---|---|
Offer |
Decoded offer with all fields as readonly properties |
InvoiceRequest |
Decoded invoice request with signature |
Invoice |
Decoded invoice with paths, payinfo, and signature |
InvoiceError |
Decoded invoice error |
BlindedPath |
Blinded path with hops |
BlindedPayInfo |
Payment info for blinded paths |
FallbackAddress |
On-chain fallback address |
OnionMessageHop |
Single hop in a blinded path |
Bip353Name |
BIP-353 name (user@domain) |
TlvEntry |
Raw TLV type-value pair |
BOLT 12 Spec Coverage
Message Types
| Type | Prefix | Encode | Decode | Sign | Verify |
|---|---|---|---|---|---|
| Offer | lno |
✅ | ✅ | ✅* | ✅* |
| Invoice Request | lnr |
✅ | ✅ | ✅ | ✅ |
| Invoice | lni |
✅ | ✅ | ✅ | ✅ |
| Invoice Error | — | ✅ | ✅ | N/A | N/A |
* Offers are optionally signed per spec
TLV Fields
| Type | Field | Offer | InvReq | Invoice |
|---|---|---|---|---|
| 0 | invreq_metadata |
— | ✅ | ✅ |
| 2 | offer_chains |
✅ | ✅ | ✅ |
| 4 | offer_metadata |
✅ | ✅ | ✅ |
| 6 | offer_currency |
✅ | ✅ | ✅ |
| 8 | offer_amount |
✅ | ✅ | ✅ |
| 10 | offer_description |
✅ | ✅ | ✅ |
| 12 | offer_features |
✅ | ✅ | ✅ |
| 14 | offer_absolute_expiry |
✅ | ✅ | ✅ |
| 16 | offer_paths |
✅ | ✅ | ✅ |
| 18 | offer_issuer |
✅ | ✅ | ✅ |
| 20 | offer_quantity_max |
✅ | ✅ | ✅ |
| 22 | offer_issuer_id |
✅ | ✅ | ✅ |
| 80 | invreq_chain |
— | ✅ | ✅ |
| 82 | invreq_amount |
— | ✅ | ✅ |
| 84 | invreq_features |
— | ✅ | ✅ |
| 86 | invreq_quantity |
— | ✅ | ✅ |
| 88 | invreq_payer_id |
— | ✅ | ✅ |
| 89 | invreq_payer_note |
— | ✅ | ✅ |
| 90 | invreq_paths |
— | ✅ | ✅ |
| 91 | invreq_bip_353_name |
— | ✅ | ✅ |
| 160 | invoice_paths |
— | — | ✅ |
| 162 | invoice_blindedpay |
— | — | ✅ |
| 164 | invoice_created_at |
— | — | ✅ |
| 166 | invoice_relative_expiry |
— | — | ✅ |
| 168 | invoice_payment_hash |
— | — | ✅ |
| 170 | invoice_amount |
— | — | ✅ |
| 172 | invoice_fallbacks |
— | — | ✅ |
| 174 | invoice_features |
— | — | ✅ |
| 176 | invoice_node_id |
— | — | ✅ |
| 240 | signature |
✅* | ✅ | ✅ |
Encoding
- Bech32 without checksum — per BOLT 12 spec
- BigSize TLV type/length encoding
- tu64 truncated unsigned 64-bit values
+concatenation support for long strings
Cryptography
- BIP-340 Schnorr signatures (64-byte raw, not DER)
- Tagged hashes —
SHA256(SHA256(tag) || SHA256(tag) || msg) - Merkle tree for multi-TLV signatures with nonce leaves
- secp256k1 elliptic curve arithmetic
Security
- BIP-340 Schnorr signatures use pure PHP with GMP for big integer arithmetic
- All elliptic curve operations use constant-time modular arithmetic via GMP
- The Merkle tree construction includes nonce leaves to prevent revealing adjacent fields
- Private keys are only used in signing operations and never stored
Exception Handling
use Nova\Bitcoin\Bolt12\Exception\{ Bolt12Exception, // Base exception DecodeException, // Malformed input EncodeException, // Missing/invalid fields SignatureException, // Signature issues }; try { $offer = Decoder::decode($bolt12String); } catch (DecodeException $e) { // Malformed string or TLV data } catch (Bolt12Exception $e) { // Any BOLT 12 error }
Development
# Install dependencies composer install # Run tests vendor/bin/phpunit # Static analysis vendor/bin/phpstan analyse # Code style check vendor/bin/php-cs-fixer fix --dry-run --diff # Fix code style vendor/bin/php-cs-fixer fix # Run all CI checks composer ci
Architecture
src/
├── Decoder.php # Main decoder (auto-detect type)
├── Encoder.php # Main encoder (all message types)
├── Signer.php # BIP-340 Schnorr + Merkle tree
├── Bech32.php # Bech32 WITHOUT checksum (per BOLT 12)
├── TlvStream.php # BigSize TLV encoding/decoding
├── TlvEntry.php # TLV entry value object
├── Offer.php # Offer value object (readonly)
├── InvoiceRequest.php # Invoice request value object (readonly)
├── Invoice.php # Invoice value object (readonly)
├── InvoiceError.php # Invoice error value object (readonly)
├── BlindedPath.php # Blinded path with encoding/decoding
├── BlindedPayInfo.php # Blinded payment info
├── FallbackAddress.php # On-chain fallback address
├── OnionMessageHop.php # Blinded path hop
├── Bip353Name.php # BIP-353 name resolution
├── Helpers.php # Byte/hex/string utilities
└── Exception/
├── Bolt12Exception.php
├── DecodeException.php
├── EncodeException.php
└── SignatureException.php
Key Differences from BOLT 11
| Feature | BOLT 11 | BOLT 12 |
|---|---|---|
| Encoding | Bech32 with checksum | Bech32 without checksum |
| Crypto | ECDSA recovery | BIP-340 Schnorr |
| Signatures | Simple message hash | Merkle tree construction |
| TLV encoding | 5-bit continuation | BigSize encoding |
| Message types | 1 (invoice only) | 4 (offer, invreq, invoice, error) |
| Privacy | Direct node ID | Blinded paths |
License
MIT — see LICENSE.