UNPKG

bolt11-decoder

Version:

A library for encoding and decoding lightning network payment requests as defined in [BOLT #11](https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md).

870 lines (758 loc) 32 kB
'use strict' const crypto = require('crypto') const bech32 = require('bech32') const secp256k1 = require('secp256k1') const Buffer = require('safe-buffer').Buffer const BN = require('bn.js') const encoding = require('./encoding') const cloneDeep = require('lodash/cloneDeep') const coininfo = require('coininfo') const BITCOINJS_NETWORK_INFO = { bitcoin: coininfo.bitcoin.main.toBitcoinJS(), testnet: coininfo.bitcoin.test.toBitcoinJS(), regtest: coininfo.bitcoin.regtest.toBitcoinJS(), litecoin: coininfo.litecoin.main.toBitcoinJS(), litecoin_testnet: coininfo.litecoin.test.toBitcoinJS() } BITCOINJS_NETWORK_INFO.bitcoin.bech32 = 'bc' BITCOINJS_NETWORK_INFO.testnet.bech32 = 'tb' BITCOINJS_NETWORK_INFO.regtest.bech32 = 'bcrt' BITCOINJS_NETWORK_INFO.litecoin.bech32 = 'ltc' BITCOINJS_NETWORK_INFO.litecoin_testnet.bech32 = 'tltc' // defaults for encode; default timestamp is current time at call const DEFAULTNETWORKSTRING = 'testnet' const DEFAULTNETWORK = BITCOINJS_NETWORK_INFO[DEFAULTNETWORKSTRING] const DEFAULTEXPIRETIME = 3600 const DEFAULTCLTVEXPIRY = 9 const DEFAULTDESCRIPTION = '' const VALIDWITNESSVERSIONS = [0] const BECH32CODES = { bc: 'bitcoin', tb: 'testnet', sb: 'simnet', bcrt: 'regtest', ltc: 'litecoin', tltc: 'litecoin_testnet' } const DIVISORS = { m: new BN(1e3, 10), u: new BN(1e6, 10), n: new BN(1e9, 10), p: new BN(1e12, 10) } const MAX_MILLISATS = new BN('2100000000000000000', 10) const MILLISATS_PER_BTC = new BN(1e11, 10) const MILLISATS_PER_MILLIBTC = new BN(1e8, 10) const MILLISATS_PER_MICROBTC = new BN(1e5, 10) const MILLISATS_PER_NANOBTC = new BN(1e2, 10) const PICOBTC_PER_MILLISATS = new BN(10, 10) const TAGCODES = { payment_hash: 1, description: 13, payee_node_key: 19, purpose_commit_hash: 23, // commit to longer descriptions (like a website) expire_time: 6, // default: 3600 (1 hour) min_final_cltv_expiry: 24, // default: 9 fallback_address: 9, routing_info: 3 // for extra routing info (private etc.) } // reverse the keys and values of TAGCODES and insert into TAGNAMES const TAGNAMES = {} for (let i = 0, keys = Object.keys(TAGCODES); i < keys.length; i++) { let currentName = keys[i] let currentCode = TAGCODES[keys[i]].toString() TAGNAMES[currentCode] = currentName } const TAGENCODERS = { payment_hash: hexToWord, // 256 bits description: textToWord, // string variable length payee_node_key: hexToWord, // 264 bits purpose_commit_hash: purposeCommitEncoder, // 256 bits expire_time: intBEToWords, // default: 3600 (1 hour) min_final_cltv_expiry: intBEToWords, // default: 9 fallback_address: fallbackAddressEncoder, routing_info: routingInfoEncoder // for extra routing info (private etc.) } const TAGPARSERS = { '1': (words) => wordsToBuffer(words, true).toString('hex'), // 256 bits '13': (words) => wordsToBuffer(words, true).toString('utf8'), // string variable length '19': (words) => wordsToBuffer(words, true).toString('hex'), // 264 bits '23': (words) => wordsToBuffer(words, true).toString('hex'), // 256 bits '6': wordsToIntBE, // default: 3600 (1 hour) '24': wordsToIntBE, // default: 9 '9': fallbackAddressParser, '3': routingInfoParser // for extra routing info (private etc.) } const unknownTagName = 'unknownTag' function unknownEncoder (data) { data.words = bech32.decode(data.words, Number.MAX_SAFE_INTEGER).words return data } function getUnknownParser (tagCode) { return (words) => ({ tagCode: parseInt(tagCode), words: bech32.encode('unknown', words, Number.MAX_SAFE_INTEGER) }) } function wordsToIntBE (words) { return words.reverse().reduce((total, item, index) => { return total + item * Math.pow(32, index) }, 0) } function intBEToWords (intBE, bits) { let words = [] if (bits === undefined) bits = 5 intBE = Math.floor(intBE) if (intBE === 0) return [0] while (intBE > 0) { words.push(intBE & (Math.pow(2, bits) - 1)) intBE = Math.floor(intBE / Math.pow(2, bits)) } return words.reverse() } function sha256 (data) { return crypto.createHash('sha256').update(data).digest() } function convert (data, inBits, outBits) { let value = 0 let bits = 0 let maxV = (1 << outBits) - 1 let result = [] for (let i = 0; i < data.length; ++i) { value = (value << inBits) | data[i] bits += inBits while (bits >= outBits) { bits -= outBits result.push((value >> bits) & maxV) } } if (bits > 0) { result.push((value << (outBits - bits)) & maxV) } return result } function wordsToBuffer (words, trim) { let buffer = Buffer.from(convert(words, 5, 8, true)) if (trim && words.length * 5 % 8 !== 0) { buffer = buffer.slice(0, -1) } return buffer } function hexToBuffer (hex) { if (hex !== undefined && (typeof hex === 'string' || hex instanceof String) && hex.match(/^([a-zA-Z0-9]{2})*$/)) { return Buffer.from(hex, 'hex') } return hex } function textToBuffer (text) { return Buffer.from(text, 'utf8') } function hexToWord (hex) { let buffer = hexToBuffer(hex) return bech32.toWords(buffer) } function textToWord (text) { let buffer = textToBuffer(text) let words = bech32.toWords(buffer) return words } // see encoder for details function fallbackAddressParser (words, network) { let version = words[0] words = words.slice(1) let addressHash = wordsToBuffer(words, true) let address = null switch (version) { case 17: address = encoding.toBase58Check(addressHash, network.pubKeyHash) break case 18: address = encoding.toBase58Check(addressHash, network.scriptHash) break case 0: address = encoding.toBech32(addressHash, version, network.bech32) break } return { code: version, address, addressHash: addressHash.toString('hex') } } // the code is the witness version OR 17 for P2PKH OR 18 for P2SH // anything besides code 17 or 18 should be bech32 encoded address. // 1 word for the code, and right pad with 0 if necessary for the addressHash // (address parsing for encode is done in the encode function) function fallbackAddressEncoder (data, network) { return [data.code].concat(hexToWord(data.addressHash)) } // first convert from words to buffer, trimming padding where necessary // parse in 51 byte chunks. See encoder for details. function routingInfoParser (words) { let routes = [] let pubkey, shortChannelId, feeBaseMSats, feeProportionalMillionths, cltvExpiryDelta let routesBuffer = wordsToBuffer(words, true) while (routesBuffer.length > 0) { pubkey = routesBuffer.slice(0, 33).toString('hex') // 33 bytes shortChannelId = routesBuffer.slice(33, 41).toString('hex') // 8 bytes feeBaseMSats = parseInt(routesBuffer.slice(41, 45).toString('hex'), 16) // 4 bytes feeProportionalMillionths = parseInt(routesBuffer.slice(45, 49).toString('hex'), 16) // 4 bytes cltvExpiryDelta = parseInt(routesBuffer.slice(49, 51).toString('hex'), 16) // 2 bytes routesBuffer = routesBuffer.slice(51) routes.push({ pubkey, short_channel_id: shortChannelId, fee_base_msat: feeBaseMSats, fee_proportional_millionths: feeProportionalMillionths, cltv_expiry_delta: cltvExpiryDelta }) } return routes } // routing info is encoded first as a large buffer // 51 bytes for each channel // 33 byte pubkey, 8 byte short_channel_id, 4 byte millisatoshi base fee (left padded) // 4 byte fee proportional millionths and a 2 byte left padded CLTV expiry delta. // after encoding these 51 byte chunks and concatenating them // convert to words right padding 0 bits. function routingInfoEncoder (datas) { let buffer = Buffer.from([]) datas.forEach(data => { buffer = Buffer.concat([buffer, hexToBuffer(data.pubkey)]) buffer = Buffer.concat([buffer, hexToBuffer(data.short_channel_id)]) buffer = Buffer.concat([buffer, Buffer.from([0, 0, 0].concat(intBEToWords(data.fee_base_msat, 8)).slice(-4))]) buffer = Buffer.concat([buffer, Buffer.from([0, 0, 0].concat(intBEToWords(data.fee_proportional_millionths, 8)).slice(-4))]) buffer = Buffer.concat([buffer, Buffer.from([0].concat(intBEToWords(data.cltv_expiry_delta, 8)).slice(-2))]) }) return hexToWord(buffer) } // if text, return the sha256 hash of the text as words. // if hex, return the words representation of that data. function purposeCommitEncoder (data) { let buffer if (data !== undefined && (typeof data === 'string' || data instanceof String)) { if (data.match(/^([a-zA-Z0-9]{2})*$/)) { buffer = Buffer.from(data, 'hex') } else { buffer = sha256(Buffer.from(data, 'utf8')) } } else { throw new Error('purpose or purpose commit must be a string or hex string') } return bech32.toWords(buffer) } function tagsItems (tags, tagName) { let tag = tags.filter(item => item.tagName === tagName) let data = tag.length > 0 ? tag[0].data : null return data } function tagsContainItem (tags, tagName) { return tagsItems(tags, tagName) !== null } function orderKeys (unorderedObj) { let orderedObj = {} Object.keys(unorderedObj).sort().forEach((key) => { orderedObj[key] = unorderedObj[key] }) return orderedObj } function satToHrp (satoshis) { if (!satoshis.toString().match(/^\d+$/)) { throw new Error('satoshis must be an integer') } let millisatoshisBN = new BN(satoshis, 10) return millisatToHrp(millisatoshisBN.mul(new BN(1000, 10))) } function millisatToHrp (millisatoshis) { if (!millisatoshis.toString().match(/^\d+$/)) { throw new Error('millisatoshis must be an integer') } let millisatoshisBN = new BN(millisatoshis, 10) let millisatoshisString = millisatoshisBN.toString(10) let millisatoshisLength = millisatoshisString.length let divisorString, valueString if (millisatoshisLength > 11 && /0{11}$/.test(millisatoshisString)) { divisorString = '' valueString = millisatoshisBN.div(MILLISATS_PER_BTC).toString(10) } else if (millisatoshisLength > 8 && /0{8}$/.test(millisatoshisString)) { divisorString = 'm' valueString = millisatoshisBN.div(MILLISATS_PER_MILLIBTC).toString(10) } else if (millisatoshisLength > 5 && /0{5}$/.test(millisatoshisString)) { divisorString = 'u' valueString = millisatoshisBN.div(MILLISATS_PER_MICROBTC).toString(10) } else if (millisatoshisLength > 2 && /0{2}$/.test(millisatoshisString)) { divisorString = 'n' valueString = millisatoshisBN.div(MILLISATS_PER_NANOBTC).toString(10) } else { divisorString = 'p' valueString = millisatoshisBN.mul(PICOBTC_PER_MILLISATS).toString(10) } return valueString + divisorString } function hrpToSat (hrpString, outputString) { let millisatoshisBN = hrpToMillisat(hrpString, false) if (!millisatoshisBN.mod(new BN(1000, 10)).eq(new BN(0, 10))) { throw new Error('Amount is outside of valid range') } let result = millisatoshisBN.div(new BN(1000, 10)) return outputString ? result.toString() : result } function hrpToMillisat (hrpString, outputString) { let divisor, value if (hrpString.slice(-1).match(/^[munp]$/)) { divisor = hrpString.slice(-1) value = hrpString.slice(0, -1) } else if (hrpString.slice(-1).match(/^[^munp0-9]$/)) { throw new Error('Not a valid multiplier for the amount') } else { value = hrpString } if (!value.match(/^\d+$/)) throw new Error('Not a valid human readable amount') let valueBN = new BN(value, 10) let millisatoshisBN = divisor ? valueBN.mul(MILLISATS_PER_BTC).div(DIVISORS[divisor]) : valueBN.mul(MILLISATS_PER_BTC) if (((divisor === 'p' && !valueBN.mod(new BN(10, 10)).eq(new BN(0, 10))) || millisatoshisBN.gt(MAX_MILLISATS))) { throw new Error('Amount is outside of valid range') } return outputString ? millisatoshisBN.toString() : millisatoshisBN } function sign (inputPayReqObj, inputPrivateKey) { let payReqObj = cloneDeep(inputPayReqObj) let privateKey = hexToBuffer(inputPrivateKey) if (payReqObj.complete && payReqObj.paymentRequest) return payReqObj if (privateKey === undefined || privateKey.length !== 32 || !secp256k1.privateKeyVerify(privateKey)) { throw new Error('privateKey must be a 32 byte Buffer and valid private key') } let nodePublicKey, tagNodePublicKey // If there is a payee_node_key tag convert to buffer if (tagsContainItem(payReqObj.tags, TAGNAMES['19'])) { tagNodePublicKey = hexToBuffer(tagsItems(payReqObj.tags, TAGNAMES['19'])) } // If there is payeeNodeKey attribute, convert to buffer if (payReqObj.payeeNodeKey) { nodePublicKey = hexToBuffer(payReqObj.payeeNodeKey) } // If they are not equal throw an error if (nodePublicKey && tagNodePublicKey && !tagNodePublicKey.equals(nodePublicKey)) { throw new Error('payee node key tag and payeeNodeKey attribute must match') } // make sure if either exist they are in nodePublicKey nodePublicKey = tagNodePublicKey || nodePublicKey let publicKey = secp256k1.publicKeyCreate(privateKey) // Check if pubkey matches for private key if (nodePublicKey && !publicKey.equals(nodePublicKey)) { throw new Error('The private key given is not the private key of the node public key given') } let words = bech32.decode(payReqObj.wordsTemp, Number.MAX_SAFE_INTEGER).words // the preimage for the signing data is the buffer of the prefix concatenated // with the buffer conversion of the data words excluding the signature // (right padded with 0 bits) let toSign = Buffer.concat([Buffer.from(payReqObj.prefix, 'utf8'), wordsToBuffer(words)]) // single SHA256 hash for the signature let payReqHash = sha256(toSign) // signature is 64 bytes (32 byte r value and 32 byte s value concatenated) // PLUS one extra byte appended to the right with the recoveryID in [0,1,2,3] // Then convert to 5 bit words with right padding 0 bits. let sigObj = secp256k1.sign(payReqHash, privateKey) let sigWords = hexToWord(sigObj.signature.toString('hex') + '0' + sigObj.recovery) // append signature words to the words, mark as complete, and add the payreq payReqObj.payeeNodeKey = publicKey.toString('hex') payReqObj.signature = sigObj.signature.toString('hex') payReqObj.recoveryFlag = sigObj.recovery payReqObj.wordsTemp = bech32.encode('temp', words.concat(sigWords), Number.MAX_SAFE_INTEGER) payReqObj.complete = true payReqObj.paymentRequest = bech32.encode(payReqObj.prefix, words.concat(sigWords), Number.MAX_SAFE_INTEGER) return orderKeys(payReqObj) } /* MUST but default OK: coinType (default: testnet OK) timestamp (default: current time OK) MUST: signature OR privatekey tags[TAGNAMES['1']] (payment hash) tags[TAGNAMES['13']] OR tags[TAGNAMES['23']] (description or description for hashing (or description hash)) MUST CHECK: IF tags[TAGNAMES['19']] (payee_node_key) THEN MUST CHECK THAT PUBKEY = PUBKEY OF PRIVATEKEY / SIGNATURE IF tags[TAGNAMES['9']] (fallback_address) THEN MUST CHECK THAT THE ADDRESS IS A VALID TYPE IF tags[TAGNAMES['3']] (routing_info) THEN MUST CHECK FOR ALL INFO IN EACH */ function encode (inputData, addDefaults) { // we don't want to affect the data being passed in, so we copy the object let data = cloneDeep(inputData) // by default we will add default values to description, expire time, and min cltv if (addDefaults === undefined) addDefaults = true let canReconstruct = !(data.signature === undefined || data.recoveryFlag === undefined) // if no cointype is defined, set to testnet let coinTypeObj if (data.coinType === undefined && !canReconstruct) { data.coinType = DEFAULTNETWORKSTRING coinTypeObj = DEFAULTNETWORK } else if (data.coinType === undefined && canReconstruct) { throw new Error('Need coinType for proper payment request reconstruction') } else { // if the coinType is not a valid name of a network in bitcoinjs-lib, fail if (!BITCOINJS_NETWORK_INFO[data.coinType]) throw new Error('Unknown coin type') coinTypeObj = BITCOINJS_NETWORK_INFO[data.coinType] } // use current time as default timestamp (seconds) if (data.timestamp === undefined && !canReconstruct) { data.timestamp = Math.floor(new Date().getTime() / 1000) } else if (data.timestamp === undefined && canReconstruct) { throw new Error('Need timestamp for proper payment request reconstruction') } if (data.tags === undefined) throw new Error('Payment Requests need tags array') // If no payment hash, fail if (!tagsContainItem(data.tags, TAGNAMES['1'])) { throw new Error('Lightning Payment Request needs a payment hash') } // If no description or purpose commit hash/message, fail if (!tagsContainItem(data.tags, TAGNAMES['13']) && !tagsContainItem(data.tags, TAGNAMES['23'])) { if (addDefaults) { data.tags.push({ tagName: TAGNAMES['13'], data: DEFAULTDESCRIPTION }) } else { throw new Error('Payment request requires description or purpose commit hash') } } // If a description exists, check to make sure the buffer isn't greater than // 639 bytes long, since 639 * 8 / 5 = 1023 words (5 bit) when padded if (tagsContainItem(data.tags, TAGNAMES['13']) && Buffer.from(tagsItems(data.tags, TAGNAMES['13']), 'utf8').length > 639) { throw new Error('Description is too long: Max length 639 bytes') } // if there's no expire time, and it is not reconstructing (must have private key) // default to adding a 3600 second expire time (1 hour) if (!tagsContainItem(data.tags, TAGNAMES['6']) && !canReconstruct && addDefaults) { data.tags.push({ tagName: TAGNAMES['6'], data: DEFAULTEXPIRETIME }) } // if there's no minimum cltv time, and it is not reconstructing (must have private key) // default to adding a 9 block minimum cltv time (90 minutes for bitcoin) if (!tagsContainItem(data.tags, TAGNAMES['24']) && !canReconstruct && addDefaults) { data.tags.push({ tagName: TAGNAMES['24'], data: DEFAULTCLTVEXPIRY }) } let nodePublicKey, tagNodePublicKey // If there is a payee_node_key tag convert to buffer if (tagsContainItem(data.tags, TAGNAMES['19'])) tagNodePublicKey = hexToBuffer(tagsItems(data.tags, TAGNAMES['19'])) // If there is payeeNodeKey attribute, convert to buffer if (data.payeeNodeKey) nodePublicKey = hexToBuffer(data.payeeNodeKey) if (nodePublicKey && tagNodePublicKey && !tagNodePublicKey.equals(nodePublicKey)) { throw new Error('payeeNodeKey and tag payee node key do not match') } // in case we have one or the other, make sure it's in nodePublicKey nodePublicKey = nodePublicKey || tagNodePublicKey if (nodePublicKey) data.payeeNodeKey = nodePublicKey.toString('hex') let code, addressHash, address // If there is a fallback address tag we must check it is valid if (tagsContainItem(data.tags, TAGNAMES['9'])) { let addrData = tagsItems(data.tags, TAGNAMES['9']) // Most people will just provide address so Hash and code will be undefined here address = addrData.address addressHash = addrData.addressHash code = addrData.code if (addressHash === undefined || code === undefined) { let bech32addr, base58addr try { bech32addr = encoding.fromBech32(address) addressHash = bech32addr.data code = bech32addr.version } catch (e) { try { base58addr = encoding.fromBase58Check(address) if (base58addr.version === coinTypeObj.pubKeyHash) { code = 17 } else if (base58addr.version === coinTypeObj.scriptHash) { code = 18 } addressHash = base58addr.hash } catch (f) { throw new Error('Fallback address type is unknown') } } if (bech32addr && !(bech32addr.version in VALIDWITNESSVERSIONS)) { throw new Error('Fallback address witness version is unknown') } if (bech32addr && bech32addr.prefix !== coinTypeObj.bech32) { throw new Error('Fallback address network type does not match payment request network type') } if (base58addr && base58addr.version !== coinTypeObj.pubKeyHash && base58addr.version !== coinTypeObj.scriptHash) { throw new Error('Fallback address version (base58) is unknown or the network type is incorrect') } // FIXME: If addressHash or code is missing, add them to the original Object // after parsing the address value... this changes the actual attributes of the data object. // Not very clean. // Without this, a person can not specify a fallback address tag with only the address key. addrData.addressHash = addressHash.toString('hex') addrData.code = code } } // If there is route info tag, check that each route has all 4 necessary info if (tagsContainItem(data.tags, TAGNAMES['3'])) { let routingInfo = tagsItems(data.tags, TAGNAMES['3']) routingInfo.forEach(route => { if (route.pubkey === undefined || route.short_channel_id === undefined || route.fee_base_msat === undefined || route.fee_proportional_millionths === undefined || route.cltv_expiry_delta === undefined) { throw new Error('Routing info is incomplete') } if (!secp256k1.publicKeyVerify(hexToBuffer(route.pubkey))) { throw new Error('Routing info pubkey is not a valid pubkey') } let shortId = hexToBuffer(route.short_channel_id) if (!(shortId instanceof Buffer) || shortId.length !== 8) { throw new Error('Routing info short channel id must be 8 bytes') } if (typeof route.fee_base_msat !== 'number' || Math.floor(route.fee_base_msat) !== route.fee_base_msat) { throw new Error('Routing info fee base msat is not an integer') } if (typeof route.fee_proportional_millionths !== 'number' || Math.floor(route.fee_proportional_millionths) !== route.fee_proportional_millionths) { throw new Error('Routing info fee proportional millionths is not an integer') } if (typeof route.cltv_expiry_delta !== 'number' || Math.floor(route.cltv_expiry_delta) !== route.cltv_expiry_delta) { throw new Error('Routing info cltv expiry delta is not an integer') } }) } let prefix = 'ln' prefix += coinTypeObj.bech32 let hrpString // calculate the smallest possible integer (removing zeroes) and add the best // divisor (m = milli, u = micro, n = nano, p = pico) if (data.millisatoshis && data.satoshis) { hrpString = millisatToHrp(new BN(data.millisatoshis, 10)) let hrpStringSat = satToHrp(new BN(data.satoshis, 10)) if (hrpStringSat !== hrpString) { throw new Error('satoshis and millisatoshis do not match') } } else if (data.millisatoshis) { hrpString = millisatToHrp(new BN(data.millisatoshis, 10)) } else if (data.satoshis) { hrpString = satToHrp(new BN(data.satoshis, 10)) } else { hrpString = '' } // bech32 human readable part is lnbc2500m (ln + coinbech32 + satoshis (optional)) // lnbc or lntb would be valid as well. (no value specified) prefix += hrpString // timestamp converted to 5 bit number array (left padded with 0 bits, NOT right padded) let timestampWords = intBEToWords(data.timestamp) let tags = data.tags let tagWords = [] tags.forEach(tag => { const possibleTagNames = Object.keys(TAGENCODERS) if (canReconstruct) possibleTagNames.push(unknownTagName) // check if the tagName exists in the encoders object, if not throw Error. if (possibleTagNames.indexOf(tag.tagName) === -1) { throw new Error('Unknown tag key: ' + tag.tagName) } let words if (tag.tagName !== unknownTagName) { // each tag starts with 1 word code for the tag tagWords.push(TAGCODES[tag.tagName]) const encoder = TAGENCODERS[tag.tagName] words = encoder(tag.data) } else { let result = unknownEncoder(tag.data) tagWords.push(result.tagCode) words = result.words } // after the tag code, 2 words are used to store the length (in 5 bit words) of the tag data // (also left padded, most integers are left padded while buffers are right padded) tagWords = tagWords.concat([0].concat(intBEToWords(words.length)).slice(-2)) // then append the tag data words tagWords = tagWords.concat(words) }) // the data part of the bech32 is TIMESTAMP || TAGS || SIGNATURE // currently dataWords = TIMESTAMP || TAGS let dataWords = timestampWords.concat(tagWords) // the preimage for the signing data is the buffer of the prefix concatenated // with the buffer conversion of the data words excluding the signature // (right padded with 0 bits) let toSign = Buffer.concat([Buffer.from(prefix, 'utf8'), Buffer.from(convert(dataWords, 5, 8))]) // single SHA256 hash for the signature let payReqHash = sha256(toSign) // signature is 64 bytes (32 byte r value and 32 byte s value concatenated) // PLUS one extra byte appended to the right with the recoveryID in [0,1,2,3] // Then convert to 5 bit words with right padding 0 bits. let sigWords if (canReconstruct) { /* Since BOLT11 does not require a payee_node_key tag in the specs, most parsers will have to recover the pubkey from the signature To ensure the tag data has been provided in the right order etc. we should check that the data we got and the node key given match when reconstructing a payment request from given signature and recoveryID. However, if a privatekey is given, the caller is the privkey owner. Earlier we check if the private key matches the payee node key IF they gave one. */ if (nodePublicKey) { let recoveredPubkey = secp256k1.recover(payReqHash, Buffer.from(data.signature, 'hex'), data.recoveryFlag, true) if (nodePublicKey && !nodePublicKey.equals(recoveredPubkey)) { throw new Error('Signature, message, and recoveryID did not produce the same pubkey as payeeNodeKey') } sigWords = hexToWord(data.signature + '0' + data.recoveryFlag) } else { throw new Error('Reconstruction with signature and recoveryID requires payeeNodeKey to verify correctness of input data.') } } if (sigWords) dataWords = dataWords.concat(sigWords) if (tagsContainItem(data.tags, TAGNAMES['6'])) { data.timeExpireDate = data.timestamp + tagsItems(data.tags, TAGNAMES['6']) data.timeExpireDateString = new Date(data.timeExpireDate * 1000).toISOString() } data.timestampString = new Date(data.timestamp * 1000).toISOString() data.paymentRequest = data.complete ? bech32.encode(prefix, dataWords, Number.MAX_SAFE_INTEGER) : '' data.prefix = prefix data.wordsTemp = bech32.encode('temp', dataWords, Number.MAX_SAFE_INTEGER) data.complete = !!sigWords // payment requests get pretty long. Nothing in the spec says anything about length. // Even though bech32 loses error correction power over 1023 characters. return orderKeys(data) } // decode will only have extra comments that aren't covered in encode comments. // also if anything is hard to read I'll comment. function decode (paymentRequest) { if (typeof paymentRequest !== 'string') throw new Error('Lightning Payment Request must be string') let lightningPrefix = '' // replace the 'lightning:' without affecting the case of the rest of the string if (paymentRequest.slice(0, 10).toLowerCase() === 'lightning:') { lightningPrefix = paymentRequest.slice(0, 10) // prefix in the original case LiGthNiNg: paymentRequest = paymentRequest.slice(10) } if (paymentRequest.slice(0, 2).toLowerCase() !== 'ln') throw new Error('Not a proper lightning payment request') let decoded = bech32.decode(paymentRequest, Number.MAX_SAFE_INTEGER) paymentRequest = paymentRequest.toLowerCase() let prefix = decoded.prefix let words = decoded.words // signature is always 104 words on the end // cutting off at the beginning helps since there's no way to tell // ahead of time how many tags there are. let sigWords = words.slice(-104) // grabbing a copy of the words for later, words will be sliced as we parse. let wordsNoSig = words.slice(0, -104) words = words.slice(0, -104) let sigBuffer = wordsToBuffer(sigWords, true) let recoveryFlag = sigBuffer.slice(-1)[0] sigBuffer = sigBuffer.slice(0, -1) if (!(recoveryFlag in [0, 1, 2, 3]) || sigBuffer.length !== 64) { throw new Error('Signature is missing or incorrect') } // Without reverse lookups, can't say that the multipier at the end must // have a number before it, so instead we parse, and if the second group // doesn't have anything, there's a good chance the last letter of the // coin type got captured by the third group, so just re-regex without // the number. let prefixMatches = prefix.match(/^ln(\S+?)(\d*)([a-zA-Z]?)$/) if (prefixMatches && !prefixMatches[2]) prefixMatches = prefix.match(/^ln(\S+)$/) if (!prefixMatches) { throw new Error('Not a proper lightning payment request') } let coinType = prefixMatches[1] let coinNetwork if (BECH32CODES[coinType]) { coinType = BECH32CODES[coinType] coinNetwork = BITCOINJS_NETWORK_INFO[coinType] } else { throw new Error('Unknown coin bech32 prefix') } let value = prefixMatches[2] let satoshis, millisatoshis, removeSatoshis if (value) { let divisor = prefixMatches[3] try { satoshis = parseInt(hrpToSat(value + divisor, true)) } catch (e) { satoshis = null removeSatoshis = true } millisatoshis = hrpToMillisat(value + divisor, true) } else { satoshis = null millisatoshis = null } // reminder: left padded 0 bits let timestamp = wordsToIntBE(words.slice(0, 7)) let timestampString = new Date(timestamp * 1000).toISOString() words = words.slice(7) // trim off the left 7 words let tags = [] let tagName, parser, tagLength, tagWords // we have no tag count to go on, so just keep hacking off words // until we have none. while (words.length > 0) { let tagCode = words[0].toString() tagName = TAGNAMES[tagCode] || unknownTagName parser = TAGPARSERS[tagCode] || getUnknownParser(tagCode) words = words.slice(1) tagLength = wordsToIntBE(words.slice(0, 2)) words = words.slice(2) tagWords = words.slice(0, tagLength) words = words.slice(tagLength) // See: parsers for more comments tags.push({ tagName, data: parser(tagWords, coinNetwork) // only fallback address needs coinNetwork }) } let timeExpireDate, timeExpireDateString // be kind and provide an absolute expiration date. // good for logs if (tagsContainItem(tags, TAGNAMES['6'])) { timeExpireDate = timestamp + tagsItems(tags, TAGNAMES['6']) timeExpireDateString = new Date(timeExpireDate * 1000).toISOString() } let toSign = Buffer.concat([Buffer.from(prefix, 'utf8'), Buffer.from(convert(wordsNoSig, 5, 8))]) let payReqHash = sha256(toSign) let sigPubkey = secp256k1.recover(payReqHash, sigBuffer, recoveryFlag, true) if (tagsContainItem(tags, TAGNAMES['19']) && tagsItems(tags, TAGNAMES['19']) !== sigPubkey.toString('hex')) { throw new Error('Lightning Payment Request signature pubkey does not match payee pubkey') } let finalResult = { paymentRequest, complete: true, prefix, wordsTemp: bech32.encode('temp', wordsNoSig.concat(sigWords), Number.MAX_SAFE_INTEGER), coinType, satoshis, millisatoshis, timestamp, timestampString, payeeNodeKey: sigPubkey.toString('hex'), signature: sigBuffer.toString('hex'), recoveryFlag, tags } if (removeSatoshis) { delete finalResult['satoshis'] } if (timeExpireDate) { finalResult = Object.assign(finalResult, {timeExpireDate, timeExpireDateString}) } finalResult.paymentRequest = lightningPrefix.concat(finalResult.paymentRequest) return orderKeys(finalResult) } module.exports = { encode, decode, sign, satToHrp, millisatToHrp, hrpToSat, hrpToMillisat }