Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,29 @@ src/*
!src/cli.h
!src/cli_main.c
!src/constants.h
!src/crypto.c
!src/datadir.h
!src/errors.c
!src/errors.h
!src/message.c
!src/message.h
!src/memory.h
!src/noise.h
!src/p2p.c
!src/peer.c
!src/peer.h
!src/protocol.c
!src/protocol.h
!src/random.h
!src/scoring.c
!src/scoring.h
!src/secure_memory.c
!src/secure_memory.h
!src/secure_random.c
!src/secure_random.h
!src/security.c
!src/sha2.c
!src/sha2.h
!src/taproot.c
!src/taproot.h
!src/threads.c
Expand Down
150 changes: 150 additions & 0 deletions functions/bolt12Semantics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
'use strict';

/**
* BOLT #12 semantic helpers for interpreting **`Lightning#decodeLightning`** (`decode` RPC) JSON from Core Lightning.
*
* **BIP-340 signatures (BOLT #12 § Signature calculation):**
* - **Offers** (`lno1…`) do **not** carry a signature TLV.
* - **`invoice_request`** and **`invoice`** streams include **signature** (TLV type **240**…1000), keyed with
* `SIG("lightning" ‖ stream ‖ field, Merkle_root, key)` per the spec ([BOLT #12](https://github.com/lightning/bolts/blob/master/12-offer-encoding.md)).
* - This module does **not** verify Schnorr signatures; use **`lightningd`** / **`decode`** and payment flows for that.
*
* **Recurrence:** TLVs such as `offer_recurrence_optional` / `_compulsory`, `invreq_recurrence_*`, `invoice_recurrence_basetime`
* are summarized from flat **`decode`** objects when present.
*
* @module functions/bolt12Semantics
* @see docs/LIGHTNING_COMPAT.md
*/

