invoice: Add invoice package
The invoice package can be used to encoded and decode invoices in the format defined in BOLT-0011. This format utilizes bech32 encoding to create invoices that can be shared and understood by the different Lightning implementations.
This commit is contained in:
parent
ff4ca664e3
commit
7662ea5d4d
158
invoice/amountunits.go
Normal file
158
invoice/amountunits.go
Normal file
@ -0,0 +1,158 @@
|
||||
package invoice
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
var (
|
||||
// toMSat is a map from a unit to a function that converts an amount
|
||||
// of that unit to millisatoshis.
|
||||
toMSat = map[byte]func(uint64) (lnwire.MilliSatoshi, error){
|
||||
'm': mBtcToMSat,
|
||||
'u': uBtcToMSat,
|
||||
'n': nBtcToMSat,
|
||||
'p': pBtcToMSat,
|
||||
}
|
||||
|
||||
// fromMSat is a map from a unit to a function that converts an amount
|
||||
// in millisatoshis to an amount of that unit.
|
||||
fromMSat = map[byte]func(lnwire.MilliSatoshi) (uint64, error){
|
||||
'm': mSatToMBtc,
|
||||
'u': mSatToUBtc,
|
||||
'n': mSatToNBtc,
|
||||
'p': mSatToPBtc,
|
||||
}
|
||||
)
|
||||
|
||||
// mBtcToMSat converts the given amount in milliBTC to millisatoshis.
|
||||
func mBtcToMSat(m uint64) (lnwire.MilliSatoshi, error) {
|
||||
return lnwire.MilliSatoshi(m) * 100000000, nil
|
||||
}
|
||||
|
||||
// uBtcToMSat converts the given amount in microBTC to millisatoshis.
|
||||
func uBtcToMSat(u uint64) (lnwire.MilliSatoshi, error) {
|
||||
return lnwire.MilliSatoshi(u * 100000), nil
|
||||
}
|
||||
|
||||
// nBtcToMSat converts the given amount in nanoBTC to millisatoshis.
|
||||
func nBtcToMSat(n uint64) (lnwire.MilliSatoshi, error) {
|
||||
return lnwire.MilliSatoshi(n * 100), nil
|
||||
}
|
||||
|
||||
// pBtcToMSat converts the given amount in picoBTC to millisatoshis.
|
||||
func pBtcToMSat(p uint64) (lnwire.MilliSatoshi, error) {
|
||||
if p < 10 {
|
||||
return 0, fmt.Errorf("minimum amount is 10p")
|
||||
}
|
||||
if p%10 != 0 {
|
||||
return 0, fmt.Errorf("amount %d pBTC not expressible in msat",
|
||||
p)
|
||||
}
|
||||
return lnwire.MilliSatoshi(p / 10), nil
|
||||
}
|
||||
|
||||
// mSatToMBtc converts the given amount in millisatoshis to milliBTC.
|
||||
func mSatToMBtc(msat lnwire.MilliSatoshi) (uint64, error) {
|
||||
if msat%100000000 != 0 {
|
||||
return 0, fmt.Errorf("%d msat not expressible "+
|
||||
"in mBTC", msat)
|
||||
}
|
||||
return uint64(msat / 100000000), nil
|
||||
}
|
||||
|
||||
// mSatToUBtc converts the given amount in millisatoshis to microBTC.
|
||||
func mSatToUBtc(msat lnwire.MilliSatoshi) (uint64, error) {
|
||||
if msat%100000 != 0 {
|
||||
return 0, fmt.Errorf("%d msat not expressible "+
|
||||
"in uBTC", msat)
|
||||
}
|
||||
return uint64(msat / 100000), nil
|
||||
}
|
||||
|
||||
// mSatToNBtc converts the given amount in millisatoshis to nanoBTC.
|
||||
func mSatToNBtc(msat lnwire.MilliSatoshi) (uint64, error) {
|
||||
if msat%100 != 0 {
|
||||
return 0, fmt.Errorf("%d msat not expressible in nBTC", msat)
|
||||
}
|
||||
return uint64(msat / 100), nil
|
||||
}
|
||||
|
||||
// mSatToPBtc converts the given amount in milllisatoshis to picoBTC.
|
||||
func mSatToPBtc(msat lnwire.MilliSatoshi) (uint64, error) {
|
||||
return uint64(msat * 10), nil
|
||||
}
|
||||
|
||||
// decodeAmount returns the amount encoded by the provided string in
|
||||
// milllisatoshi.
|
||||
func decodeAmount(amount string) (lnwire.MilliSatoshi, error) {
|
||||
if len(amount) < 1 {
|
||||
return 0, fmt.Errorf("amount must be non-empty")
|
||||
}
|
||||
|
||||
// If last character is a digit, then the amount can just be
|
||||
// interpreted as BTC.
|
||||
char := amount[len(amount)-1]
|
||||
digit := char - '0'
|
||||
if digit >= 0 && digit <= 9 {
|
||||
btc, err := strconv.ParseUint(amount, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return lnwire.MilliSatoshi(btc) * mSatPerBtc, nil
|
||||
}
|
||||
|
||||
// If not a digit, it must be part of the known units.
|
||||
conv, ok := toMSat[char]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("unknown multiplier %c", char)
|
||||
}
|
||||
|
||||
// Known unit.
|
||||
num := amount[:len(amount)-1]
|
||||
if len(num) < 1 {
|
||||
return 0, fmt.Errorf("number must be non-empty")
|
||||
}
|
||||
|
||||
am, err := strconv.ParseUint(num, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return conv(am)
|
||||
}
|
||||
|
||||
// encodeAmount encodes the provided millisatoshi amount using as few characters
|
||||
// as possible.
|
||||
func encodeAmount(msat lnwire.MilliSatoshi) (string, error) {
|
||||
// If possible to express in BTC, that will always be the shortest
|
||||
// representation.
|
||||
if msat%mSatPerBtc == 0 {
|
||||
return strconv.FormatInt(int64(msat/mSatPerBtc), 10), nil
|
||||
}
|
||||
|
||||
// Should always be expressible in pico BTC.
|
||||
pico, err := fromMSat['p'](msat)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to express %d msat as pBTC: %v",
|
||||
msat, err)
|
||||
}
|
||||
shortened := strconv.FormatUint(pico, 10) + "p"
|
||||
for unit, conv := range fromMSat {
|
||||
am, err := conv(msat)
|
||||
if err != nil {
|
||||
// Not expressible using this unit.
|
||||
continue
|
||||
}
|
||||
|
||||
// Save the shortest found representation.
|
||||
str := strconv.FormatUint(am, 10) + string(unit)
|
||||
if len(str) < len(shortened) {
|
||||
shortened = str
|
||||
}
|
||||
}
|
||||
|
||||
return shortened, nil
|
||||
}
|
168
invoice/bech32.go
Normal file
168
invoice/bech32.go
Normal file
@ -0,0 +1,168 @@
|
||||
package invoice
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||
|
||||
var gen = []int{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}
|
||||
|
||||
// NOTE: This method it a slight modification of the method bech32.Decode found
|
||||
// btcutil, allowing strings to be more than 90 characters.
|
||||
|
||||
// decodeBech32 decodes a bech32 encoded string, returning the human-readable
|
||||
// part and the data part excluding the checksum.
|
||||
// Note: the data will be base32 encoded, that is each element of the returned
|
||||
// byte array will encode 5 bits of data. Use the ConvertBits method to convert
|
||||
// this to 8-bit representation.
|
||||
func decodeBech32(bech string) (string, []byte, error) {
|
||||
// The maximum allowed length for a bech32 string is 90. It must also
|
||||
// be at least 8 characters, since it needs a non-empty HRP, a
|
||||
// separator, and a 6 character checksum.
|
||||
// NB: The 90 character check specified in BIP173 is skipped here, to
|
||||
// allow strings longer than 90 characters.
|
||||
if len(bech) < 8 {
|
||||
return "", nil, fmt.Errorf("invalid bech32 string length %d",
|
||||
len(bech))
|
||||
}
|
||||
// Only ASCII characters between 33 and 126 are allowed.
|
||||
for i := 0; i < len(bech); i++ {
|
||||
if bech[i] < 33 || bech[i] > 126 {
|
||||
return "", nil, fmt.Errorf("invalid character in "+
|
||||
"string: '%c'", bech[i])
|
||||
}
|
||||
}
|
||||
|
||||
// The characters must be either all lowercase or all uppercase.
|
||||
lower := strings.ToLower(bech)
|
||||
upper := strings.ToUpper(bech)
|
||||
if bech != lower && bech != upper {
|
||||
return "", nil, fmt.Errorf("string not all lowercase or all " +
|
||||
"uppercase")
|
||||
}
|
||||
|
||||
// We'll work with the lowercase string from now on.
|
||||
bech = lower
|
||||
|
||||
// The string is invalid if the last '1' is non-existent, it is the
|
||||
// first character of the string (no human-readable part) or one of the
|
||||
// last 6 characters of the string (since checksum cannot contain '1'),
|
||||
// or if the string is more than 90 characters in total.
|
||||
one := strings.LastIndexByte(bech, '1')
|
||||
if one < 1 || one+7 > len(bech) {
|
||||
return "", nil, fmt.Errorf("invalid index of 1")
|
||||
}
|
||||
|
||||
// The human-readable part is everything before the last '1'.
|
||||
hrp := bech[:one]
|
||||
data := bech[one+1:]
|
||||
|
||||
// Each character corresponds to the byte with value of the index in
|
||||
// 'charset'.
|
||||
decoded, err := toBytes(data)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed converting data to bytes: "+
|
||||
"%v", err)
|
||||
}
|
||||
|
||||
if !bech32VerifyChecksum(hrp, decoded) {
|
||||
moreInfo := ""
|
||||
checksum := bech[len(bech)-6:]
|
||||
expected, err := toChars(bech32Checksum(hrp,
|
||||
decoded[:len(decoded)-6]))
|
||||
if err == nil {
|
||||
moreInfo = fmt.Sprintf("Expected %v, got %v.",
|
||||
expected, checksum)
|
||||
}
|
||||
return "", nil, fmt.Errorf("checksum failed. " + moreInfo)
|
||||
}
|
||||
|
||||
// We exclude the last 6 bytes, which is the checksum.
|
||||
return hrp, decoded[:len(decoded)-6], nil
|
||||
}
|
||||
|
||||
// toBytes converts each character in the string 'chars' to the value of the
|
||||
// index of the correspoding character in 'charset'.
|
||||
func toBytes(chars string) ([]byte, error) {
|
||||
decoded := make([]byte, 0, len(chars))
|
||||
for i := 0; i < len(chars); i++ {
|
||||
index := strings.IndexByte(charset, chars[i])
|
||||
if index < 0 {
|
||||
return nil, fmt.Errorf("invalid character not part of "+
|
||||
"charset: %v", chars[i])
|
||||
}
|
||||
decoded = append(decoded, byte(index))
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
// toChars converts the byte slice 'data' to a string where each byte in 'data'
|
||||
// encodes the index of a character in 'charset'.
|
||||
func toChars(data []byte) (string, error) {
|
||||
result := make([]byte, 0, len(data))
|
||||
for _, b := range data {
|
||||
if int(b) >= len(charset) {
|
||||
return "", fmt.Errorf("invalid data byte: %v", b)
|
||||
}
|
||||
result = append(result, charset[b])
|
||||
}
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
// For more details on the checksum calculation, please refer to BIP 173.
|
||||
func bech32Checksum(hrp string, data []byte) []byte {
|
||||
// Convert the bytes to list of integers, as this is needed for the
|
||||
// checksum calculation.
|
||||
integers := make([]int, len(data))
|
||||
for i, b := range data {
|
||||
integers[i] = int(b)
|
||||
}
|
||||
values := append(bech32HrpExpand(hrp), integers...)
|
||||
values = append(values, []int{0, 0, 0, 0, 0, 0}...)
|
||||
polymod := bech32Polymod(values) ^ 1
|
||||
var res []byte
|
||||
for i := 0; i < 6; i++ {
|
||||
res = append(res, byte((polymod>>uint(5*(5-i)))&31))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// For more details on the polymod calculation, please refer to BIP 173.
|
||||
func bech32Polymod(values []int) int {
|
||||
chk := 1
|
||||
for _, v := range values {
|
||||
b := chk >> 25
|
||||
chk = (chk&0x1ffffff)<<5 ^ v
|
||||
for i := 0; i < 5; i++ {
|
||||
if (b>>uint(i))&1 == 1 {
|
||||
chk ^= gen[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return chk
|
||||
}
|
||||
|
||||
// For more details on HRP expansion, please refer to BIP 173.
|
||||
func bech32HrpExpand(hrp string) []int {
|
||||
v := make([]int, 0, len(hrp)*2+1)
|
||||
for i := 0; i < len(hrp); i++ {
|
||||
v = append(v, int(hrp[i]>>5))
|
||||
}
|
||||
v = append(v, 0)
|
||||
for i := 0; i < len(hrp); i++ {
|
||||
v = append(v, int(hrp[i]&31))
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// For more details on the checksum verification, please refer to BIP 173.
|
||||
func bech32VerifyChecksum(hrp string, data []byte) bool {
|
||||
integers := make([]int, len(data))
|
||||
for i, b := range data {
|
||||
integers[i] = int(b)
|
||||
}
|
||||
concat := append(bech32HrpExpand(hrp), integers...)
|
||||
return bech32Polymod(concat) == 1
|
||||
}
|
933
invoice/invoice.go
Normal file
933
invoice/invoice.go
Normal file
@ -0,0 +1,933 @@
|
||||
package invoice
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/roasbeef/btcd/btcec"
|
||||
"github.com/roasbeef/btcd/chaincfg"
|
||||
"github.com/roasbeef/btcd/chaincfg/chainhash"
|
||||
"github.com/roasbeef/btcutil"
|
||||
"github.com/roasbeef/btcutil/bech32"
|
||||
)
|
||||
|
||||
const (
|
||||
// mSatPerBtc is the number of millisatoshis in 1 BTC.
|
||||
mSatPerBtc = 100000000000
|
||||
|
||||
// signatureBase32Len is the number of 5-bit groups needed to encode
|
||||
// the 512 bit signature + 8 bit recovery ID.
|
||||
signatureBase32Len = 104
|
||||
|
||||
// timestampBase32Len is the number of 5-bit groups needed to encode
|
||||
// the 35-bit timestamp.
|
||||
timestampBase32Len = 7
|
||||
|
||||
// hashBase32Len is the number of 5-bit groups needed to encode a
|
||||
// 256-bit hash. Note that the last group will be padded with zeroes.
|
||||
hashBase32Len = 52
|
||||
|
||||
// pubKeyBase32Len is the number of 5-bit groups needed to encode a
|
||||
// 33-byte compressed pubkey. Note that the last group will be padded
|
||||
// with zeroes.
|
||||
pubKeyBase32Len = 53
|
||||
|
||||
// The following byte values correspond to the supported field types.
|
||||
// The field name is the character representing that 5-bit value in the
|
||||
// bech32 string.
|
||||
|
||||
// fieldTypeP is the field containing the payment hash.
|
||||
fieldTypeP = 1
|
||||
|
||||
// fieldTypeD contains a short description of the payment.
|
||||
fieldTypeD = 13
|
||||
|
||||
// fieldTypeN contains the pubkey of the target node.
|
||||
fieldTypeN = 19
|
||||
|
||||
// fieldTypeH contains the hash of a description of the payment.
|
||||
fieldTypeH = 23
|
||||
|
||||
// fieldTypeX contains the expiry in seconds of the invoice.
|
||||
fieldTypeX = 6
|
||||
|
||||
// fieldTypeF contains a fallback on-chain address.
|
||||
fieldTypeF = 9
|
||||
|
||||
// fieldTypeR contains extra routing information.
|
||||
fieldTypeR = 3
|
||||
)
|
||||
|
||||
// MessageSigner is passed to the Encode method to provide a signature
|
||||
// corresponding to the node's pubkey.
|
||||
type MessageSigner struct {
|
||||
|
||||
// SignCompact signs the passed hash with the node's privkey. The
|
||||
// returned signature should be 65 bytes, where the last 64 are the
|
||||
// compact signature, and the first one is a header byte. This is the
|
||||
// format returned by btcec.SignCompact.
|
||||
SignCompact func(hash []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// Invoice represents a decoded invoice, or to-be-encoded invoice. Some of the
|
||||
// fields are optional, and will only be non-nil if the invoice this was parsed
|
||||
// from contains that field. When encoding, only the non-nil fields will be
|
||||
// added to the encoded invoice.
|
||||
type Invoice struct {
|
||||
// Net specifies what network this Lightning invoice is meant for.
|
||||
Net *chaincfg.Params
|
||||
|
||||
// MilliSat specifies the amount of this invoice in millisatoshi.
|
||||
// Optional.
|
||||
MilliSat *lnwire.MilliSatoshi
|
||||
|
||||
// Timestamp specifies the time this invoice was created.
|
||||
// Mandatory
|
||||
Timestamp time.Time
|
||||
|
||||
// PaymentHash is the payment hash to be used for a payment to this
|
||||
// invoice.
|
||||
PaymentHash *[32]byte
|
||||
|
||||
// Destination is the public key of the target node. This will always
|
||||
// be set after decoding, and can optionally be set before encoding to
|
||||
// include the pubkey as an 'n' field. If this is not set before
|
||||
// encoding then the destination pubkey won't be added as an 'n' field,
|
||||
// and the pubkey will be extracted from the signature during decoding.
|
||||
Destination *btcec.PublicKey
|
||||
|
||||
// Description is a short description of the purpose of this invoice.
|
||||
// Optional. Non-nil iff DescriptionHash is nil.
|
||||
Description *string
|
||||
|
||||
// DescriptionHash is the SHA256 hash of a description of the purpose of
|
||||
// this invoice.
|
||||
// Optional. Non-nil iff Description is nil.
|
||||
DescriptionHash *[32]byte
|
||||
|
||||
// Expiry specifies the timespan this invoice will be valid.
|
||||
// Optional. If not set, a default expiry of 60 min will be implied.
|
||||
Expiry *time.Time
|
||||
|
||||
// FallbackAddr is an on-chain address that can be used for payment in
|
||||
// case the Lightning payment fails.
|
||||
// Optional.
|
||||
FallbackAddr btcutil.Address
|
||||
|
||||
// RoutingInfo is one or more entries containing extra routing
|
||||
// information for a private route to the target node.
|
||||
// Optional.
|
||||
RoutingInfo []ExtraRoutingInfo
|
||||
}
|
||||
|
||||
// ExtraRoutingInfo holds the information needed to route a payment along one
|
||||
// private channel.
|
||||
type ExtraRoutingInfo struct {
|
||||
// PubKey is the public key of the node at the start of this channel.
|
||||
PubKey *btcec.PublicKey
|
||||
|
||||
// ShortChanID is the channel ID of the channel.
|
||||
ShortChanID uint64
|
||||
|
||||
// Fee is the fee required for routing along this channel.
|
||||
Fee uint64
|
||||
|
||||
// CltvExpDelta is this channel's cltv expiry delta.
|
||||
CltvExpDelta uint16
|
||||
}
|
||||
|
||||
// Amount is a functional option that allows callers of NewInvoice to set the
|
||||
// amount in millisatoshis that the Invoice should encode.
|
||||
func Amount(milliSat lnwire.MilliSatoshi) func(*Invoice) {
|
||||
return func(i *Invoice) {
|
||||
i.MilliSat = &milliSat
|
||||
}
|
||||
}
|
||||
|
||||
// Destination is a functional option that allows callers of NewInvoice to
|
||||
// explicitly set the pubkey of the Invoice's destination node.
|
||||
func Destination(destination *btcec.PublicKey) func(*Invoice) {
|
||||
return func(i *Invoice) {
|
||||
i.Destination = destination
|
||||
}
|
||||
}
|
||||
|
||||
// Description is a functional option that allows callers of NewInvoice to set
|
||||
// the payment description of the created Invoice.
|
||||
// Note: Must be used if and only if DescriptionHash is not used.
|
||||
func Description(description string) func(*Invoice) {
|
||||
return func(i *Invoice) {
|
||||
i.Description = &description
|
||||
}
|
||||
}
|
||||
|
||||
// DescriptionHash is a functional option that allows callers of NewInvoice to
|
||||
// set the payment description hash of the created Invoice.
|
||||
// Note: Must be used if and only if Description is not used.
|
||||
func DescriptionHash(descriptionHash [32]byte) func(*Invoice) {
|
||||
return func(i *Invoice) {
|
||||
i.DescriptionHash = &descriptionHash
|
||||
}
|
||||
}
|
||||
|
||||
// Expiry is a functional option that allows callers of NewInvoice to set the
|
||||
// expiry of the created Invoice. If not set, a default expiry of 60 min will
|
||||
// be implied.
|
||||
func Expiry(expiry time.Time) func(*Invoice) {
|
||||
return func(i *Invoice) {
|
||||
i.Expiry = &expiry
|
||||
}
|
||||
}
|
||||
|
||||
// FallbackAddr is a functional option that allows callers of NewInvoice to set
|
||||
// the Invoice's fallback on-chain address that can be used for payment in case
|
||||
// the Lightning payment fails
|
||||
func FallbackAddr(fallbackAddr btcutil.Address) func(*Invoice) {
|
||||
return func(i *Invoice) {
|
||||
i.FallbackAddr = fallbackAddr
|
||||
}
|
||||
}
|
||||
|
||||
// RoutingInfo is a functional option that allows callers of NewInvoice to set
|
||||
// one or more entries containing extra routing information for a private route
|
||||
// to the target node.
|
||||
func RoutingInfo(routingInfo []ExtraRoutingInfo) func(*Invoice) {
|
||||
return func(i *Invoice) {
|
||||
i.RoutingInfo = routingInfo
|
||||
}
|
||||
}
|
||||
|
||||
// NewInvoice creates a new Invoice object. The last parameter is a set of
|
||||
// variadic argumements for setting optional fields of the invoice.
|
||||
// Note: Either Description or DescriptionHash must be provided for the Invoice
|
||||
// to be considered valid.
|
||||
func NewInvoice(net *chaincfg.Params, paymentHash [32]byte,
|
||||
timestamp time.Time, options ...func(*Invoice)) (*Invoice, error) {
|
||||
|
||||
invoice := &Invoice{
|
||||
Net: net,
|
||||
PaymentHash: &paymentHash,
|
||||
Timestamp: timestamp,
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
option(invoice)
|
||||
}
|
||||
|
||||
if err := validateInvoice(invoice); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return invoice, nil
|
||||
}
|
||||
|
||||
// Decode parses the provided encoded invoice, and returns a decoded Invoice in
|
||||
// case it is valid by BOLT-0011.
|
||||
func Decode(invoice string) (*Invoice, error) {
|
||||
decodedInvoice := Invoice{}
|
||||
|
||||
// Decode the invoice using the modified bech32 decoder.
|
||||
hrp, data, err := decodeBech32(invoice)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We expect the human-readable part to at least have ln + two chars
|
||||
// encoding the network.
|
||||
if len(hrp) < 4 {
|
||||
return nil, fmt.Errorf("hrp too short")
|
||||
}
|
||||
|
||||
// First two characters of HRP should be "ln".
|
||||
if hrp[:2] != "ln" {
|
||||
return nil, fmt.Errorf("prefix should be \"ln\"")
|
||||
}
|
||||
|
||||
// The next characters should be a valid prefix for a segwit BIP173
|
||||
// address. This will also determine which network this invoice is
|
||||
// meant for.
|
||||
var net *chaincfg.Params
|
||||
if strings.HasPrefix(hrp[2:], chaincfg.MainNetParams.Bech32HRPSegwit) {
|
||||
net = &chaincfg.MainNetParams
|
||||
} else if strings.HasPrefix(hrp[2:], chaincfg.TestNet3Params.Bech32HRPSegwit) {
|
||||
net = &chaincfg.TestNet3Params
|
||||
} else if strings.HasPrefix(hrp[2:], chaincfg.SimNetParams.Bech32HRPSegwit) {
|
||||
net = &chaincfg.SimNetParams
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown network")
|
||||
}
|
||||
decodedInvoice.Net = net
|
||||
|
||||
// Optionally, if there's anything left of the HRP, it encodes the
|
||||
// payment amount.
|
||||
if len(hrp) > 4 {
|
||||
amount, err := decodeAmount(hrp[4:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decodedInvoice.MilliSat = &amount
|
||||
}
|
||||
|
||||
// Everything except the last 520 bits of the data encodes the invoice's
|
||||
// timestamp and tagged fields.
|
||||
invoiceData := data[:len(data)-signatureBase32Len]
|
||||
|
||||
// Parse the timestamp and tagged fields, and fill the Invoice struct.
|
||||
if err := parseData(&decodedInvoice, invoiceData, net); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The last 520 bits (104 groups) make up the signature.
|
||||
sigBase32 := data[len(data)-signatureBase32Len:]
|
||||
sigBase256, err := bech32.ConvertBits(sigBase32, 5, 8, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sigBytes [64]byte
|
||||
copy(sigBytes[:], sigBase256[:64])
|
||||
recoveryID := sigBase256[64]
|
||||
|
||||
// The signature is over the hrp + the data the invoice, encoded in
|
||||
// base 256.
|
||||
taggedDataBytes, err := bech32.ConvertBits(invoiceData, 5, 8, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
toSign := append([]byte(hrp), taggedDataBytes...)
|
||||
|
||||
// We expect the signature to be over the single SHA-256 hash of that
|
||||
// data.
|
||||
hash := chainhash.HashB(toSign)
|
||||
|
||||
// If the destination pubkey was provided as a tagged field, use that
|
||||
// to verify the signature, if not do public key recovery.
|
||||
if decodedInvoice.Destination != nil {
|
||||
var signature *btcec.Signature
|
||||
err := lnwire.DeserializeSigFromWire(&signature, sigBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to deserialize "+
|
||||
"signature: %v", err)
|
||||
}
|
||||
if !signature.Verify(hash, decodedInvoice.Destination) {
|
||||
return nil, fmt.Errorf("invalid invoice signature")
|
||||
}
|
||||
} else {
|
||||
headerByte := recoveryID + 27 + 4
|
||||
compactSign := append([]byte{headerByte}, sigBytes[:]...)
|
||||
pubkey, _, err := btcec.RecoverCompact(btcec.S256(),
|
||||
compactSign, hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decodedInvoice.Destination = pubkey
|
||||
}
|
||||
|
||||
// Now that we have created the invoice, make sure it has the required
|
||||
// fields set.
|
||||
if err := validateInvoice(&decodedInvoice); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &decodedInvoice, nil
|
||||
}
|
||||
|
||||
// Encode takes the given MessageSigner and returns a string encoding this
|
||||
// invoice signed by the node key of the signer.
|
||||
func (invoice *Invoice) Encode(signer MessageSigner) (string, error) {
|
||||
// First check that this invoice is valid before starting the encoding.
|
||||
if err := validateInvoice(invoice); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// The buffer will encoded the invoice data using 5-bit groups (base32).
|
||||
var bufferBase32 bytes.Buffer
|
||||
|
||||
// The timestamp will be encoded using 35 bits, in base32.
|
||||
timestampBase32 := uint64ToBase32(uint64(invoice.Timestamp.Unix()))
|
||||
|
||||
// The timestamp must be exactly 35 bits, which means 7 groups. If it
|
||||
// can fit into fewer groups we add leading zero groups, if it is too
|
||||
// big we fail early, as there is not possible to encode it.
|
||||
if len(timestampBase32) > timestampBase32Len {
|
||||
return "", fmt.Errorf("timestamp too big: %d",
|
||||
invoice.Timestamp.Unix())
|
||||
}
|
||||
|
||||
// Add zero bytes to the first timestampBase32Len-len(timestampBase32)
|
||||
// groups, then add the non-zero groups.
|
||||
zeroes := make([]byte, timestampBase32Len-len(timestampBase32),
|
||||
timestampBase32Len-len(timestampBase32))
|
||||
_, err := bufferBase32.Write(zeroes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to write to buffer: %v", err)
|
||||
}
|
||||
_, err = bufferBase32.Write(timestampBase32)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to write to buffer: %v", err)
|
||||
}
|
||||
|
||||
// We now write the tagged fields to the buffer, which will fill the
|
||||
// rest of the data part before the signature.
|
||||
if err := writeTaggedFields(&bufferBase32, invoice); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// The human-readable part (hrp) is "ln" + net hrp + optional amount.
|
||||
hrp := "ln" + invoice.Net.Bech32HRPSegwit
|
||||
if invoice.MilliSat != nil {
|
||||
// Encode the amount using the fewest possible characters.
|
||||
am, err := encodeAmount(*invoice.MilliSat)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
hrp += am
|
||||
}
|
||||
|
||||
// The signature is over the single SHA-256 hash of the hrp + the
|
||||
// tagged fields encoded in base256.
|
||||
taggedFieldsBytes, err := bech32.ConvertBits(bufferBase32.Bytes(), 5, 8, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
toSign := append([]byte(hrp), taggedFieldsBytes...)
|
||||
hash := chainhash.HashB(toSign)
|
||||
|
||||
// We use compact signature format, and also encoded the recovery ID
|
||||
// such that a reader of the invoice can recover our pubkey from the
|
||||
// signature.
|
||||
sign, err := signer.SignCompact(hash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// From the header byte we can extract the recovery ID, and the last 64
|
||||
// bytes encode the signature.
|
||||
recoveryID := sign[0] - 27 - 4
|
||||
var sigBytes [64]byte
|
||||
copy(sigBytes[:], sign[1:])
|
||||
|
||||
// If the pubkey field was explicitly set, it must be set to the pubkey
|
||||
// used to create the signature.
|
||||
if invoice.Destination != nil {
|
||||
var signature *btcec.Signature
|
||||
err = lnwire.DeserializeSigFromWire(&signature, sigBytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to deserialize "+
|
||||
"signature: %v", err)
|
||||
}
|
||||
valid := signature.Verify(hash, invoice.Destination)
|
||||
if !valid {
|
||||
return "", fmt.Errorf("signature does not match " +
|
||||
"provided pubkey")
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the signature to base32 before writing it to the buffer.
|
||||
signBase32, err := bech32.ConvertBits(append(sigBytes[:], recoveryID), 8, 5, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
bufferBase32.Write(signBase32)
|
||||
|
||||
// Now we can create the bech32 encoded string from the base32 buffer.
|
||||
b32, err := bech32.Encode(hrp, bufferBase32.Bytes())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return b32, nil
|
||||
}
|
||||
|
||||
// validateInvoice does a sanity check of the provided Invoice, making sure it
|
||||
// has all the necessary fields set for it to be considered valid by BOLT-0011.
|
||||
func validateInvoice(invoice *Invoice) error {
|
||||
// The net must be set.
|
||||
if invoice.Net == nil {
|
||||
return fmt.Errorf("net params not set")
|
||||
}
|
||||
|
||||
// The invoice must contain a payment hash.
|
||||
if invoice.PaymentHash == nil {
|
||||
return fmt.Errorf("no payment hash found")
|
||||
}
|
||||
|
||||
// Either Description or DescriptionHash must be set, not both.
|
||||
if invoice.Description != nil && invoice.DescriptionHash != nil {
|
||||
return fmt.Errorf("both description and description hash set")
|
||||
}
|
||||
if invoice.Description == nil && invoice.DescriptionHash == nil {
|
||||
return fmt.Errorf("neither description nor description hash set")
|
||||
}
|
||||
|
||||
// Can have at most 20 extra hops for routing.
|
||||
if len(invoice.RoutingInfo) > 20 {
|
||||
return fmt.Errorf("too many extra hops: %d",
|
||||
len(invoice.RoutingInfo))
|
||||
}
|
||||
|
||||
// Check that we support the field lengths.
|
||||
if len(invoice.PaymentHash) != 32 {
|
||||
return fmt.Errorf("unsupported payment hash length: %d",
|
||||
len(invoice.PaymentHash))
|
||||
}
|
||||
|
||||
if invoice.DescriptionHash != nil && len(invoice.DescriptionHash) != 32 {
|
||||
return fmt.Errorf("unsupported description hash length: %d",
|
||||
len(invoice.DescriptionHash))
|
||||
}
|
||||
|
||||
if invoice.Destination != nil &&
|
||||
len(invoice.Destination.SerializeCompressed()) != 33 {
|
||||
return fmt.Errorf("unsupported pubkey length: %d",
|
||||
len(invoice.Destination.SerializeCompressed()))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseData parses the data part of the invoice. It expects base32 data
|
||||
// returned from the bech32.Decode method, except signature.
|
||||
func parseData(invoice *Invoice, data []byte, net *chaincfg.Params) error {
|
||||
// It must contain the timestamp, encoded using 35 bits (7 groups).
|
||||
if len(data) < timestampBase32Len {
|
||||
return fmt.Errorf("data too short: %d", len(data))
|
||||
}
|
||||
|
||||
// Timestamp: 35 bits, 7 groups.
|
||||
t, err := base32ToUint64(data[:7])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
invoice.Timestamp = time.Unix(int64(t), 0)
|
||||
|
||||
// The rest are tagged parts.
|
||||
tagData := data[7:]
|
||||
if err := parseTaggedFields(invoice, tagData, net); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseTimestamp converts a 35-bit timestamp (encoded in base32) to uint64.
|
||||
func parseTimestamp(data []byte) (uint64, error) {
|
||||
if len(data) != 7 {
|
||||
return 0, fmt.Errorf("timestamp must be 35 bits, was %d",
|
||||
len(data)*5)
|
||||
}
|
||||
|
||||
return base32ToUint64(data)
|
||||
}
|
||||
|
||||
// parseTaggedFields takes the base32 encoded tagged fields of the invoice, and
|
||||
// fills the Invoice struct accordingly.
|
||||
func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) error {
|
||||
index := 0
|
||||
for {
|
||||
// If less than 3 groups less, it cannot possibly contain more
|
||||
// interesting information, as we need the type (1 group) and
|
||||
// length (2 groups).
|
||||
if len(fields)-index < 3 {
|
||||
break
|
||||
}
|
||||
|
||||
typ := fields[index]
|
||||
dataLength := uint16(fields[index+1]<<5) | uint16(fields[index+2])
|
||||
|
||||
// If we don't have enough field data left to read this length,
|
||||
// return error.
|
||||
if len(fields) < index+3+int(dataLength) {
|
||||
return fmt.Errorf("invalid field length")
|
||||
}
|
||||
base32Data := fields[index+3 : index+3+int(dataLength)]
|
||||
|
||||
// Advance the index in preparation for the next iteration.
|
||||
index += 3 + int(dataLength)
|
||||
|
||||
switch typ {
|
||||
case fieldTypeP:
|
||||
if invoice.PaymentHash != nil {
|
||||
// We skip the field if we have already seen a
|
||||
// supported one.
|
||||
continue
|
||||
}
|
||||
|
||||
if dataLength != hashBase32Len {
|
||||
// Skipping unknown field length.
|
||||
continue
|
||||
}
|
||||
hash, err := bech32.ConvertBits(base32Data, 5, 8, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var pHash [32]byte
|
||||
copy(pHash[:], hash[:])
|
||||
invoice.PaymentHash = &pHash
|
||||
case fieldTypeD:
|
||||
if invoice.Description != nil {
|
||||
// We skip the field if we have already seen a
|
||||
// supported one.
|
||||
continue
|
||||
}
|
||||
|
||||
base256Data, err := bech32.ConvertBits(base32Data, 5, 8,
|
||||
false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
desc := string(base256Data)
|
||||
invoice.Description = &desc
|
||||
case fieldTypeN:
|
||||
if invoice.Destination != nil {
|
||||
// We skip the field if we have already seen a
|
||||
// supported one.
|
||||
continue
|
||||
}
|
||||
|
||||
if len(base32Data) != pubKeyBase32Len {
|
||||
// Skip unknown length.
|
||||
continue
|
||||
}
|
||||
|
||||
base256Data, err := bech32.ConvertBits(base32Data, 5, 8,
|
||||
false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
invoice.Destination, err = btcec.ParsePubKey(base256Data,
|
||||
btcec.S256())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case fieldTypeH:
|
||||
if invoice.DescriptionHash != nil {
|
||||
// We skip the field if we have already seen a
|
||||
// supported one.
|
||||
continue
|
||||
}
|
||||
|
||||
if len(base32Data) != hashBase32Len {
|
||||
// Skip unknown length.
|
||||
continue
|
||||
}
|
||||
hash, err := bech32.ConvertBits(base32Data, 5, 8, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var dHash [32]byte
|
||||
copy(dHash[:], hash[:])
|
||||
invoice.DescriptionHash = &dHash
|
||||
case fieldTypeX:
|
||||
if invoice.Expiry != nil {
|
||||
// We skip the field if we have already seen a
|
||||
// supported one.
|
||||
continue
|
||||
}
|
||||
|
||||
exp, err := base32ToUint64(base32Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
unix := time.Unix(int64(exp), 0)
|
||||
invoice.Expiry = &unix
|
||||
case fieldTypeF:
|
||||
if invoice.FallbackAddr != nil {
|
||||
// We skip the field if we have already seen a
|
||||
// supported one.
|
||||
continue
|
||||
}
|
||||
|
||||
var addr btcutil.Address
|
||||
version := base32Data[0]
|
||||
switch version {
|
||||
case 0:
|
||||
witness, err := bech32.ConvertBits(
|
||||
base32Data[1:], 5, 8, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch len(witness) {
|
||||
case 20:
|
||||
addr, err = btcutil.NewAddressWitnessPubKeyHash(
|
||||
witness, net)
|
||||
case 32:
|
||||
addr, err = btcutil.NewAddressWitnessScriptHash(
|
||||
witness, net)
|
||||
default:
|
||||
return fmt.Errorf("unknow witness "+
|
||||
"program length: %d", len(witness))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case 17:
|
||||
pkHash, err := bech32.ConvertBits(base32Data[1:],
|
||||
5, 8, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addr, err = btcutil.NewAddressPubKeyHash(pkHash,
|
||||
net)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case 18:
|
||||
scriptHash, err := bech32.ConvertBits(
|
||||
base32Data[1:], 5, 8, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addr, err = btcutil.NewAddressScriptHashFromHash(
|
||||
scriptHash, net)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
// Skipping unknown witness version.
|
||||
continue
|
||||
}
|
||||
invoice.FallbackAddr = addr
|
||||
case fieldTypeR:
|
||||
if invoice.RoutingInfo != nil {
|
||||
// We skip the field if we have already seen a
|
||||
// supported one.
|
||||
continue
|
||||
}
|
||||
|
||||
base256Data, err := bech32.ConvertBits(base32Data, 5, 8,
|
||||
false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for len(base256Data) > 0 {
|
||||
info := ExtraRoutingInfo{}
|
||||
info.PubKey, err = btcec.ParsePubKey(
|
||||
base256Data[:33], btcec.S256())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info.ShortChanID = binary.BigEndian.Uint64(
|
||||
base256Data[33:41])
|
||||
info.Fee = binary.BigEndian.Uint64(
|
||||
base256Data[41:49])
|
||||
info.CltvExpDelta = binary.BigEndian.Uint16(
|
||||
base256Data[49:51])
|
||||
invoice.RoutingInfo = append(
|
||||
invoice.RoutingInfo, info)
|
||||
base256Data = base256Data[51:]
|
||||
}
|
||||
default:
|
||||
// Ignore unknown type.
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeTaggedFields writes the non-nil tagged fields of the Invoice to the
|
||||
// base32 buffer.
|
||||
func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error {
|
||||
if invoice.PaymentHash != nil {
|
||||
// Convert 32 byte hash to 52 5-bit groups.
|
||||
base32, err := bech32.ConvertBits(invoice.PaymentHash[:], 8, 5,
|
||||
true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(base32) != hashBase32Len {
|
||||
return fmt.Errorf("invalid payment hash length: %d",
|
||||
len(invoice.PaymentHash))
|
||||
}
|
||||
|
||||
err = writeTaggedField(bufferBase32, fieldTypeP, base32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if invoice.Description != nil {
|
||||
base32, err := bech32.ConvertBits([]byte(*invoice.Description),
|
||||
8, 5, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = writeTaggedField(bufferBase32, fieldTypeD, base32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if invoice.DescriptionHash != nil {
|
||||
// Convert 32 byte hash to 52 5-bit groups.
|
||||
descBase32, err := bech32.ConvertBits(
|
||||
invoice.DescriptionHash[:], 8, 5, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(descBase32) != hashBase32Len {
|
||||
return fmt.Errorf("invalid description hash length: %d",
|
||||
len(invoice.DescriptionHash))
|
||||
}
|
||||
|
||||
err = writeTaggedField(bufferBase32, fieldTypeH, descBase32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if invoice.Expiry != nil {
|
||||
unix := invoice.Expiry.Unix()
|
||||
expiry := uint64ToBase32(uint64(unix))
|
||||
err := writeTaggedField(bufferBase32, fieldTypeX, expiry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if invoice.FallbackAddr != nil {
|
||||
var version byte
|
||||
switch addr := invoice.FallbackAddr.(type) {
|
||||
case *btcutil.AddressPubKeyHash:
|
||||
version = 17
|
||||
case *btcutil.AddressScriptHash:
|
||||
version = 18
|
||||
case *btcutil.AddressWitnessPubKeyHash:
|
||||
version = addr.WitnessVersion()
|
||||
case *btcutil.AddressWitnessScriptHash:
|
||||
version = addr.WitnessVersion()
|
||||
default:
|
||||
return fmt.Errorf("unknown fallback address type")
|
||||
}
|
||||
base32Addr, err := bech32.ConvertBits(
|
||||
invoice.FallbackAddr.ScriptAddress(), 8, 5, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = writeTaggedField(bufferBase32, fieldTypeF,
|
||||
append([]byte{version}, base32Addr...))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(invoice.RoutingInfo) > 0 {
|
||||
// Each extra routing info is encoded using 51 bytes.
|
||||
routingDataBase256 := make([]byte, 0, 51*len(invoice.RoutingInfo))
|
||||
for _, r := range invoice.RoutingInfo {
|
||||
base256 := make([]byte, 51)
|
||||
copy(base256[:33], r.PubKey.SerializeCompressed())
|
||||
binary.BigEndian.PutUint64(base256[33:41], r.ShortChanID)
|
||||
binary.BigEndian.PutUint64(base256[41:49], r.Fee)
|
||||
binary.BigEndian.PutUint16(base256[49:51], r.CltvExpDelta)
|
||||
routingDataBase256 = append(routingDataBase256, base256...)
|
||||
}
|
||||
routingDataBase32, err := bech32.ConvertBits(routingDataBase256,
|
||||
8, 5, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = writeTaggedField(bufferBase32, fieldTypeR, routingDataBase32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if invoice.Destination != nil {
|
||||
// Convert 33 byte pubkey to 53 5-bit groups.
|
||||
pubKeyBase32, err := bech32.ConvertBits(
|
||||
invoice.Destination.SerializeCompressed(), 8, 5, true)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(pubKeyBase32) != pubKeyBase32Len {
|
||||
return fmt.Errorf("invalid pubkey length: %d",
|
||||
len(invoice.Destination.SerializeCompressed()))
|
||||
}
|
||||
|
||||
err = writeTaggedField(bufferBase32, fieldTypeN, pubKeyBase32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeTaggedField takes the type of a tagged data field, and the data of
|
||||
// the tagged field (encoded in base32), and writes the type, length and data
|
||||
// to the buffer.
|
||||
func writeTaggedField(bufferBase32 *bytes.Buffer, dataType byte, data []byte) error {
|
||||
// Length must be exactly 10 bits, so add leading zero groups if
|
||||
// needed.
|
||||
lenBase32 := uint64ToBase32(uint64(len(data)))
|
||||
for len(lenBase32) < 2 {
|
||||
lenBase32 = append([]byte{0}, lenBase32...)
|
||||
}
|
||||
|
||||
if len(lenBase32) != 2 {
|
||||
return fmt.Errorf("data length too big to fit within 10 bits: %d",
|
||||
len(data))
|
||||
}
|
||||
|
||||
err := bufferBase32.WriteByte(dataType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write to buffer: %v", err)
|
||||
}
|
||||
_, err = bufferBase32.Write(lenBase32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write to buffer: %v", err)
|
||||
}
|
||||
_, err = bufferBase32.Write(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write to buffer: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// base32ToUint64 converts a base32 encoded number to uint64.
|
||||
func base32ToUint64(data []byte) (uint64, error) {
|
||||
// Maximum that fits in uint64 is 64 / 5 = 12 groups.
|
||||
if len(data) > 12 {
|
||||
return 0, fmt.Errorf("cannot parse data of length %d as uint64",
|
||||
len(data))
|
||||
}
|
||||
|
||||
val := uint64(0)
|
||||
for i := 0; i < len(data); i++ {
|
||||
val = val<<5 | uint64(data[i])
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// uint64ToBase32 converts a uint64 to a base32 encoded integer encoded using
|
||||
// as few 5-bit groups as possible.
|
||||
func uint64ToBase32(num uint64) []byte {
|
||||
// Return at least one group.
|
||||
if num == 0 {
|
||||
return []byte{0}
|
||||
}
|
||||
|
||||
// To fit an uint64, we need at most is 64 / 5 = 12 groups.
|
||||
arr := make([]byte, 12)
|
||||
i := 12
|
||||
for num > 0 {
|
||||
i--
|
||||
arr[i] = byte(num & uint64(31)) // 0b11111 in binary
|
||||
num = num >> 5
|
||||
}
|
||||
|
||||
// We only return non-zero leading groups.
|
||||
return arr[i:]
|
||||
}
|
242
invoice/invoice_internal_test.go
Normal file
242
invoice/invoice_internal_test.go
Normal file
@ -0,0 +1,242 @@
|
||||
package invoice
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
|
||||
// TestDecodeAmount ensures that the amount string in the hrp of the Invoice
|
||||
// properly gets decoded into millisatoshis.
|
||||
func TestDecodeAmount(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
amount string
|
||||
valid bool
|
||||
result lnwire.MilliSatoshi
|
||||
}{
|
||||
{
|
||||
amount: "",
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
amount: "20n00",
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
amount: "2000y",
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
amount: "2000mm",
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
amount: "2000nm",
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
amount: "m",
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
amount: "1p", // pBTC
|
||||
valid: false, // too small
|
||||
},
|
||||
{
|
||||
amount: "1109p", // pBTC
|
||||
valid: false, // not divisible by 10
|
||||
},
|
||||
{
|
||||
amount: "10p", // pBTC
|
||||
valid: true,
|
||||
result: 1, // mSat
|
||||
},
|
||||
{
|
||||
amount: "1000p", // pBTC
|
||||
valid: true,
|
||||
result: 100, // mSat
|
||||
},
|
||||
{
|
||||
amount: "1n", // nBTC
|
||||
valid: true,
|
||||
result: 100, // mSat
|
||||
},
|
||||
{
|
||||
amount: "9000n", // nBTC
|
||||
valid: true,
|
||||
result: 900000, // mSat
|
||||
},
|
||||
{
|
||||
amount: "9u", // uBTC
|
||||
valid: true,
|
||||
result: 900000, // mSat
|
||||
},
|
||||
{
|
||||
amount: "2000u", // uBTC
|
||||
valid: true,
|
||||
result: 200000000, // mSat
|
||||
},
|
||||
{
|
||||
amount: "2m", // mBTC
|
||||
valid: true,
|
||||
result: 200000000, // mSat
|
||||
},
|
||||
{
|
||||
amount: "2000m", // mBTC
|
||||
valid: true,
|
||||
result: 200000000000, // mSat
|
||||
},
|
||||
{
|
||||
amount: "2", // BTC
|
||||
valid: true,
|
||||
result: 200000000000, // mSat
|
||||
},
|
||||
{
|
||||
amount: "2000", // BTC
|
||||
valid: true,
|
||||
result: 200000000000000, // mSat
|
||||
},
|
||||
{
|
||||
amount: "2009", // BTC
|
||||
valid: true,
|
||||
result: 200900000000000, // mSat
|
||||
},
|
||||
{
|
||||
amount: "1234", // BTC
|
||||
valid: true,
|
||||
result: 123400000000000, // mSat
|
||||
},
|
||||
{
|
||||
amount: "21000000", // BTC
|
||||
valid: true,
|
||||
result: 2100000000000000000, // mSat
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
sat, err := decodeAmount(test.amount)
|
||||
if (err == nil) != test.valid {
|
||||
t.Errorf("Amount decoding test %d failed: %v", i, err)
|
||||
return
|
||||
}
|
||||
if test.valid && sat != test.result {
|
||||
t.Fatalf("%d) failed decoding amount, expected %v, "+
|
||||
"got %v", i, test.result, sat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEncodeAmount checks that the given amount in millisatoshis gets encoded
|
||||
// into the shortest possible amount string.
|
||||
func TestEncodeAmount(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
msat lnwire.MilliSatoshi
|
||||
valid bool
|
||||
result string
|
||||
}{
|
||||
{
|
||||
msat: 1, // mSat
|
||||
valid: true,
|
||||
result: "10p", // pBTC
|
||||
},
|
||||
{
|
||||
msat: 120, // mSat
|
||||
valid: true,
|
||||
result: "1200p", // pBTC
|
||||
},
|
||||
{
|
||||
msat: 100, // mSat
|
||||
valid: true,
|
||||
result: "1n", // nBTC
|
||||
},
|
||||
{
|
||||
msat: 900000, // mSat
|
||||
valid: true,
|
||||
result: "9u", // uBTC
|
||||
},
|
||||
{
|
||||
msat: 200000000, // mSat
|
||||
valid: true,
|
||||
result: "2m", // mBTC
|
||||
},
|
||||
{
|
||||
msat: 200000000000, // mSat
|
||||
valid: true,
|
||||
result: "2", // BTC
|
||||
},
|
||||
{
|
||||
msat: 200000000000000, // mSat
|
||||
valid: true,
|
||||
result: "2000", // BTC
|
||||
},
|
||||
{
|
||||
msat: 200900000000000, // mSat
|
||||
valid: true,
|
||||
result: "2009", // BTC
|
||||
},
|
||||
{
|
||||
msat: 123400000000000, // mSat
|
||||
valid: true,
|
||||
result: "1234", // BTC
|
||||
},
|
||||
{
|
||||
msat: 2100000000000000000, // mSat
|
||||
valid: true,
|
||||
result: "21000000", // BTC
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
shortened, err := encodeAmount(test.msat)
|
||||
if (err == nil) != test.valid {
|
||||
t.Errorf("Amount encoding test %d failed: %v", i, err)
|
||||
return
|
||||
}
|
||||
if test.valid && shortened != test.result {
|
||||
t.Fatalf("%d) failed encoding amount, expected %v, "+
|
||||
"got %v", i, test.result, shortened)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseTimestamp checks that the 35 bit timestamp is properly parsed.
|
||||
func TestParseTimestamp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
data []byte
|
||||
valid bool
|
||||
result uint64
|
||||
}{
|
||||
{
|
||||
data: []byte(""),
|
||||
valid: false, // empty data
|
||||
},
|
||||
{
|
||||
data: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
valid: false, // data too short
|
||||
},
|
||||
{
|
||||
data: []byte{0x01, 0x0c, 0x12, 0x1f, 0x1c, 0x19, 0x02},
|
||||
valid: true, // timestamp 1496314658
|
||||
result: 1496314658,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
time, err := parseTimestamp(test.data)
|
||||
if (err == nil) != test.valid {
|
||||
t.Errorf("Data decoding test %d failed: %v", i, err)
|
||||
return
|
||||
}
|
||||
if test.valid && time != test.result {
|
||||
t.Errorf("Timestamp decoding test %d failed: expected "+
|
||||
"timestamp %d, got %d", i, test.result, time)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
629
invoice/invoice_test.go
Normal file
629
invoice/invoice_test.go
Normal file
@ -0,0 +1,629 @@
|
||||
package invoice_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/lightningnetwork/lnd/invoice"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/roasbeef/btcd/btcec"
|
||||
"github.com/roasbeef/btcd/chaincfg"
|
||||
"github.com/roasbeef/btcd/chaincfg/chainhash"
|
||||
"github.com/roasbeef/btcutil"
|
||||
)
|
||||
|
||||
var (
|
||||
testPrivKeyBytes, _ = hex.DecodeString("e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734")
|
||||
testPrivKey, testPubKey = btcec.PrivKeyFromBytes(btcec.S256(), testPrivKeyBytes)
|
||||
|
||||
testRoutingInfoPubkeyBytes, _ = hex.DecodeString("029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255")
|
||||
testRoutingInfoPubkey, _ = btcec.ParsePubKey(testRoutingInfoPubkeyBytes, btcec.S256())
|
||||
testRoutingInfoPubkeyBytes2, _ = hex.DecodeString("039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255")
|
||||
testRoutingInfoPubkey2, _ = btcec.ParsePubKey(testRoutingInfoPubkeyBytes2, btcec.S256())
|
||||
|
||||
testMillisat24BTC = lnwire.MilliSatoshi(2400000000000)
|
||||
testMillisat2500uBTC = lnwire.MilliSatoshi(250000000)
|
||||
testMillisat20mBTC = lnwire.MilliSatoshi(2000000000)
|
||||
|
||||
testExpiry60 = time.Unix(60, 0)
|
||||
testEmptyString = ""
|
||||
testCupOfCoffee = "1 cup coffee"
|
||||
testPleaseConsider = "Please consider supporting this project"
|
||||
testRustyAddr, _ = btcutil.DecodeAddress("1RustyRX2oai4EYYDpQGWvEL62BBGqN9T", &chaincfg.MainNetParams)
|
||||
testAddrTestnet, _ = btcutil.DecodeAddress("mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP", &chaincfg.TestNet3Params)
|
||||
testAddrMainnetP2SH, _ = btcutil.DecodeAddress("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX", &chaincfg.MainNetParams)
|
||||
testAddrMainnetP2WPKH, _ = btcutil.DecodeAddress("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", &chaincfg.MainNetParams)
|
||||
testAddrMainnetP2WSH, _ = btcutil.DecodeAddress("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3", &chaincfg.MainNetParams)
|
||||
testPaymentHashSlice, _ = hex.DecodeString("0001020304050607080900010203040506070809000102030405060708090102")
|
||||
testDescriptionHashSlice = chainhash.HashB([]byte("One piece of chocolate cake, one icecream cone, one pickle, one slice of swiss cheese, one slice of salami, one lollypop, one piece of cherry pie, one sausage, one cupcake, and one slice of watermelon"))
|
||||
|
||||
// Must be initialized in init().
|
||||
testPaymentHash [32]byte
|
||||
testDescriptionHash [32]byte
|
||||
|
||||
testMessageSigner = invoice.MessageSigner{
|
||||
SignCompact: func(hash []byte) ([]byte, error) {
|
||||
sig, err := btcec.SignCompact(btcec.S256(),
|
||||
testPrivKey, hash, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't sign the "+
|
||||
"message: %v", err)
|
||||
}
|
||||
return sig, nil
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
copy(testPaymentHash[:], testPaymentHashSlice[:])
|
||||
copy(testDescriptionHash[:], testDescriptionHashSlice[:])
|
||||
}
|
||||
|
||||
// TestDecodeEncode tests that an encoded invoice gets decoded into the expected
|
||||
// Invoice object, and that reencoding the decoded invoice gets us back to the
|
||||
// original encoded string.
|
||||
func TestDecodeEncode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
encodedInvoice string
|
||||
valid bool
|
||||
decodedInvoice *invoice.Invoice
|
||||
skipEncoding bool
|
||||
beforeEncoding func(*invoice.Invoice)
|
||||
}{
|
||||
{
|
||||
encodedInvoice: "asdsaddnasdnas", // no hrp
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
encodedInvoice: "lnbc1abcde", // too short
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
encodedInvoice: "1asdsaddnv4wudz", // empty hrp
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
encodedInvoice: "lnb1asdsaddnv4wudz", // hrp too short
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
encodedInvoice: "llts1dasdajtkfl6", // no "ln" prefix
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
encodedInvoice: "lnts1dasdapukz0w", // invalid segwit prefix
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
encodedInvoice: "lnbcm1aaamcu25m", // invalid amount
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
encodedInvoice: "lnbc1000000000m1", // invalid amount
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
// no payment hash set
|
||||
encodedInvoice: "lnbc20m1pvjluezhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsjv38luh6p6s2xrv3mzvlmzaya43376h0twal5ax0k6p47498hp3hnaymzhsn424rxqjs0q7apn26yrhaxltq3vzwpqj9nc2r3kzwccsplnq470",
|
||||
valid: false,
|
||||
decodedInvoice: &invoice.Invoice{
|
||||
Net: &chaincfg.MainNetParams,
|
||||
MilliSat: &testMillisat20mBTC,
|
||||
Timestamp: time.Unix(1496314658, 0),
|
||||
DescriptionHash: &testDescriptionHash,
|
||||
Destination: testPubKey,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Both Description and DescriptionHash set.
|
||||
encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqs03vghs8y0kuj4ulrzls8ln7fnm9dk7sjsnqmghql6hd6jut36clkqpyuq0s5m6fhureyz0szx2qjc8hkgf4xc2hpw8jpu26jfeyvf4cpga36gt",
|
||||
valid: false,
|
||||
decodedInvoice: &invoice.Invoice{
|
||||
Net: &chaincfg.MainNetParams,
|
||||
MilliSat: &testMillisat20mBTC,
|
||||
Timestamp: time.Unix(1496314658, 0),
|
||||
PaymentHash: &testPaymentHash,
|
||||
Description: &testPleaseConsider,
|
||||
DescriptionHash: &testDescriptionHash,
|
||||
Destination: testPubKey,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Neither Description nor DescriptionHash set.
|
||||
encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqn2rne0kagfl4e0xag0w6hqeg2dwgc54hrm9m0auw52dhwhwcu559qav309h598pyzn69wh2nqauneyyesnpmaax0g6acr8lh9559jmcquyq5a9",
|
||||
valid: false,
|
||||
decodedInvoice: &invoice.Invoice{
|
||||
Net: &chaincfg.MainNetParams,
|
||||
MilliSat: &testMillisat20mBTC,
|
||||
Timestamp: time.Unix(1496314658, 0),
|
||||
PaymentHash: &testPaymentHash,
|
||||
Destination: testPubKey,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Has a few unknown fields, should just be ignored.
|
||||
encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaqtq2v93xxer9vczq8v93xxeqv72xr42ca60022jqu6fu73n453tmnr0ukc0pl0t23w7eavtensjz0j2wcu7nkxhfdgp9y37welajh5kw34mq7m4xuay0a72cwec8qwgqt5vqht",
|
||||
valid: true,
|
||||
decodedInvoice: &invoice.Invoice{
|
||||
Net: &chaincfg.MainNetParams,
|
||||
MilliSat: &testMillisat20mBTC,
|
||||
Timestamp: time.Unix(1496314658, 0),
|
||||
PaymentHash: &testPaymentHash,
|
||||
Description: &testPleaseConsider,
|
||||
Destination: testPubKey,
|
||||
},
|
||||
skipEncoding: true, // Skip encoding since we don't have the unknown fields to encode.
|
||||
},
|
||||
{
|
||||
// Ignore unknown witness version in fallback address.
|
||||
encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpppw508d6qejxtdg4y5r3zarvary0c5xw7k8txqv6x0a75xuzp0zsdzk5hq6tmfgweltvs6jk5nhtyd9uqksvr48zga9mw08667w8264gkspluu66jhtcmct36nx363km6cquhhv2cpc6q43r",
|
||||
valid: true,
|
||||
decodedInvoice: &invoice.Invoice{
|
||||
Net: &chaincfg.MainNetParams,
|
||||
MilliSat: &testMillisat20mBTC,
|
||||
Timestamp: time.Unix(1496314658, 0),
|
||||
PaymentHash: &testPaymentHash,
|
||||
DescriptionHash: &testDescriptionHash,
|
||||
Destination: testPubKey,
|
||||
},
|
||||
skipEncoding: true, // Skip encoding since we don't have the unknown fields to encode.
|
||||
},
|
||||
{
|
||||
// Ignore fields with unknown lengths.
|
||||
encodedInvoice: "lnbc241pveeq09pp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqpp3qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqshp38yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66np3q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfy8huflvs2zwkymx47cszugvzn5v64ahemzzlmm62rpn9l9rm05h35aceq00tkt296289wepws9jh4499wq2l0vk6xcxffd90dpuqchqqztyayq",
|
||||
valid: true,
|
||||
decodedInvoice: &invoice.Invoice{
|
||||
Net: &chaincfg.MainNetParams,
|
||||
MilliSat: &testMillisat24BTC,
|
||||
Timestamp: time.Unix(1503429093, 0),
|
||||
PaymentHash: &testPaymentHash,
|
||||
Destination: testPubKey,
|
||||
DescriptionHash: &testDescriptionHash,
|
||||
},
|
||||
skipEncoding: true, // Skip encoding since we don't have the unknown fields to encode.
|
||||
},
|
||||
{
|
||||
// Please make a donation of any amount using rhash 0001020304050607080900010203040506070809000102030405060708090102 to me @03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad
|
||||
encodedInvoice: "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w",
|
||||
valid: true,
|
||||
decodedInvoice: &invoice.Invoice{
|
||||
Net: &chaincfg.MainNetParams,
|
||||
Timestamp: time.Unix(1496314658, 0),
|
||||
PaymentHash: &testPaymentHash,
|
||||
Description: &testPleaseConsider,
|
||||
Destination: testPubKey,
|
||||
},
|
||||
beforeEncoding: func(i *invoice.Invoice) {
|
||||
// Since this destination pubkey was recovered
|
||||
// from the signature, we must set it nil before
|
||||
// encoding to get back the same invoice string.
|
||||
i.Destination = nil
|
||||
},
|
||||
},
|
||||
{
|
||||
// Same as above, pubkey set in 'n' field.
|
||||
encodedInvoice: "lnbc241pveeq09pp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66jd3m5klcwhq68vdsmx2rjgxeay5v0tkt2v5sjaky4eqahe4fx3k9sqavvce3capfuwv8rvjng57jrtfajn5dkpqv8yelsewtljwmmycq62k443",
|
||||
valid: true,
|
||||
decodedInvoice: &invoice.Invoice{
|
||||
Net: &chaincfg.MainNetParams,
|
||||
MilliSat: &testMillisat24BTC,
|
||||
Timestamp: time.Unix(1503429093, 0),
|
||||
PaymentHash: &testPaymentHash,
|
||||
Destination: testPubKey,
|
||||
Description: &testEmptyString,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Please send $3 for a cup of coffee to the same peer, within 1 minute
|
||||
encodedInvoice: "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspfj9srp",
|
||||
valid: true,
|
||||
decodedInvoice: &invoice.Invoice{
|
||||
Net: &chaincfg.MainNetParams,
|
||||
MilliSat: &testMillisat2500uBTC,
|
||||
Timestamp: time.Unix(1496314658, 0),
|
||||
PaymentHash: &testPaymentHash,
|
||||
Description: &testCupOfCoffee,
|
||||
Destination: testPubKey,
|
||||
Expiry: &testExpiry60,
|
||||
},
|
||||
beforeEncoding: func(i *invoice.Invoice) {
|
||||
// Since this destination pubkey was recovered
|
||||
// from the signature, we must set it nil before
|
||||
// encoding to get back the same invoice string.
|
||||
i.Destination = nil
|
||||
},
|
||||
},
|
||||
{
|
||||
// Now send $24 for an entire list of things (hashed)
|
||||
encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqscc6gd6ql3jrc5yzme8v4ntcewwz5cnw92tz0pc8qcuufvq7khhr8wpald05e92xw006sq94mg8v2ndf4sefvf9sygkshp5zfem29trqq2yxxz7",
|
||||
valid: true,
|
||||
decodedInvoice: &invoice.Invoice{
|
||||
Net: &chaincfg.MainNetParams,
|
||||
MilliSat: &testMillisat20mBTC,
|
||||
Timestamp: time.Unix(1496314658, 0),
|
||||
PaymentHash: &testPaymentHash,
|
||||
DescriptionHash: &testDescriptionHash,
|
||||
Destination: testPubKey,
|
||||
},
|
||||
beforeEncoding: func(i *invoice.Invoice) {
|
||||
// Since this destination pubkey was recovered
|
||||
// from the signature, we must set it nil before
|
||||
// encoding to get back the same invoice string.
|
||||
i.Destination = nil
|
||||
},
|
||||
},
|
||||
{
|
||||
// The same, on testnet, with a fallback address mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP
|
||||
encodedInvoice: "lntb20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3x9et2e20v6pu37c5d9vax37wxq72un98k6vcx9fz94w0qf237cm2rqv9pmn5lnexfvf5579slr4zq3u8kmczecytdx0xg9rwzngp7e6guwqpqlhssu04sucpnz4axcv2dstmknqq6jsk2l",
|
||||
valid: true,
|
||||
decodedInvoice: &invoice.Invoice{
|
||||
Net: &chaincfg.TestNet3Params,
|
||||
MilliSat: &testMillisat20mBTC,
|
||||
Timestamp: time.Unix(1496314658, 0),
|
||||
PaymentHash: &testPaymentHash,
|
||||
DescriptionHash: &testDescriptionHash,
|
||||
Destination: testPubKey,
|
||||
FallbackAddr: testAddrTestnet,
|
||||
},
|
||||
beforeEncoding: func(i *invoice.Invoice) {
|
||||
// Since this destination pubkey was recovered
|
||||
// from the signature, we must set it nil before
|
||||
// encoding to get back the same invoice string.
|
||||
i.Destination = nil
|
||||
},
|
||||
},
|
||||
{
|
||||
// On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to get to node 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255
|
||||
encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85frzjq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqqqqqqq9qqqvncsk57n4v9ehw86wq8fzvjejhv9z3w3q5zh6qkql005x9xl240ch23jk79ujzvr4hsmmafyxghpqe79psktnjl668ntaf4ne7ucs5csqh5mnnk",
|
||||
valid: true,
|
||||
decodedInvoice: &invoice.Invoice{
|
||||
Net: &chaincfg.MainNetParams,
|
||||
MilliSat: &testMillisat20mBTC,
|
||||
Timestamp: time.Unix(1496314658, 0),
|
||||
PaymentHash: &testPaymentHash,
|
||||
DescriptionHash: &testDescriptionHash,
|
||||
Destination: testPubKey,
|
||||
FallbackAddr: testRustyAddr,
|
||||
RoutingInfo: []invoice.ExtraRoutingInfo{
|
||||
{
|
||||
PubKey: testRoutingInfoPubkey,
|
||||
ShortChanID: 0x0102030405060708,
|
||||
Fee: 20,
|
||||
CltvExpDelta: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
beforeEncoding: func(i *invoice.Invoice) {
|
||||
// Since this destination pubkey was recovered
|
||||
// from the signature, we must set it nil before
|
||||
// encoding to get back the same invoice string.
|
||||
i.Destination = nil
|
||||
},
|
||||
},
|
||||
{
|
||||
// On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to go via nodes 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 then 039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255
|
||||
encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqqqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqqqqqqq7qqzqfnlkwydm8rg30gjku7wmxmk06sevjp53fmvrcfegvwy7d5443jvyhxsel0hulkstws7vqv400q4j3wgpk4crg49682hr4scqvmad43cqd5m7tf",
|
||||
valid: true,
|
||||
decodedInvoice: &invoice.Invoice{
|
||||
Net: &chaincfg.MainNetParams,
|
||||
MilliSat: &testMillisat20mBTC,
|
||||
Timestamp: time.Unix(1496314658, 0),
|
||||
PaymentHash: &testPaymentHash,
|
||||
DescriptionHash: &testDescriptionHash,
|
||||
Destination: testPubKey,
|
||||
FallbackAddr: testRustyAddr,
|
||||
RoutingInfo: []invoice.ExtraRoutingInfo{
|
||||
{
|
||||
PubKey: testRoutingInfoPubkey,
|
||||
ShortChanID: 0x0102030405060708,
|
||||
Fee: 20,
|
||||
CltvExpDelta: 3,
|
||||
},
|
||||
{
|
||||
PubKey: testRoutingInfoPubkey2,
|
||||
ShortChanID: 0x030405060708090a,
|
||||
Fee: 30,
|
||||
CltvExpDelta: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
beforeEncoding: func(i *invoice.Invoice) {
|
||||
// Since this destination pubkey was recovered
|
||||
// from the signature, we must set it nil before
|
||||
// encoding to get back the same invoice string.
|
||||
i.Destination = nil
|
||||
},
|
||||
},
|
||||
{
|
||||
// On mainnet, with fallback (p2sh) address 3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX
|
||||
encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfppj3a24vwu6r8ejrss3axul8rxldph2q7z9kk822r8plup77n9yq5ep2dfpcydrjwzxs0la84v3tfw43t3vqhek7f05m6uf8lmfkjn7zv7enn76sq65d8u9lxav2pl6x3xnc2ww3lqpagnh0u",
|
||||
valid: true,
|
||||
decodedInvoice: &invoice.Invoice{
|
||||
Net: &chaincfg.MainNetParams,
|
||||
MilliSat: &testMillisat20mBTC,
|
||||
Timestamp: time.Unix(1496314658, 0),
|
||||
PaymentHash: &testPaymentHash,
|
||||
DescriptionHash: &testDescriptionHash,
|
||||
Destination: testPubKey,
|
||||
FallbackAddr: testAddrMainnetP2SH,
|
||||
},
|
||||
beforeEncoding: func(i *invoice.Invoice) {
|
||||
// Since this destination pubkey was recovered
|
||||
// from the signature, we must set it nil before
|
||||
// encoding to get back the same invoice string.
|
||||
i.Destination = nil
|
||||
},
|
||||
},
|
||||
{
|
||||
// On mainnet, with fallback (p2wpkh) address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
|
||||
encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfppqw508d6qejxtdg4y5r3zarvary0c5xw7kknt6zz5vxa8yh8jrnlkl63dah48yh6eupakk87fjdcnwqfcyt7snnpuz7vp83txauq4c60sys3xyucesxjf46yqnpplj0saq36a554cp9wt865",
|
||||
valid: true,
|
||||
decodedInvoice: &invoice.Invoice{
|
||||
Net: &chaincfg.MainNetParams,
|
||||
MilliSat: &testMillisat20mBTC,
|
||||
Timestamp: time.Unix(1496314658, 0),
|
||||
PaymentHash: &testPaymentHash,
|
||||
DescriptionHash: &testDescriptionHash,
|
||||
Destination: testPubKey,
|
||||
FallbackAddr: testAddrMainnetP2WPKH,
|
||||
},
|
||||
beforeEncoding: func(i *invoice.Invoice) {
|
||||
// Since this destination pubkey was recovered
|
||||
// from the signature, we must set it nil before
|
||||
// encoding to get back the same invoice string.
|
||||
i.Destination = nil
|
||||
},
|
||||
},
|
||||
{
|
||||
// On mainnet, with fallback (p2wsh) address bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3
|
||||
encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfp4qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qvnjha2auylmwrltv2pkp2t22uy8ura2xsdwhq5nm7s574xva47djmnj2xeycsu7u5v8929mvuux43j0cqhhf32wfyn2th0sv4t9x55sppz5we8",
|
||||
valid: true,
|
||||
decodedInvoice: &invoice.Invoice{
|
||||
Net: &chaincfg.MainNetParams,
|
||||
MilliSat: &testMillisat20mBTC,
|
||||
Timestamp: time.Unix(1496314658, 0),
|
||||
PaymentHash: &testPaymentHash,
|
||||
DescriptionHash: &testDescriptionHash,
|
||||
Destination: testPubKey,
|
||||
FallbackAddr: testAddrMainnetP2WSH,
|
||||
},
|
||||
beforeEncoding: func(i *invoice.Invoice) {
|
||||
// Since this destination pubkey was recovered
|
||||
// from the signature, we must set it nil before
|
||||
// encoding to get back the same invoice string.
|
||||
i.Destination = nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
invoice, err := invoice.Decode(test.encodedInvoice)
|
||||
if (err == nil) != test.valid {
|
||||
t.Errorf("Decoding test %d failed: %v", i, err)
|
||||
return
|
||||
}
|
||||
|
||||
if test.valid {
|
||||
if err := compareInvoices(test.decodedInvoice, invoice); err != nil {
|
||||
t.Errorf("Invoice decoding result %d not as expected: %v", i, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if test.skipEncoding {
|
||||
continue
|
||||
}
|
||||
|
||||
if test.beforeEncoding != nil {
|
||||
test.beforeEncoding(test.decodedInvoice)
|
||||
}
|
||||
|
||||
if test.decodedInvoice != nil {
|
||||
reencoded, err := test.decodedInvoice.Encode(
|
||||
testMessageSigner,
|
||||
)
|
||||
if (err == nil) != test.valid {
|
||||
t.Errorf("Encoding test %d failed: %v", i, err)
|
||||
return
|
||||
}
|
||||
|
||||
if test.valid && test.encodedInvoice != reencoded {
|
||||
t.Errorf("Encoding %d failed, expected %v, got %v",
|
||||
i, test.encodedInvoice, reencoded)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewInvoice tests that providing the optional arguments to the NewInvoice
|
||||
// method creates an Invoice that encodes to the expected string.
|
||||
func TestNewInvoice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
newInvoice func() (*invoice.Invoice, error)
|
||||
encodedInvoice string
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
// Both Description and DescriptionHash set.
|
||||
newInvoice: func() (*invoice.Invoice, error) {
|
||||
return invoice.NewInvoice(&chaincfg.MainNetParams,
|
||||
testPaymentHash, time.Unix(1496314658, 0),
|
||||
invoice.DescriptionHash(testDescriptionHash),
|
||||
invoice.Description(testPleaseConsider))
|
||||
},
|
||||
valid: false, // Both Description and DescriptionHash set.
|
||||
},
|
||||
{
|
||||
// 'n' field set.
|
||||
newInvoice: func() (*invoice.Invoice, error) {
|
||||
return invoice.NewInvoice(&chaincfg.MainNetParams,
|
||||
testPaymentHash, time.Unix(1503429093, 0),
|
||||
invoice.Amount(testMillisat24BTC),
|
||||
invoice.Description(testEmptyString),
|
||||
invoice.Destination(testPubKey))
|
||||
},
|
||||
valid: true,
|
||||
encodedInvoice: "lnbc241pveeq09pp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66jd3m5klcwhq68vdsmx2rjgxeay5v0tkt2v5sjaky4eqahe4fx3k9sqavvce3capfuwv8rvjng57jrtfajn5dkpqv8yelsewtljwmmycq62k443",
|
||||
},
|
||||
{
|
||||
// On mainnet, with fallback address 1RustyRX2oai4EYYDpQGWvEL62BBGqN9T with extra routing info to go via nodes 029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255 then 039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255
|
||||
newInvoice: func() (*invoice.Invoice, error) {
|
||||
return invoice.NewInvoice(&chaincfg.MainNetParams,
|
||||
testPaymentHash, time.Unix(1496314658, 0),
|
||||
invoice.Amount(testMillisat20mBTC),
|
||||
invoice.DescriptionHash(testDescriptionHash),
|
||||
invoice.FallbackAddr(testRustyAddr),
|
||||
invoice.RoutingInfo(
|
||||
[]invoice.ExtraRoutingInfo{
|
||||
{
|
||||
PubKey: testRoutingInfoPubkey,
|
||||
ShortChanID: 0x0102030405060708,
|
||||
Fee: 20,
|
||||
CltvExpDelta: 3,
|
||||
},
|
||||
{
|
||||
PubKey: testRoutingInfoPubkey2,
|
||||
ShortChanID: 0x030405060708090a,
|
||||
Fee: 30,
|
||||
CltvExpDelta: 4,
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
valid: true,
|
||||
encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfpp3qjmp7lwpagxun9pygexvgpjdc4jdj85fr9yq20q82gphp2nflc7jtzrcazrra7wwgzxqc8u7754cdlpfrmccae92qgzqvzq2ps8pqqqqqqqqqqqq9qqqvpeuqafqxu92d8lr6fvg0r5gv0heeeqgcrqlnm6jhphu9y00rrhy4grqszsvpcgpy9qqqqqqqqqqqq7qqzqfnlkwydm8rg30gjku7wmxmk06sevjp53fmvrcfegvwy7d5443jvyhxsel0hulkstws7vqv400q4j3wgpk4crg49682hr4scqvmad43cqd5m7tf",
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
|
||||
invoice, err := test.newInvoice()
|
||||
if err != nil && !test.valid {
|
||||
continue
|
||||
}
|
||||
encoded, err := invoice.Encode(testMessageSigner)
|
||||
if (err == nil) != test.valid {
|
||||
t.Errorf("NewInvoice test %d failed: %v", i, err)
|
||||
return
|
||||
}
|
||||
|
||||
if test.valid && test.encodedInvoice != encoded {
|
||||
t.Errorf("Encoding %d failed, expected %v, got %v",
|
||||
i, test.encodedInvoice, encoded)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func compareInvoices(expected, actual *invoice.Invoice) error {
|
||||
if !reflect.DeepEqual(expected.Net, actual.Net) {
|
||||
return fmt.Errorf("expected net %v, got %v",
|
||||
expected.Net, actual.Net)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected.MilliSat, actual.MilliSat) {
|
||||
return fmt.Errorf("expected milli sat %d, got %d",
|
||||
*expected.MilliSat, *actual.MilliSat)
|
||||
}
|
||||
if expected.Timestamp != actual.Timestamp {
|
||||
return fmt.Errorf("expected Timestamp %d, got %d",
|
||||
expected.Timestamp, actual.Timestamp)
|
||||
}
|
||||
|
||||
if !compareHashes(expected.PaymentHash, actual.PaymentHash) {
|
||||
return fmt.Errorf("expected payment hash %x, got %x",
|
||||
*expected.PaymentHash, *actual.PaymentHash)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected.Description, actual.Description) {
|
||||
return fmt.Errorf("expected description \"%s\", got \"%s\"",
|
||||
*expected.Description, *actual.Description)
|
||||
}
|
||||
|
||||
if !comparePubkeys(expected.Destination, actual.Destination) {
|
||||
return fmt.Errorf("expected destination pubkey %x, got %x",
|
||||
expected.Destination, actual.Destination)
|
||||
}
|
||||
|
||||
if !compareHashes(expected.DescriptionHash, actual.DescriptionHash) {
|
||||
return fmt.Errorf("expected description hash %x, got %x",
|
||||
*expected.DescriptionHash, *actual.DescriptionHash)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected.Expiry, actual.Expiry) {
|
||||
return fmt.Errorf("expected expiry %d, got %d",
|
||||
expected.Expiry, actual.Expiry)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected.FallbackAddr, actual.FallbackAddr) {
|
||||
return fmt.Errorf("expected FallbackAddr %v, got %v",
|
||||
expected.FallbackAddr, actual.FallbackAddr)
|
||||
}
|
||||
|
||||
if len(expected.RoutingInfo) != len(actual.RoutingInfo) {
|
||||
return fmt.Errorf("expected len routingInfo %d, got %d",
|
||||
len(expected.RoutingInfo), len(actual.RoutingInfo))
|
||||
}
|
||||
|
||||
for i := 0; i < len(expected.RoutingInfo); i++ {
|
||||
a := expected.RoutingInfo[i]
|
||||
b := actual.RoutingInfo[i]
|
||||
|
||||
if !comparePubkeys(a.PubKey, b.PubKey) {
|
||||
return fmt.Errorf("expected routingInfo pubkey %x, "+
|
||||
"got %x", a.PubKey, b.PubKey)
|
||||
}
|
||||
|
||||
if a.ShortChanID != b.ShortChanID {
|
||||
return fmt.Errorf("expected routingInfo shortChanID "+
|
||||
"%d, got %d", a.ShortChanID, b.ShortChanID)
|
||||
}
|
||||
|
||||
if a.Fee != b.Fee {
|
||||
return fmt.Errorf("expected routingInfo fee %d, got %d",
|
||||
a.Fee, b.Fee)
|
||||
}
|
||||
|
||||
if a.CltvExpDelta != b.CltvExpDelta {
|
||||
return fmt.Errorf("expected routingInfo cltvExpDelta "+
|
||||
"%d, got %d", a.CltvExpDelta, b.CltvExpDelta)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func comparePubkeys(a, b *btcec.PublicKey) bool {
|
||||
if a == b {
|
||||
return true
|
||||
}
|
||||
if a == nil && b != nil {
|
||||
return false
|
||||
}
|
||||
if b == nil && a != nil {
|
||||
return false
|
||||
}
|
||||
return a.IsEqual(b)
|
||||
}
|
||||
|
||||
func compareHashes(a, b *[32]byte) bool {
|
||||
if a == b {
|
||||
return true
|
||||
}
|
||||
if a == nil && b != nil {
|
||||
return false
|
||||
}
|
||||
if b == nil && a != nil {
|
||||
return false
|
||||
}
|
||||
return bytes.Equal(a[:], b[:])
|
||||
}
|
Loading…
Reference in New Issue
Block a user