/**
* TLV types named in BOLT #12 (offer / invoice_request / invoice ranges differ by stream).
* @readonly
*/
const BOLT12_TLV = Object.freeze({
INVREQ_METADATA: 0,
OFFER_CHAINS: 2,
OFFER_METADATA: 4,
OFFER_CURRENCY: 6,
OFFER_AMOUNT: 8,
OFFER_DESCRIPTION: 10,
OFFER_FEATURES: 12,
OFFER_ABSOLUTE_EXPIRY: 14,
OFFER_PATHS: 16,
OFFER_ISSUER: 18,
OFFER_QUANTITY_MAX: 20,
OFFER_ISSUER_ID: 22,
OFFER_RECURRENCE_COMPULSORY: 24,
OFFER_RECURRENCE_OPTIONAL: 25,
OFFER_RECURRENCE_BASE: 26,
OFFER_RECURRENCE_PAYWINDOW: 27,
OFFER_RECURRENCE_LIMIT: 29,
INVREQ_CHAIN: 80,
INVREQ_AMOUNT: 82,
INVREQ_QUANTITY: 86,
INVREQ_PAYER_ID: 88,
INVREQ_RECURRENCE_COUNTER: 92,
INVREQ_RECURRENCE_START: 93,
INVREQ_RECURRENCE_CANCEL: 94,
SIGNATURE_MIN: 240,
SIGNATURE_MAX: 1000,
INVOICE_PATHS: 160,
INVOICE_BLINDEDPAY: 162,
INVOICE_CREATED_AT: 164,
INVOICE_PAYMENT_HASH: 168,
INVOICE_AMOUNT: 170,
INVOICE_NODE_ID: 176,
INVOICE_RECURRENCE_BASETIME: 177
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

/**
* Normalized kind for a value returned by **`decode`** / **`decodeLightning`**.
* @readonly
* @enum {string}
*/
const Bolt12StreamKind = Object.freeze({
bolt12_offer: 'bolt12_offer',
bolt12_invoice_request: 'bolt12_invoice_request',
bolt12_invoice: 'bolt12_invoice',
bolt11_invoice: 'bolt11_invoice',
unknown: 'unknown'
});

/**
* Classify **`decode`** result from Core Lightning using its **`type`** field (wording varies slightly by version).
* @param {Object|null|undefined} decoded
* @returns {typeof Bolt12StreamKind[keyof typeof Bolt12StreamKind]}
*/
function classifyDecodedBolt12 (decoded) {
if (!decoded || typeof decoded !== 'object') return Bolt12StreamKind.unknown;
const t = decoded.type;
if (typeof t !== 'string') return Bolt12StreamKind.unknown;
const s = t.toLowerCase();
if (s.includes('bolt12') && s.includes('offer') && !s.includes('invoice')) {
return Bolt12StreamKind.bolt12_offer;
}
if (s.includes('invoice_request') || (s.includes('bolt12') && s.includes('invoice') && s.includes('request'))) {
return Bolt12StreamKind.bolt12_invoice_request;
}
if (s.includes('bolt12') && s.includes('invoice') && !s.includes('request')) {
return Bolt12StreamKind.bolt12_invoice;
}
if (s.includes('bolt11') || /^bitcoin\s*invoice/i.test(t)) {
return Bolt12StreamKind.bolt11_invoice;
}
return Bolt12StreamKind.unknown;
}

/**
* Whether BOLT #12 Merkle **BIP-340** signatures apply to this stream (not offers).
* @param {typeof Bolt12StreamKind[keyof typeof Bolt12StreamKind]} kind
* @returns {boolean}
*/
function bip340SignatureApplies (kind) {
return kind === Bolt12StreamKind.bolt12_invoice_request || kind === Bolt12StreamKind.bolt12_invoice;
}

/**
* Collect recurrence-related fields from a flat **`decode`** object (snake_case keys as CLN tends to emit).
* @param {Object|null|undefined} decoded
* @returns {Object|null} Plain object copy, or `null` if nothing recurrence-related.
*/
function summarizeBolt12RecurrenceFromDecode (decoded) {
if (!decoded || typeof decoded !== 'object') return null;
/** @type {Record<string, unknown>} */
const out = {};

const offerKeys = [
'offer_recurrence',
'offer_recurrence_compulsory',
'offer_recurrence_optional',
'offer_recurrence_base',
'offer_recurrence_paywindow',
'offer_recurrence_limit'
];
for (const k of offerKeys) {
if (decoded[k] != null) out[k] = decoded[k];
}

const invreqKeys = [
'invreq_recurrence_counter',
'invreq_recurrence_start',
'invreq_recurrence_cancel'
];
for (const k of invreqKeys) {
if (decoded[k] != null) out[k] = decoded[k];
}

if (decoded.invoice_recurrence_basetime != null) {
out.invoice_recurrence_basetime = decoded.invoice_recurrence_basetime;
}

return Object.keys(out).length ? out : null;
}

module.exports = {
BOLT12_TLV,
Bolt12StreamKind,
bip340SignatureApplies,
classifyDecodedBolt12,
summarizeBolt12RecurrenceFromDecode
};
130 changes: 130 additions & 0 deletions functions/fabricPaymentBech32.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use strict';

const bech32 = require('./bech32');
const lb = require('./lightningBolt12');

const FABRIC_ROUTED_PAYMENT_HRP = 'fa';

/** All such strings use the `1` separator after the HRP, e.g. `fa1…`. */
const FABRIC_ROUTED_PAYMENT_PREFIX = `${FABRIC_ROUTED_PAYMENT_HRP}1`;

/** Payload format byte (first byte after decode). */
const FABRIC_PAYMENT_VERSION = 0;

/**
* Route / binding type (second byte). `0` = 32-byte document **content hash** (see L1 document exchange).
* @readonly
* @enum {number}
*/
const FabricRouteType = Object.freeze({
/** SHA256 hash as used with `purchaseContentHashHex` / canonical publish preimage chain. */
DOCUMENT_CONTENT_HASH: 0
});

/**
* @param {Array|unknown} arr
* @returns {Buffer}
*/
function hash32ByteArrayToBuffer (arr) {
if (!Array.isArray(arr) || arr.length !== 32) {
throw new TypeError('hash32 array must have exactly 32 elements');
}
const out = Buffer.alloc(32);
for (let i = 0; i < 32; i++) {
const x = arr[i];
if (!Number.isInteger(x) || x < 0 || x > 255) {
throw new TypeError('hash32 array elements must be integers in 0..255');
}
out[i] = x;
}
return out;
}

/**
* @param {string|null|undefined} s
* @returns {boolean}
*/
function isFabricRoutedPaymentString (s) {
return String(s || '').trim().toLowerCase().startsWith(FABRIC_ROUTED_PAYMENT_PREFIX);
}

/**
* Encode a v0 Fabric-routed payment reference: version + route type + 32-byte id (e.g. content hash).
* @param {Object} o
* @param {Buffer|Uint8Array|number[]} o.hash32 Exactly 32 bytes (e.g. document content hash).
* @param {number} [o.routeType=0] {@link FabricRouteType}
* @returns {string} bech32m string (`fa1…`)
*/
function encodeFabricRoutedPaymentV0 (o) {
if (!o || !o.hash32) throw new TypeError('encodeFabricRoutedPaymentV0 requires hash32');
let buf;
if (Buffer.isBuffer(o.hash32)) buf = o.hash32;
else if (o.hash32 instanceof Uint8Array) buf = Buffer.from(o.hash32);
else if (Array.isArray(o.hash32)) buf = hash32ByteArrayToBuffer(o.hash32);
else throw new TypeError('hash32 must be Buffer, Uint8Array, or byte array');
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (buf.length !== 32) throw new TypeError('hash32 must be exactly 32 bytes');
const routeType = o.routeType != null ? Number(o.routeType) : FabricRouteType.DOCUMENT_CONTENT_HASH;
if (!Number.isInteger(routeType) || routeType < 0 || routeType > 255) {
throw new TypeError('routeType must be an integer 0..255');
}
const payload = Buffer.concat([Buffer.from([FABRIC_PAYMENT_VERSION, routeType]), buf]);
const words = bech32.toWords(payload);
return bech32.encode(FABRIC_ROUTED_PAYMENT_HRP, words, 'bech32m');
}

/**
* Decode an `fa1…` string. Returns `null` if HRP/spec mismatch or payload shape is wrong.
* @param {string} str
* @returns {{ version: number, routeType: number, hash32: Buffer, hrp: string, spec: string }|null}
*/
function decodeFabricRoutedPayment (str) {
if (typeof str !== 'string' || !str.trim()) return null;
let d;
try {
d = bech32.decode(str.trim());
} catch {
return null;
}
if (d.hrp !== FABRIC_ROUTED_PAYMENT_HRP || d.spec !== 'bech32m') return null;
let raw;
try {
raw = bech32.fromWords(d.words);
} catch {
return null;
}
if (raw.length !== 34) return null;
const version = raw[0];
const routeType = raw[1];
const hash32 = Buffer.from(raw.slice(2, 34));
if (version !== FABRIC_PAYMENT_VERSION) return null;
return {
version,
routeType,
hash32,
hrp: d.hrp,
spec: d.spec
};
}

/**
* Classify a payment-related encoded string: Fabric `fa1…`, then Lightning `ln…` (see {@link ./lightningBolt12}).
* @param {string|null|undefined} s
* @returns {string}
*/
function classifyPaymentEncodingString (s) {
const t = String(s || '').trim();
if (!t) return 'empty';
if (isFabricRoutedPaymentString(t)) return 'fabric_routed_payment';
return lb.classifyLightningEncodedString(t);
}

module.exports = {
FABRIC_ROUTED_PAYMENT_HRP,
FABRIC_ROUTED_PAYMENT_PREFIX,
FABRIC_PAYMENT_VERSION,
FabricRouteType,
classifyPaymentEncodingString,
decodeFabricRoutedPayment,
encodeFabricRoutedPaymentV0,
isFabricRoutedPaymentString
};
Loading
Loading