From 49c601e62ab9b6e7f87c540680e123c4df623776 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Fri, 24 Jul 2020 13:13:29 -0700 Subject: [PATCH 1/5] zpay32: split off encoding/decoding into own files As a preliminary step to isolating zpay32 in migrations 01-11, we'll split out the encoding and decoding logic into separate files. Migration 11 only requires invoice decoding, so this prevents us from needing to copy in the encoding logic that would otherwise be unused. --- zpay32/decode.go | 497 ++++++++++++++++++++++++++++ zpay32/encode.go | 344 ++++++++++++++++++++ zpay32/invoice.go | 814 ---------------------------------------------- 3 files changed, 841 insertions(+), 814 deletions(-) create mode 100644 zpay32/decode.go create mode 100644 zpay32/encode.go diff --git a/zpay32/decode.go b/zpay32/decode.go new file mode 100644 index 000000000..93f24af7a --- /dev/null +++ b/zpay32/decode.go @@ -0,0 +1,497 @@ +package zpay32 + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "strings" + "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/bech32" + "github.com/lightningnetwork/lnd/lnwire" +) + +// Decode parses the provided encoded invoice and returns a decoded Invoice if +// it is valid by BOLT-0011 and matches the provided active network. +func Decode(invoice string, net *chaincfg.Params) (*Invoice, error) { + decodedInvoice := Invoice{} + + // Before bech32 decoding the invoice, make sure that it is not too large. + // This is done as an anti-DoS measure since bech32 decoding is expensive. + if len(invoice) > maxInvoiceLength { + return nil, ErrInvoiceTooLarge + } + + // 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 + one char + // encoding the network. + if len(hrp) < 3 { + 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 that match the active network. + if !strings.HasPrefix(hrp[2:], net.Bech32HRPSegwit) { + return nil, fmt.Errorf( + "invoice not for current active network '%s'", net.Name) + } + decodedInvoice.Net = net + + // Optionally, if there's anything left of the HRP after ln + the segwit + // prefix, we try to decode this as the payment amount. + var netPrefixLength = len(net.Bech32HRPSegwit) + 2 + if len(hrp) > netPrefixLength { + amount, err := decodeAmount(hrp[netPrefixLength:]) + 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. + if len(data) < signatureBase32Len { + return nil, errors.New("short invoice") + } + 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 sig lnwire.Sig + copy(sig[:], 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 { + signature, err := sig.ToSignature() + 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}, sig[:]...) + pubkey, _, err := btcec.RecoverCompact(btcec.S256(), + compactSign, hash) + if err != nil { + return nil, err + } + decodedInvoice.Destination = pubkey + } + + // If no feature vector was decoded, populate an empty one. + if decodedInvoice.Features == nil { + decodedInvoice.Features = lnwire.NewFeatureVector( + nil, lnwire.Features, + ) + } + + // 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 +} + +// 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)) + } + + t, err := parseTimestamp(data[:timestampBase32Len]) + if err != nil { + return err + } + invoice.Timestamp = time.Unix(int64(t), 0) + + // The rest are tagged parts. + tagData := data[7:] + return parseTaggedFields(invoice, tagData, net) +} + +// parseTimestamp converts a 35-bit timestamp (encoded in base32) to uint64. +func parseTimestamp(data []byte) (uint64, error) { + if len(data) != timestampBase32Len { + 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 len(fields)-index > 0 { + // If there are less than 3 groups to read, there cannot be more + // interesting information, as we need the type (1 group) and + // length (2 groups). + // + // This means the last tagged field is broken. + if len(fields)-index < 3 { + return ErrBrokenTaggedField + } + + typ := fields[index] + dataLength, err := parseFieldDataLength(fields[index+1 : index+3]) + if err != nil { + return err + } + + // If we don't have enough field data left to read this length, + // return error. + if len(fields) < index+3+int(dataLength) { + return ErrInvalidFieldLength + } + 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 + } + + invoice.PaymentHash, err = parse32Bytes(base32Data) + case fieldTypeS: + if invoice.PaymentAddr != nil { + // We skip the field if we have already seen a + // supported one. + continue + } + + invoice.PaymentAddr, err = parse32Bytes(base32Data) + case fieldTypeD: + if invoice.Description != nil { + // We skip the field if we have already seen a + // supported one. + continue + } + + invoice.Description, err = parseDescription(base32Data) + case fieldTypeN: + if invoice.Destination != nil { + // We skip the field if we have already seen a + // supported one. + continue + } + + invoice.Destination, err = parseDestination(base32Data) + case fieldTypeH: + if invoice.DescriptionHash != nil { + // We skip the field if we have already seen a + // supported one. + continue + } + + invoice.DescriptionHash, err = parse32Bytes(base32Data) + case fieldTypeX: + if invoice.expiry != nil { + // We skip the field if we have already seen a + // supported one. + continue + } + + invoice.expiry, err = parseExpiry(base32Data) + case fieldTypeC: + if invoice.minFinalCLTVExpiry != nil { + // We skip the field if we have already seen a + // supported one. + continue + } + + invoice.minFinalCLTVExpiry, err = parseMinFinalCLTVExpiry(base32Data) + case fieldTypeF: + if invoice.FallbackAddr != nil { + // We skip the field if we have already seen a + // supported one. + continue + } + + invoice.FallbackAddr, err = parseFallbackAddr(base32Data, net) + case fieldTypeR: + // An `r` field can be included in an invoice multiple + // times, so we won't skip it if we have already seen + // one. + routeHint, err := parseRouteHint(base32Data) + if err != nil { + return err + } + + invoice.RouteHints = append(invoice.RouteHints, routeHint) + case fieldType9: + if invoice.Features != nil { + // We skip the field if we have already seen a + // supported one. + continue + } + + invoice.Features, err = parseFeatures(base32Data) + default: + // Ignore unknown type. + } + + // Check if there was an error from parsing any of the tagged + // fields and return it. + if err != nil { + return err + } + } + + return nil +} + +// parseFieldDataLength converts the two byte slice into a uint16. +func parseFieldDataLength(data []byte) (uint16, error) { + if len(data) != 2 { + return 0, fmt.Errorf("data length must be 2 bytes, was %d", + len(data)) + } + + return uint16(data[0])<<5 | uint16(data[1]), nil +} + +// parse32Bytes converts a 256-bit value (encoded in base32) to *[32]byte. This +// can be used for payment hashes, description hashes, payment addresses, etc. +func parse32Bytes(data []byte) (*[32]byte, error) { + var paymentHash [32]byte + + // As BOLT-11 states, a reader must skip over the 32-byte fields if + // it does not have a length of 52, so avoid returning an error. + if len(data) != hashBase32Len { + return nil, nil + } + + hash, err := bech32.ConvertBits(data, 5, 8, false) + if err != nil { + return nil, err + } + + copy(paymentHash[:], hash[:]) + + return &paymentHash, nil +} + +// parseDescription converts the data (encoded in base32) into a string to use +// as the description. +func parseDescription(data []byte) (*string, error) { + base256Data, err := bech32.ConvertBits(data, 5, 8, false) + if err != nil { + return nil, err + } + + description := string(base256Data) + + return &description, nil +} + +// parseDestination converts the data (encoded in base32) into a 33-byte public +// key of the payee node. +func parseDestination(data []byte) (*btcec.PublicKey, error) { + // As BOLT-11 states, a reader must skip over the destination field + // if it does not have a length of 53, so avoid returning an error. + if len(data) != pubKeyBase32Len { + return nil, nil + } + + base256Data, err := bech32.ConvertBits(data, 5, 8, false) + if err != nil { + return nil, err + } + + return btcec.ParsePubKey(base256Data, btcec.S256()) +} + +// parseExpiry converts the data (encoded in base32) into the expiry time. +func parseExpiry(data []byte) (*time.Duration, error) { + expiry, err := base32ToUint64(data) + if err != nil { + return nil, err + } + + duration := time.Duration(expiry) * time.Second + + return &duration, nil +} + +// parseMinFinalCLTVExpiry converts the data (encoded in base32) into a uint64 +// to use as the minFinalCLTVExpiry. +func parseMinFinalCLTVExpiry(data []byte) (*uint64, error) { + expiry, err := base32ToUint64(data) + if err != nil { + return nil, err + } + + return &expiry, nil +} + +// parseFallbackAddr converts the data (encoded in base32) into a fallback +// on-chain address. +func parseFallbackAddr(data []byte, net *chaincfg.Params) (btcutil.Address, error) { + // Checks if the data is empty or contains a version without an address. + if len(data) < 2 { + return nil, fmt.Errorf("empty fallback address field") + } + + var addr btcutil.Address + + version := data[0] + switch version { + case 0: + witness, err := bech32.ConvertBits(data[1:], 5, 8, false) + if err != nil { + return nil, err + } + + switch len(witness) { + case 20: + addr, err = btcutil.NewAddressWitnessPubKeyHash(witness, net) + case 32: + addr, err = btcutil.NewAddressWitnessScriptHash(witness, net) + default: + return nil, fmt.Errorf("unknown witness program length %d", + len(witness)) + } + + if err != nil { + return nil, err + } + case 17: + pubKeyHash, err := bech32.ConvertBits(data[1:], 5, 8, false) + if err != nil { + return nil, err + } + + addr, err = btcutil.NewAddressPubKeyHash(pubKeyHash, net) + if err != nil { + return nil, err + } + case 18: + scriptHash, err := bech32.ConvertBits(data[1:], 5, 8, false) + if err != nil { + return nil, err + } + + addr, err = btcutil.NewAddressScriptHashFromHash(scriptHash, net) + if err != nil { + return nil, err + } + default: + // Ignore unknown version. + } + + return addr, nil +} + +// parseRouteHint converts the data (encoded in base32) into an array containing +// one or more routing hop hints that represent a single route hint. +func parseRouteHint(data []byte) ([]HopHint, error) { + base256Data, err := bech32.ConvertBits(data, 5, 8, false) + if err != nil { + return nil, err + } + + // Check that base256Data is a multiple of hopHintLen. + if len(base256Data)%hopHintLen != 0 { + return nil, fmt.Errorf("expected length multiple of %d bytes, "+ + "got %d", hopHintLen, len(base256Data)) + } + + var routeHint []HopHint + + for len(base256Data) > 0 { + hopHint := HopHint{} + hopHint.NodeID, err = btcec.ParsePubKey(base256Data[:33], btcec.S256()) + if err != nil { + return nil, err + } + hopHint.ChannelID = binary.BigEndian.Uint64(base256Data[33:41]) + hopHint.FeeBaseMSat = binary.BigEndian.Uint32(base256Data[41:45]) + hopHint.FeeProportionalMillionths = binary.BigEndian.Uint32(base256Data[45:49]) + hopHint.CLTVExpiryDelta = binary.BigEndian.Uint16(base256Data[49:51]) + + routeHint = append(routeHint, hopHint) + + base256Data = base256Data[51:] + } + + return routeHint, nil +} + +// parseFeatures decodes any feature bits directly from the base32 +// representation. +func parseFeatures(data []byte) (*lnwire.FeatureVector, error) { + rawFeatures := lnwire.NewRawFeatureVector() + err := rawFeatures.DecodeBase32(bytes.NewReader(data), len(data)) + if err != nil { + return nil, err + } + + return lnwire.NewFeatureVector(rawFeatures, lnwire.Features), nil +} + +// base32ToUint64 converts a base32 encoded number to uint64. +func base32ToUint64(data []byte) (uint64, error) { + // Maximum that fits in uint64 is ceil(64 / 5) = 12 groups. + if len(data) > 13 { + 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 +} diff --git a/zpay32/encode.go b/zpay32/encode.go new file mode 100644 index 000000000..8a9c3bf6d --- /dev/null +++ b/zpay32/encode.go @@ -0,0 +1,344 @@ +package zpay32 + +import ( + "bytes" + "encoding/binary" + "fmt" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/bech32" + "github.com/lightningnetwork/lnd/lnwire" +) + +// 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 sig lnwire.Sig + copy(sig[:], 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 { + signature, err := sig.ToSignature() + 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(sig[:], 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 + } + + // Before returning, check that the bech32 encoded string is not greater + // than our largest supported invoice size. + if len(b32) > maxInvoiceLength { + return "", ErrInvoiceTooLarge + } + + return b32, 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 { + err := writeBytes32(bufferBase32, fieldTypeP, *invoice.PaymentHash) + 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 { + err := writeBytes32( + bufferBase32, fieldTypeH, *invoice.DescriptionHash, + ) + if err != nil { + return err + } + } + + if invoice.minFinalCLTVExpiry != nil { + finalDelta := uint64ToBase32(uint64(*invoice.minFinalCLTVExpiry)) + err := writeTaggedField(bufferBase32, fieldTypeC, finalDelta) + if err != nil { + return err + } + } + + if invoice.expiry != nil { + seconds := invoice.expiry.Seconds() + expiry := uint64ToBase32(uint64(seconds)) + 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 + } + } + + for _, routeHint := range invoice.RouteHints { + // Each hop hint is encoded using 51 bytes, so we'll make to + // sure to allocate enough space for the whole route hint. + routeHintBase256 := make([]byte, 0, hopHintLen*len(routeHint)) + + for _, hopHint := range routeHint { + hopHintBase256 := make([]byte, hopHintLen) + copy(hopHintBase256[:33], hopHint.NodeID.SerializeCompressed()) + binary.BigEndian.PutUint64( + hopHintBase256[33:41], hopHint.ChannelID, + ) + binary.BigEndian.PutUint32( + hopHintBase256[41:45], hopHint.FeeBaseMSat, + ) + binary.BigEndian.PutUint32( + hopHintBase256[45:49], hopHint.FeeProportionalMillionths, + ) + binary.BigEndian.PutUint16( + hopHintBase256[49:51], hopHint.CLTVExpiryDelta, + ) + routeHintBase256 = append(routeHintBase256, hopHintBase256...) + } + + routeHintBase32, err := bech32.ConvertBits( + routeHintBase256, 8, 5, true, + ) + if err != nil { + return err + } + + err = writeTaggedField(bufferBase32, fieldTypeR, routeHintBase32) + 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 err + } + + 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 + } + } + if invoice.PaymentAddr != nil { + err := writeBytes32( + bufferBase32, fieldTypeS, *invoice.PaymentAddr, + ) + if err != nil { + return err + } + } + if invoice.Features.SerializeSize32() > 0 { + var b bytes.Buffer + err := invoice.Features.RawFeatureVector.EncodeBase32(&b) + if err != nil { + return err + } + + err = writeTaggedField(bufferBase32, fieldType9, b.Bytes()) + if err != nil { + return err + } + } + + return nil +} + +// writeBytes32 encodes a 32-byte array as base32 and writes it to bufferBase32 +// under the passed fieldType. +func writeBytes32(bufferBase32 *bytes.Buffer, fieldType byte, b [32]byte) error { + // Convert 32 byte hash to 52 5-bit groups. + base32, err := bech32.ConvertBits(b[:], 8, 5, true) + if err != nil { + return err + } + + return writeTaggedField(bufferBase32, fieldType, base32) +} + +// 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 +} + +// 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 ceil(64 / 5) = 13 groups. + arr := make([]byte, 13) + i := 13 + 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:] +} diff --git a/zpay32/invoice.go b/zpay32/invoice.go index 1c183231f..dbb991e6f 100644 --- a/zpay32/invoice.go +++ b/zpay32/invoice.go @@ -1,18 +1,13 @@ package zpay32 import ( - "bytes" - "encoding/binary" "errors" "fmt" - "strings" "time" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcutil/bech32" "github.com/lightningnetwork/lnd/lnwire" ) @@ -310,237 +305,6 @@ func NewInvoice(net *chaincfg.Params, paymentHash [32]byte, return invoice, nil } -// Decode parses the provided encoded invoice and returns a decoded Invoice if -// it is valid by BOLT-0011 and matches the provided active network. -func Decode(invoice string, net *chaincfg.Params) (*Invoice, error) { - decodedInvoice := Invoice{} - - // Before bech32 decoding the invoice, make sure that it is not too large. - // This is done as an anti-DoS measure since bech32 decoding is expensive. - if len(invoice) > maxInvoiceLength { - return nil, ErrInvoiceTooLarge - } - - // 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 + one char - // encoding the network. - if len(hrp) < 3 { - 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 that match the active network. - if !strings.HasPrefix(hrp[2:], net.Bech32HRPSegwit) { - return nil, fmt.Errorf( - "invoice not for current active network '%s'", net.Name) - } - decodedInvoice.Net = net - - // Optionally, if there's anything left of the HRP after ln + the segwit - // prefix, we try to decode this as the payment amount. - var netPrefixLength = len(net.Bech32HRPSegwit) + 2 - if len(hrp) > netPrefixLength { - amount, err := decodeAmount(hrp[netPrefixLength:]) - 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 sig lnwire.Sig - copy(sig[:], 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 { - signature, err := sig.ToSignature() - 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}, sig[:]...) - pubkey, _, err := btcec.RecoverCompact(btcec.S256(), - compactSign, hash) - if err != nil { - return nil, err - } - decodedInvoice.Destination = pubkey - } - - // If no feature vector was decoded, populate an empty one. - if decodedInvoice.Features == nil { - decodedInvoice.Features = lnwire.NewFeatureVector( - nil, lnwire.Features, - ) - } - - // 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 sig lnwire.Sig - copy(sig[:], 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 { - signature, err := sig.ToSignature() - 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(sig[:], 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 - } - - // Before returning, check that the bech32 encoded string is not greater - // than our largest supported invoice size. - if len(b32) > maxInvoiceLength { - return "", ErrInvoiceTooLarge - } - - return b32, nil -} - // Expiry returns the expiry time for this invoice. If expiry time is not set // explicitly, the default 3600 second expiry will be returned. func (invoice *Invoice) Expiry() time.Duration { @@ -608,581 +372,3 @@ func validateInvoice(invoice *Invoice) error { 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)) - } - - t, err := parseTimestamp(data[:timestampBase32Len]) - if err != nil { - return err - } - invoice.Timestamp = time.Unix(int64(t), 0) - - // The rest are tagged parts. - tagData := data[7:] - return parseTaggedFields(invoice, tagData, net) -} - -// parseTimestamp converts a 35-bit timestamp (encoded in base32) to uint64. -func parseTimestamp(data []byte) (uint64, error) { - if len(data) != timestampBase32Len { - 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 len(fields)-index > 0 { - // If there are less than 3 groups to read, there cannot be more - // interesting information, as we need the type (1 group) and - // length (2 groups). - // - // This means the last tagged field is broken. - if len(fields)-index < 3 { - return ErrBrokenTaggedField - } - - typ := fields[index] - dataLength, err := parseFieldDataLength(fields[index+1 : index+3]) - if err != nil { - return err - } - - // If we don't have enough field data left to read this length, - // return error. - if len(fields) < index+3+int(dataLength) { - return ErrInvalidFieldLength - } - 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 - } - - invoice.PaymentHash, err = parse32Bytes(base32Data) - case fieldTypeS: - if invoice.PaymentAddr != nil { - // We skip the field if we have already seen a - // supported one. - continue - } - - invoice.PaymentAddr, err = parse32Bytes(base32Data) - case fieldTypeD: - if invoice.Description != nil { - // We skip the field if we have already seen a - // supported one. - continue - } - - invoice.Description, err = parseDescription(base32Data) - case fieldTypeN: - if invoice.Destination != nil { - // We skip the field if we have already seen a - // supported one. - continue - } - - invoice.Destination, err = parseDestination(base32Data) - case fieldTypeH: - if invoice.DescriptionHash != nil { - // We skip the field if we have already seen a - // supported one. - continue - } - - invoice.DescriptionHash, err = parse32Bytes(base32Data) - case fieldTypeX: - if invoice.expiry != nil { - // We skip the field if we have already seen a - // supported one. - continue - } - - invoice.expiry, err = parseExpiry(base32Data) - case fieldTypeC: - if invoice.minFinalCLTVExpiry != nil { - // We skip the field if we have already seen a - // supported one. - continue - } - - invoice.minFinalCLTVExpiry, err = parseMinFinalCLTVExpiry(base32Data) - case fieldTypeF: - if invoice.FallbackAddr != nil { - // We skip the field if we have already seen a - // supported one. - continue - } - - invoice.FallbackAddr, err = parseFallbackAddr(base32Data, net) - case fieldTypeR: - // An `r` field can be included in an invoice multiple - // times, so we won't skip it if we have already seen - // one. - routeHint, err := parseRouteHint(base32Data) - if err != nil { - return err - } - - invoice.RouteHints = append(invoice.RouteHints, routeHint) - case fieldType9: - if invoice.Features != nil { - // We skip the field if we have already seen a - // supported one. - continue - } - - invoice.Features, err = parseFeatures(base32Data) - default: - // Ignore unknown type. - } - - // Check if there was an error from parsing any of the tagged - // fields and return it. - if err != nil { - return err - } - } - - return nil -} - -// parseFieldDataLength converts the two byte slice into a uint16. -func parseFieldDataLength(data []byte) (uint16, error) { - if len(data) != 2 { - return 0, fmt.Errorf("data length must be 2 bytes, was %d", - len(data)) - } - - return uint16(data[0])<<5 | uint16(data[1]), nil -} - -// parse32Bytes converts a 256-bit value (encoded in base32) to *[32]byte. This -// can be used for payment hashes, description hashes, payment addresses, etc. -func parse32Bytes(data []byte) (*[32]byte, error) { - var paymentHash [32]byte - - // As BOLT-11 states, a reader must skip over the 32-byte fields if - // it does not have a length of 52, so avoid returning an error. - if len(data) != hashBase32Len { - return nil, nil - } - - hash, err := bech32.ConvertBits(data, 5, 8, false) - if err != nil { - return nil, err - } - - copy(paymentHash[:], hash[:]) - - return &paymentHash, nil -} - -// parseDescription converts the data (encoded in base32) into a string to use -// as the description. -func parseDescription(data []byte) (*string, error) { - base256Data, err := bech32.ConvertBits(data, 5, 8, false) - if err != nil { - return nil, err - } - - description := string(base256Data) - - return &description, nil -} - -// parseDestination converts the data (encoded in base32) into a 33-byte public -// key of the payee node. -func parseDestination(data []byte) (*btcec.PublicKey, error) { - // As BOLT-11 states, a reader must skip over the destination field - // if it does not have a length of 53, so avoid returning an error. - if len(data) != pubKeyBase32Len { - return nil, nil - } - - base256Data, err := bech32.ConvertBits(data, 5, 8, false) - if err != nil { - return nil, err - } - - return btcec.ParsePubKey(base256Data, btcec.S256()) -} - -// parseExpiry converts the data (encoded in base32) into the expiry time. -func parseExpiry(data []byte) (*time.Duration, error) { - expiry, err := base32ToUint64(data) - if err != nil { - return nil, err - } - - duration := time.Duration(expiry) * time.Second - - return &duration, nil -} - -// parseMinFinalCLTVExpiry converts the data (encoded in base32) into a uint64 -// to use as the minFinalCLTVExpiry. -func parseMinFinalCLTVExpiry(data []byte) (*uint64, error) { - expiry, err := base32ToUint64(data) - if err != nil { - return nil, err - } - - return &expiry, nil -} - -// parseFallbackAddr converts the data (encoded in base32) into a fallback -// on-chain address. -func parseFallbackAddr(data []byte, net *chaincfg.Params) (btcutil.Address, error) { - // Checks if the data is empty or contains a version without an address. - if len(data) < 2 { - return nil, fmt.Errorf("empty fallback address field") - } - - var addr btcutil.Address - - version := data[0] - switch version { - case 0: - witness, err := bech32.ConvertBits(data[1:], 5, 8, false) - if err != nil { - return nil, err - } - - switch len(witness) { - case 20: - addr, err = btcutil.NewAddressWitnessPubKeyHash(witness, net) - case 32: - addr, err = btcutil.NewAddressWitnessScriptHash(witness, net) - default: - return nil, fmt.Errorf("unknown witness program length %d", - len(witness)) - } - - if err != nil { - return nil, err - } - case 17: - pubKeyHash, err := bech32.ConvertBits(data[1:], 5, 8, false) - if err != nil { - return nil, err - } - - addr, err = btcutil.NewAddressPubKeyHash(pubKeyHash, net) - if err != nil { - return nil, err - } - case 18: - scriptHash, err := bech32.ConvertBits(data[1:], 5, 8, false) - if err != nil { - return nil, err - } - - addr, err = btcutil.NewAddressScriptHashFromHash(scriptHash, net) - if err != nil { - return nil, err - } - default: - // Ignore unknown version. - } - - return addr, nil -} - -// parseRouteHint converts the data (encoded in base32) into an array containing -// one or more routing hop hints that represent a single route hint. -func parseRouteHint(data []byte) ([]HopHint, error) { - base256Data, err := bech32.ConvertBits(data, 5, 8, false) - if err != nil { - return nil, err - } - - // Check that base256Data is a multiple of hopHintLen. - if len(base256Data)%hopHintLen != 0 { - return nil, fmt.Errorf("expected length multiple of %d bytes, "+ - "got %d", hopHintLen, len(base256Data)) - } - - var routeHint []HopHint - - for len(base256Data) > 0 { - hopHint := HopHint{} - hopHint.NodeID, err = btcec.ParsePubKey(base256Data[:33], btcec.S256()) - if err != nil { - return nil, err - } - hopHint.ChannelID = binary.BigEndian.Uint64(base256Data[33:41]) - hopHint.FeeBaseMSat = binary.BigEndian.Uint32(base256Data[41:45]) - hopHint.FeeProportionalMillionths = binary.BigEndian.Uint32(base256Data[45:49]) - hopHint.CLTVExpiryDelta = binary.BigEndian.Uint16(base256Data[49:51]) - - routeHint = append(routeHint, hopHint) - - base256Data = base256Data[51:] - } - - return routeHint, nil -} - -// parseFeatures decodes any feature bits directly from the base32 -// representation. -func parseFeatures(data []byte) (*lnwire.FeatureVector, error) { - rawFeatures := lnwire.NewRawFeatureVector() - err := rawFeatures.DecodeBase32(bytes.NewReader(data), len(data)) - if err != nil { - return nil, err - } - - return lnwire.NewFeatureVector(rawFeatures, lnwire.Features), 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 { - err := writeBytes32(bufferBase32, fieldTypeP, *invoice.PaymentHash) - 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 { - err := writeBytes32( - bufferBase32, fieldTypeH, *invoice.DescriptionHash, - ) - if err != nil { - return err - } - } - - if invoice.minFinalCLTVExpiry != nil { - finalDelta := uint64ToBase32(uint64(*invoice.minFinalCLTVExpiry)) - err := writeTaggedField(bufferBase32, fieldTypeC, finalDelta) - if err != nil { - return err - } - } - - if invoice.expiry != nil { - seconds := invoice.expiry.Seconds() - expiry := uint64ToBase32(uint64(seconds)) - 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 - } - } - - for _, routeHint := range invoice.RouteHints { - // Each hop hint is encoded using 51 bytes, so we'll make to - // sure to allocate enough space for the whole route hint. - routeHintBase256 := make([]byte, 0, hopHintLen*len(routeHint)) - - for _, hopHint := range routeHint { - hopHintBase256 := make([]byte, hopHintLen) - copy(hopHintBase256[:33], hopHint.NodeID.SerializeCompressed()) - binary.BigEndian.PutUint64( - hopHintBase256[33:41], hopHint.ChannelID, - ) - binary.BigEndian.PutUint32( - hopHintBase256[41:45], hopHint.FeeBaseMSat, - ) - binary.BigEndian.PutUint32( - hopHintBase256[45:49], hopHint.FeeProportionalMillionths, - ) - binary.BigEndian.PutUint16( - hopHintBase256[49:51], hopHint.CLTVExpiryDelta, - ) - routeHintBase256 = append(routeHintBase256, hopHintBase256...) - } - - routeHintBase32, err := bech32.ConvertBits( - routeHintBase256, 8, 5, true, - ) - if err != nil { - return err - } - - err = writeTaggedField(bufferBase32, fieldTypeR, routeHintBase32) - 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 err - } - - 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 - } - } - if invoice.PaymentAddr != nil { - err := writeBytes32( - bufferBase32, fieldTypeS, *invoice.PaymentAddr, - ) - if err != nil { - return err - } - } - if invoice.Features.SerializeSize32() > 0 { - var b bytes.Buffer - err := invoice.Features.RawFeatureVector.EncodeBase32(&b) - if err != nil { - return err - } - - err = writeTaggedField(bufferBase32, fieldType9, b.Bytes()) - if err != nil { - return err - } - } - - return nil -} - -// writeBytes32 encodes a 32-byte array as base32 and writes it to bufferBase32 -// under the passed fieldType. -func writeBytes32(bufferBase32 *bytes.Buffer, fieldType byte, b [32]byte) error { - // Convert 32 byte hash to 52 5-bit groups. - base32, err := bech32.ConvertBits(b[:], 8, 5, true) - if err != nil { - return err - } - - return writeTaggedField(bufferBase32, fieldType, base32) -} - -// 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 ceil(64 / 5) = 12 groups. - if len(data) > 13 { - 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 ceil(64 / 5) = 13 groups. - arr := make([]byte, 13) - i := 13 - 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:] -} From 73256e6d0ef8111198a02dfca7f7de81c278a9ca Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Fri, 24 Jul 2020 13:13:39 -0700 Subject: [PATCH 2/5] zpay32: fix linter errors in encode/decode --- zpay32/decode.go | 2 +- zpay32/encode.go | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/zpay32/decode.go b/zpay32/decode.go index 93f24af7a..07929b45b 100644 --- a/zpay32/decode.go +++ b/zpay32/decode.go @@ -318,7 +318,7 @@ func parse32Bytes(data []byte) (*[32]byte, error) { return nil, err } - copy(paymentHash[:], hash[:]) + copy(paymentHash[:], hash) return &paymentHash, nil } diff --git a/zpay32/encode.go b/zpay32/encode.go index 8a9c3bf6d..9084e2305 100644 --- a/zpay32/encode.go +++ b/zpay32/encode.go @@ -35,8 +35,7 @@ func (invoice *Invoice) Encode(signer MessageSigner) (string, error) { // 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)) + zeroes := make([]byte, timestampBase32Len-len(timestampBase32)) _, err := bufferBase32.Write(zeroes) if err != nil { return "", fmt.Errorf("unable to write to buffer: %v", err) @@ -157,7 +156,7 @@ func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error { } if invoice.minFinalCLTVExpiry != nil { - finalDelta := uint64ToBase32(uint64(*invoice.minFinalCLTVExpiry)) + finalDelta := uint64ToBase32(*invoice.minFinalCLTVExpiry) err := writeTaggedField(bufferBase32, fieldTypeC, finalDelta) if err != nil { return err @@ -336,7 +335,7 @@ func uint64ToBase32(num uint64) []byte { for num > 0 { i-- arr[i] = byte(num & uint64(31)) // 0b11111 in binary - num = num >> 5 + num >>= 5 } // We only return non-zero leading groups. From 213c51a2813294870d91f4b5d5a031a06eb7015b Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Fri, 24 Jul 2020 13:13:47 -0700 Subject: [PATCH 3/5] channeldb/migration_01_to_11: isolate zpay32 decoding This commit copies over the relevant zpay32 decoding logic to ensure that our prior migrations aren't affected by upcoming changes to the zpay32 package, most notably changes to the default final_cltv_expiry and expiry values. --- .../migration_11_invoices.go | 2 +- .../migration_01_to_11/zpay32/amountunits.go | 158 ++++++ channeldb/migration_01_to_11/zpay32/bech32.go | 168 ++++++ channeldb/migration_01_to_11/zpay32/decode.go | 497 ++++++++++++++++++ .../migration_01_to_11/zpay32/hophint.go | 43 ++ .../migration_01_to_11/zpay32/invoice.go | 374 +++++++++++++ 6 files changed, 1241 insertions(+), 1 deletion(-) create mode 100644 channeldb/migration_01_to_11/zpay32/amountunits.go create mode 100644 channeldb/migration_01_to_11/zpay32/bech32.go create mode 100644 channeldb/migration_01_to_11/zpay32/decode.go create mode 100644 channeldb/migration_01_to_11/zpay32/hophint.go create mode 100644 channeldb/migration_01_to_11/zpay32/invoice.go diff --git a/channeldb/migration_01_to_11/migration_11_invoices.go b/channeldb/migration_01_to_11/migration_11_invoices.go index 242631d86..7cb9ea886 100644 --- a/channeldb/migration_01_to_11/migration_11_invoices.go +++ b/channeldb/migration_01_to_11/migration_11_invoices.go @@ -9,8 +9,8 @@ import ( bitcoinCfg "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb/kvdb" + "github.com/lightningnetwork/lnd/channeldb/migration_01_to_11/zpay32" "github.com/lightningnetwork/lnd/lnwire" - "github.com/lightningnetwork/lnd/zpay32" litecoinCfg "github.com/ltcsuite/ltcd/chaincfg" ) diff --git a/channeldb/migration_01_to_11/zpay32/amountunits.go b/channeldb/migration_01_to_11/zpay32/amountunits.go new file mode 100644 index 000000000..f53f3ff00 --- /dev/null +++ b/channeldb/migration_01_to_11/zpay32/amountunits.go @@ -0,0 +1,158 @@ +package zpay32 + +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 millisatoshis to picoBTC. +func mSatToPBtc(msat lnwire.MilliSatoshi) (uint64, error) { + return uint64(msat * 10), nil +} + +// decodeAmount returns the amount encoded by the provided string in +// millisatoshi. +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 +} diff --git a/channeldb/migration_01_to_11/zpay32/bech32.go b/channeldb/migration_01_to_11/zpay32/bech32.go new file mode 100644 index 000000000..cb1714e48 --- /dev/null +++ b/channeldb/migration_01_to_11/zpay32/bech32.go @@ -0,0 +1,168 @@ +package zpay32 + +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 corresponding 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 +} diff --git a/channeldb/migration_01_to_11/zpay32/decode.go b/channeldb/migration_01_to_11/zpay32/decode.go new file mode 100644 index 000000000..07929b45b --- /dev/null +++ b/channeldb/migration_01_to_11/zpay32/decode.go @@ -0,0 +1,497 @@ +package zpay32 + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "strings" + "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/bech32" + "github.com/lightningnetwork/lnd/lnwire" +) + +// Decode parses the provided encoded invoice and returns a decoded Invoice if +// it is valid by BOLT-0011 and matches the provided active network. +func Decode(invoice string, net *chaincfg.Params) (*Invoice, error) { + decodedInvoice := Invoice{} + + // Before bech32 decoding the invoice, make sure that it is not too large. + // This is done as an anti-DoS measure since bech32 decoding is expensive. + if len(invoice) > maxInvoiceLength { + return nil, ErrInvoiceTooLarge + } + + // 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 + one char + // encoding the network. + if len(hrp) < 3 { + 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 that match the active network. + if !strings.HasPrefix(hrp[2:], net.Bech32HRPSegwit) { + return nil, fmt.Errorf( + "invoice not for current active network '%s'", net.Name) + } + decodedInvoice.Net = net + + // Optionally, if there's anything left of the HRP after ln + the segwit + // prefix, we try to decode this as the payment amount. + var netPrefixLength = len(net.Bech32HRPSegwit) + 2 + if len(hrp) > netPrefixLength { + amount, err := decodeAmount(hrp[netPrefixLength:]) + 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. + if len(data) < signatureBase32Len { + return nil, errors.New("short invoice") + } + 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 sig lnwire.Sig + copy(sig[:], 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 { + signature, err := sig.ToSignature() + 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}, sig[:]...) + pubkey, _, err := btcec.RecoverCompact(btcec.S256(), + compactSign, hash) + if err != nil { + return nil, err + } + decodedInvoice.Destination = pubkey + } + + // If no feature vector was decoded, populate an empty one. + if decodedInvoice.Features == nil { + decodedInvoice.Features = lnwire.NewFeatureVector( + nil, lnwire.Features, + ) + } + + // 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 +} + +// 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)) + } + + t, err := parseTimestamp(data[:timestampBase32Len]) + if err != nil { + return err + } + invoice.Timestamp = time.Unix(int64(t), 0) + + // The rest are tagged parts. + tagData := data[7:] + return parseTaggedFields(invoice, tagData, net) +} + +// parseTimestamp converts a 35-bit timestamp (encoded in base32) to uint64. +func parseTimestamp(data []byte) (uint64, error) { + if len(data) != timestampBase32Len { + 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 len(fields)-index > 0 { + // If there are less than 3 groups to read, there cannot be more + // interesting information, as we need the type (1 group) and + // length (2 groups). + // + // This means the last tagged field is broken. + if len(fields)-index < 3 { + return ErrBrokenTaggedField + } + + typ := fields[index] + dataLength, err := parseFieldDataLength(fields[index+1 : index+3]) + if err != nil { + return err + } + + // If we don't have enough field data left to read this length, + // return error. + if len(fields) < index+3+int(dataLength) { + return ErrInvalidFieldLength + } + 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 + } + + invoice.PaymentHash, err = parse32Bytes(base32Data) + case fieldTypeS: + if invoice.PaymentAddr != nil { + // We skip the field if we have already seen a + // supported one. + continue + } + + invoice.PaymentAddr, err = parse32Bytes(base32Data) + case fieldTypeD: + if invoice.Description != nil { + // We skip the field if we have already seen a + // supported one. + continue + } + + invoice.Description, err = parseDescription(base32Data) + case fieldTypeN: + if invoice.Destination != nil { + // We skip the field if we have already seen a + // supported one. + continue + } + + invoice.Destination, err = parseDestination(base32Data) + case fieldTypeH: + if invoice.DescriptionHash != nil { + // We skip the field if we have already seen a + // supported one. + continue + } + + invoice.DescriptionHash, err = parse32Bytes(base32Data) + case fieldTypeX: + if invoice.expiry != nil { + // We skip the field if we have already seen a + // supported one. + continue + } + + invoice.expiry, err = parseExpiry(base32Data) + case fieldTypeC: + if invoice.minFinalCLTVExpiry != nil { + // We skip the field if we have already seen a + // supported one. + continue + } + + invoice.minFinalCLTVExpiry, err = parseMinFinalCLTVExpiry(base32Data) + case fieldTypeF: + if invoice.FallbackAddr != nil { + // We skip the field if we have already seen a + // supported one. + continue + } + + invoice.FallbackAddr, err = parseFallbackAddr(base32Data, net) + case fieldTypeR: + // An `r` field can be included in an invoice multiple + // times, so we won't skip it if we have already seen + // one. + routeHint, err := parseRouteHint(base32Data) + if err != nil { + return err + } + + invoice.RouteHints = append(invoice.RouteHints, routeHint) + case fieldType9: + if invoice.Features != nil { + // We skip the field if we have already seen a + // supported one. + continue + } + + invoice.Features, err = parseFeatures(base32Data) + default: + // Ignore unknown type. + } + + // Check if there was an error from parsing any of the tagged + // fields and return it. + if err != nil { + return err + } + } + + return nil +} + +// parseFieldDataLength converts the two byte slice into a uint16. +func parseFieldDataLength(data []byte) (uint16, error) { + if len(data) != 2 { + return 0, fmt.Errorf("data length must be 2 bytes, was %d", + len(data)) + } + + return uint16(data[0])<<5 | uint16(data[1]), nil +} + +// parse32Bytes converts a 256-bit value (encoded in base32) to *[32]byte. This +// can be used for payment hashes, description hashes, payment addresses, etc. +func parse32Bytes(data []byte) (*[32]byte, error) { + var paymentHash [32]byte + + // As BOLT-11 states, a reader must skip over the 32-byte fields if + // it does not have a length of 52, so avoid returning an error. + if len(data) != hashBase32Len { + return nil, nil + } + + hash, err := bech32.ConvertBits(data, 5, 8, false) + if err != nil { + return nil, err + } + + copy(paymentHash[:], hash) + + return &paymentHash, nil +} + +// parseDescription converts the data (encoded in base32) into a string to use +// as the description. +func parseDescription(data []byte) (*string, error) { + base256Data, err := bech32.ConvertBits(data, 5, 8, false) + if err != nil { + return nil, err + } + + description := string(base256Data) + + return &description, nil +} + +// parseDestination converts the data (encoded in base32) into a 33-byte public +// key of the payee node. +func parseDestination(data []byte) (*btcec.PublicKey, error) { + // As BOLT-11 states, a reader must skip over the destination field + // if it does not have a length of 53, so avoid returning an error. + if len(data) != pubKeyBase32Len { + return nil, nil + } + + base256Data, err := bech32.ConvertBits(data, 5, 8, false) + if err != nil { + return nil, err + } + + return btcec.ParsePubKey(base256Data, btcec.S256()) +} + +// parseExpiry converts the data (encoded in base32) into the expiry time. +func parseExpiry(data []byte) (*time.Duration, error) { + expiry, err := base32ToUint64(data) + if err != nil { + return nil, err + } + + duration := time.Duration(expiry) * time.Second + + return &duration, nil +} + +// parseMinFinalCLTVExpiry converts the data (encoded in base32) into a uint64 +// to use as the minFinalCLTVExpiry. +func parseMinFinalCLTVExpiry(data []byte) (*uint64, error) { + expiry, err := base32ToUint64(data) + if err != nil { + return nil, err + } + + return &expiry, nil +} + +// parseFallbackAddr converts the data (encoded in base32) into a fallback +// on-chain address. +func parseFallbackAddr(data []byte, net *chaincfg.Params) (btcutil.Address, error) { + // Checks if the data is empty or contains a version without an address. + if len(data) < 2 { + return nil, fmt.Errorf("empty fallback address field") + } + + var addr btcutil.Address + + version := data[0] + switch version { + case 0: + witness, err := bech32.ConvertBits(data[1:], 5, 8, false) + if err != nil { + return nil, err + } + + switch len(witness) { + case 20: + addr, err = btcutil.NewAddressWitnessPubKeyHash(witness, net) + case 32: + addr, err = btcutil.NewAddressWitnessScriptHash(witness, net) + default: + return nil, fmt.Errorf("unknown witness program length %d", + len(witness)) + } + + if err != nil { + return nil, err + } + case 17: + pubKeyHash, err := bech32.ConvertBits(data[1:], 5, 8, false) + if err != nil { + return nil, err + } + + addr, err = btcutil.NewAddressPubKeyHash(pubKeyHash, net) + if err != nil { + return nil, err + } + case 18: + scriptHash, err := bech32.ConvertBits(data[1:], 5, 8, false) + if err != nil { + return nil, err + } + + addr, err = btcutil.NewAddressScriptHashFromHash(scriptHash, net) + if err != nil { + return nil, err + } + default: + // Ignore unknown version. + } + + return addr, nil +} + +// parseRouteHint converts the data (encoded in base32) into an array containing +// one or more routing hop hints that represent a single route hint. +func parseRouteHint(data []byte) ([]HopHint, error) { + base256Data, err := bech32.ConvertBits(data, 5, 8, false) + if err != nil { + return nil, err + } + + // Check that base256Data is a multiple of hopHintLen. + if len(base256Data)%hopHintLen != 0 { + return nil, fmt.Errorf("expected length multiple of %d bytes, "+ + "got %d", hopHintLen, len(base256Data)) + } + + var routeHint []HopHint + + for len(base256Data) > 0 { + hopHint := HopHint{} + hopHint.NodeID, err = btcec.ParsePubKey(base256Data[:33], btcec.S256()) + if err != nil { + return nil, err + } + hopHint.ChannelID = binary.BigEndian.Uint64(base256Data[33:41]) + hopHint.FeeBaseMSat = binary.BigEndian.Uint32(base256Data[41:45]) + hopHint.FeeProportionalMillionths = binary.BigEndian.Uint32(base256Data[45:49]) + hopHint.CLTVExpiryDelta = binary.BigEndian.Uint16(base256Data[49:51]) + + routeHint = append(routeHint, hopHint) + + base256Data = base256Data[51:] + } + + return routeHint, nil +} + +// parseFeatures decodes any feature bits directly from the base32 +// representation. +func parseFeatures(data []byte) (*lnwire.FeatureVector, error) { + rawFeatures := lnwire.NewRawFeatureVector() + err := rawFeatures.DecodeBase32(bytes.NewReader(data), len(data)) + if err != nil { + return nil, err + } + + return lnwire.NewFeatureVector(rawFeatures, lnwire.Features), nil +} + +// base32ToUint64 converts a base32 encoded number to uint64. +func base32ToUint64(data []byte) (uint64, error) { + // Maximum that fits in uint64 is ceil(64 / 5) = 12 groups. + if len(data) > 13 { + 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 +} diff --git a/channeldb/migration_01_to_11/zpay32/hophint.go b/channeldb/migration_01_to_11/zpay32/hophint.go new file mode 100644 index 000000000..250286281 --- /dev/null +++ b/channeldb/migration_01_to_11/zpay32/hophint.go @@ -0,0 +1,43 @@ +package zpay32 + +import "github.com/btcsuite/btcd/btcec" + +const ( + // DefaultFinalCLTVDelta is the default value to be used as the final + // CLTV delta for a route if one is unspecified. + DefaultFinalCLTVDelta = 9 +) + +// HopHint is a routing hint that contains the minimum information of a channel +// required for an intermediate hop in a route to forward the payment to the +// next. This should be ideally used for private channels, since they are not +// publicly advertised to the network for routing. +type HopHint struct { + // NodeID is the public key of the node at the start of the channel. + NodeID *btcec.PublicKey + + // ChannelID is the unique identifier of the channel. + ChannelID uint64 + + // FeeBaseMSat is the base fee of the channel in millisatoshis. + FeeBaseMSat uint32 + + // FeeProportionalMillionths is the fee rate, in millionths of a + // satoshi, for every satoshi sent through the channel. + FeeProportionalMillionths uint32 + + // CLTVExpiryDelta is the time-lock delta of the channel. + CLTVExpiryDelta uint16 +} + +// Copy returns a deep copy of the hop hint. +func (h HopHint) Copy() HopHint { + nodeID := *h.NodeID + return HopHint{ + NodeID: &nodeID, + ChannelID: h.ChannelID, + FeeBaseMSat: h.FeeBaseMSat, + FeeProportionalMillionths: h.FeeProportionalMillionths, + CLTVExpiryDelta: h.CLTVExpiryDelta, + } +} diff --git a/channeldb/migration_01_to_11/zpay32/invoice.go b/channeldb/migration_01_to_11/zpay32/invoice.go new file mode 100644 index 000000000..dbb991e6f --- /dev/null +++ b/channeldb/migration_01_to_11/zpay32/invoice.go @@ -0,0 +1,374 @@ +package zpay32 + +import ( + "errors" + "fmt" + "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/lnwire" +) + +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 + + // hopHintLen is the number of bytes needed to encode the hop hint of a + // single private route. + hopHintLen = 51 + + // 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 + + // fieldTypeC contains an optional requested final CLTV delta. + fieldTypeC = 24 + + // fieldType9 contains one or more bytes for signaling features + // supported or required by the receiver. + fieldType9 = 5 + + // fieldTypeS contains a 32-byte payment address, which is a nonce + // included in the final hop's payload to prevent intermediaries from + // probing the recipient. + fieldTypeS = 16 + + // maxInvoiceLength is the maximum total length an invoice can have. + // This is chosen to be the maximum number of bytes that can fit into a + // single QR code: https://en.wikipedia.org/wiki/QR_code#Storage + maxInvoiceLength = 7089 + + // DefaultInvoiceExpiry is the default expiry duration from the creation + // timestamp if expiry is set to zero. + DefaultInvoiceExpiry = time.Hour +) + +var ( + // ErrInvoiceTooLarge is returned when an invoice exceeds + // maxInvoiceLength. + ErrInvoiceTooLarge = errors.New("invoice is too large") + + // ErrInvalidFieldLength is returned when a tagged field was specified + // with a length larger than the left over bytes of the data field. + ErrInvalidFieldLength = errors.New("invalid field length") + + // ErrBrokenTaggedField is returned when the last tagged field is + // incorrectly formatted and doesn't have enough bytes to be read. + ErrBrokenTaggedField = errors.New("last tagged field is broken") +) + +// 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 + + // PaymentAddr is the payment address to be used by payments to prevent + // probing of the destination. + PaymentAddr *[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 + + // minFinalCLTVExpiry is the value that the creator of the invoice + // expects to be used for the CLTV expiry of the HTLC extended to it in + // the last hop. + // + // NOTE: This value is optional, and should be set to nil if the + // invoice creator doesn't have a strong requirement on the CLTV expiry + // of the final HTLC extended to it. + // + // This field is un-exported and can only be read by the + // MinFinalCLTVExpiry() method. By forcing callers to read via this + // method, we can easily enforce the default if not specified. + minFinalCLTVExpiry *uint64 + + // 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. + // + // This field is unexported and can be read by the Expiry() method. This + // method makes sure the default expiry time is returned in case the + // field is not set. + expiry *time.Duration + + // FallbackAddr is an on-chain address that can be used for payment in + // case the Lightning payment fails. + // Optional. + FallbackAddr btcutil.Address + + // RouteHints represents one or more different route hints. Each route + // hint can be individually used to reach the destination. These usually + // represent private routes. + // + // NOTE: This is optional. + RouteHints [][]HopHint + + // Features represents an optional field used to signal optional or + // required support for features by the receiver. + Features *lnwire.FeatureVector +} + +// 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 + } +} + +// CLTVExpiry is an optional value which allows the receiver of the payment to +// specify the delta between the current height and the HTLC extended to the +// receiver. +func CLTVExpiry(delta uint64) func(*Invoice) { + return func(i *Invoice) { + i.minFinalCLTVExpiry = &delta + } +} + +// 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.Duration) 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 + } +} + +// RouteHint is a functional option that allows callers of NewInvoice to add +// one or more hop hints that represent a private route to the destination. +func RouteHint(routeHint []HopHint) func(*Invoice) { + return func(i *Invoice) { + i.RouteHints = append(i.RouteHints, routeHint) + } +} + +// Features is a functional option that allows callers of NewInvoice to set the +// desired feature bits that are advertised on the invoice. If this option is +// not used, an empty feature vector will automatically be populated. +func Features(features *lnwire.FeatureVector) func(*Invoice) { + return func(i *Invoice) { + i.Features = features + } +} + +// PaymentAddr is a functional option that allows callers of NewInvoice to set +// the desired payment address tht is advertised on the invoice. +func PaymentAddr(addr [32]byte) func(*Invoice) { + return func(i *Invoice) { + i.PaymentAddr = &addr + } +} + +// NewInvoice creates a new Invoice object. The last parameter is a set of +// variadic arguments 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 no features were set, we'll populate an empty feature vector. + if invoice.Features == nil { + invoice.Features = lnwire.NewFeatureVector( + nil, lnwire.Features, + ) + } + + if err := validateInvoice(invoice); err != nil { + return nil, err + } + + return invoice, nil +} + +// Expiry returns the expiry time for this invoice. If expiry time is not set +// explicitly, the default 3600 second expiry will be returned. +func (invoice *Invoice) Expiry() time.Duration { + if invoice.expiry != nil { + return *invoice.expiry + } + + // If no expiry is set for this invoice, default is 3600 seconds. + return DefaultInvoiceExpiry +} + +// MinFinalCLTVExpiry returns the minimum final CLTV expiry delta as specified +// by the creator of the invoice. This value specifies the delta between the +// current height and the expiry height of the HTLC extended in the last hop. +func (invoice *Invoice) MinFinalCLTVExpiry() uint64 { + if invoice.minFinalCLTVExpiry != nil { + return *invoice.minFinalCLTVExpiry + } + + return DefaultFinalCLTVDelta +} + +// 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") + } + + // 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())) + } + + // Ensure that all invoices have feature vectors. + if invoice.Features == nil { + return fmt.Errorf("missing feature vector") + } + + return nil +} From 6622c4814ee29e4857698b80aacf4d39c7940934 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Fri, 24 Jul 2020 13:13:56 -0700 Subject: [PATCH 4/5] multi: enforce routing.MinCLTVDelta=18 for invoices + chanupd This commit clamps all user-chosen CLTVs in LND to be at least 18, which is the new conservative value used in the sepc. This minimum is applied uniformly to forwarding CLTV deltas (via channel updates) as well as final CLTV deltas for new invoices. --- config.go | 2 +- lnrpc/invoicesrpc/addinvoice.go | 9 +++++++++ routing/pathfind_test.go | 26 +++++++++++++------------- routing/router.go | 21 +++++++++++++++++++++ routing/router_test.go | 7 +++---- 5 files changed, 47 insertions(+), 18 deletions(-) diff --git a/config.go b/config.go index 368661bad..3a52ca964 100644 --- a/config.go +++ b/config.go @@ -73,7 +73,7 @@ const ( // minTimeLockDelta is the minimum timelock we require for incoming // HTLCs on our channels. - minTimeLockDelta = 4 + minTimeLockDelta = routing.MinCLTVDelta // defaultAcceptorTimeout is the time after which an RPCAcceptor will time // out and return false if it hasn't yet received a response. diff --git a/lnrpc/invoicesrpc/addinvoice.go b/lnrpc/invoicesrpc/addinvoice.go index c1cde48a3..573d2f381 100644 --- a/lnrpc/invoicesrpc/addinvoice.go +++ b/lnrpc/invoicesrpc/addinvoice.go @@ -17,6 +17,7 @@ import ( "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/netann" + "github.com/lightningnetwork/lnd/routing" "github.com/lightningnetwork/lnd/zpay32" ) @@ -228,6 +229,14 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig, return nil, nil, fmt.Errorf("CLTV delta of %v is too large, max "+ "accepted is: %v", invoice.CltvExpiry, math.MaxUint16) case invoice.CltvExpiry != 0: + // Disallow user-chosen final CLTV deltas below the required + // minimum. + if invoice.CltvExpiry < routing.MinCLTVDelta { + return nil, nil, fmt.Errorf("CLTV delta of %v must be "+ + "greater than minimum of %v", + routing.MinCLTVDelta, invoice.CltvExpiry) + } + options = append(options, zpay32.CLTVExpiry(invoice.CltvExpiry)) default: diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index ba1f39ebd..92696ca38 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -26,7 +26,6 @@ import ( "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/routing/route" - "github.com/lightningnetwork/lnd/zpay32" ) const ( @@ -2076,7 +2075,7 @@ func TestPathFindSpecExample(t *testing.T) { const amt lnwire.MilliSatoshi = 4999999 route, err := ctx.router.FindRoute( bobNode.PubKeyBytes, carol, amt, noRestrictions, nil, nil, - zpay32.DefaultFinalCLTVDelta, + MinCLTVDelta, ) if err != nil { t.Fatalf("unable to find route: %v", err) @@ -2100,14 +2099,14 @@ func TestPathFindSpecExample(t *testing.T) { t.Fatalf("wrong hop fee: got %v, expected %v", fee, 0) } - // The CLTV expiry should be the current height plus 9 (the expiry for + // The CLTV expiry should be the current height plus 18 (the expiry for // the B -> C channel. if route.TotalTimeLock != - startingHeight+zpay32.DefaultFinalCLTVDelta { + startingHeight+MinCLTVDelta { t.Fatalf("wrong total time lock: got %v, expecting %v", route.TotalTimeLock, - startingHeight+zpay32.DefaultFinalCLTVDelta) + startingHeight+MinCLTVDelta) } // Next, we'll set A as the source node so we can assert that we create @@ -2132,7 +2131,7 @@ func TestPathFindSpecExample(t *testing.T) { // We'll now request a route from A -> B -> C. route, err = ctx.router.FindRoute( source.PubKeyBytes, carol, amt, noRestrictions, nil, nil, - zpay32.DefaultFinalCLTVDelta, + MinCLTVDelta, ) if err != nil { t.Fatalf("unable to find routes: %v", err) @@ -2145,15 +2144,16 @@ func TestPathFindSpecExample(t *testing.T) { } // The total amount should factor in a fee of 10199 and also use a CLTV - // delta total of 29 (20 + 9), + // delta total of 38 (20 + 18), expectedAmt := lnwire.MilliSatoshi(5010198) if route.TotalAmount != expectedAmt { t.Fatalf("wrong amount: got %v, expected %v", route.TotalAmount, expectedAmt) } - if route.TotalTimeLock != startingHeight+29 { + expectedDelta := uint32(20 + MinCLTVDelta) + if route.TotalTimeLock != startingHeight+expectedDelta { t.Fatalf("wrong total time lock: got %v, expecting %v", - route.TotalTimeLock, startingHeight+29) + route.TotalTimeLock, startingHeight+expectedDelta) } // Ensure that the hops of the route are properly crafted. @@ -2188,11 +2188,11 @@ func TestPathFindSpecExample(t *testing.T) { // The outgoing CLTV value itself should be the current height plus 30 // to meet Carol's requirements. if route.Hops[0].OutgoingTimeLock != - startingHeight+zpay32.DefaultFinalCLTVDelta { + startingHeight+MinCLTVDelta { t.Fatalf("wrong total time lock: got %v, expecting %v", route.Hops[0].OutgoingTimeLock, - startingHeight+zpay32.DefaultFinalCLTVDelta) + startingHeight+MinCLTVDelta) } // For B -> C, we assert that the final hop also has the proper @@ -2203,11 +2203,11 @@ func TestPathFindSpecExample(t *testing.T) { lastHop.AmtToForward, amt) } if lastHop.OutgoingTimeLock != - startingHeight+zpay32.DefaultFinalCLTVDelta { + startingHeight+MinCLTVDelta { t.Fatalf("wrong total time lock: got %v, expecting %v", lastHop.OutgoingTimeLock, - startingHeight+zpay32.DefaultFinalCLTVDelta) + startingHeight+MinCLTVDelta) } } diff --git a/routing/router.go b/routing/router.go index 9fa7bea5e..84fb9d0b1 100644 --- a/routing/router.go +++ b/routing/router.go @@ -46,6 +46,27 @@ const ( // stats related to processing new channels, updates, or node // announcements. defaultStatInterval = time.Minute + + // MinCLTVDelta is the minimum CLTV value accepted by LND for all + // timelock deltas. This includes both forwarding CLTV deltas set on + // channel updates, as well as final CLTV deltas used to create BOLT 11 + // payment requests. + // + // NOTE: For payment requests, BOLT 11 stipulates that a final CLTV + // delta of 9 should be used when no value is decoded. This however + // leads to inflexiblity in upgrading this default parameter, since it + // can create inconsistencies around the assumed value between sender + // and receiver. Specifically, if the receiver assumes a higher value + // than the sender, the receiver will always see the received HTLCs as + // invalid due to their timelock not meeting the required delta. + // + // We skirt this by always setting an explicit CLTV delta when creating + // invoices. This allows LND nodes to freely update the minimum without + // creating incompatibilities during the upgrade process. For some time + // LND has used an explicit default final CLTV delta of 40 blocks for + // bitcoin (160 for litecoin), though we now clamp the lower end of this + // range for user-chosen deltas to 18 blocks to be conservative. + MinCLTVDelta = 18 ) var ( diff --git a/routing/router_test.go b/routing/router_test.go index 3f31225b0..9844b5268 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -23,7 +23,6 @@ import ( "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/routing/route" - "github.com/lightningnetwork/lnd/zpay32" ) var uniquePaymentID uint64 = 1 // to be used atomically @@ -223,7 +222,7 @@ func TestFindRoutesWithFeeLimit(t *testing.T) { route, err := ctx.router.FindRoute( ctx.router.selfNode.PubKeyBytes, target, paymentAmt, restrictions, nil, nil, - zpay32.DefaultFinalCLTVDelta, + MinCLTVDelta, ) if err != nil { t.Fatalf("unable to find any routes: %v", err) @@ -1284,7 +1283,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) { _, err = ctx.router.FindRoute( ctx.router.selfNode.PubKeyBytes, targetPubKeyBytes, paymentAmt, noRestrictions, nil, nil, - zpay32.DefaultFinalCLTVDelta, + MinCLTVDelta, ) if err != nil { t.Fatalf("unable to find any routes: %v", err) @@ -1327,7 +1326,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) { _, err = ctx.router.FindRoute( ctx.router.selfNode.PubKeyBytes, targetPubKeyBytes, paymentAmt, noRestrictions, nil, nil, - zpay32.DefaultFinalCLTVDelta, + MinCLTVDelta, ) if err != nil { t.Fatalf("unable to find any routes: %v", err) From 53ab22305d9b1a5e3bb4310faeb2bd18eeb11814 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Fri, 24 Jul 2020 13:14:03 -0700 Subject: [PATCH 5/5] zpay32: rename to DefaultAssumedFinalCLTVDelta This renames the paramter to clarify that this value is only used when no CLTV value was decoded from the payment request --- zpay32/hophint.go | 7 ++++--- zpay32/invoice.go | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/zpay32/hophint.go b/zpay32/hophint.go index 250286281..067c8437d 100644 --- a/zpay32/hophint.go +++ b/zpay32/hophint.go @@ -3,9 +3,10 @@ package zpay32 import "github.com/btcsuite/btcd/btcec" const ( - // DefaultFinalCLTVDelta is the default value to be used as the final - // CLTV delta for a route if one is unspecified. - DefaultFinalCLTVDelta = 9 + // DefaultAssumedFinalCLTVDelta is the default value to be used as the + // final CLTV delta for a route if one is unspecified in the payment + // request. + DefaultAssumedFinalCLTVDelta = 9 ) // HopHint is a routing hint that contains the minimum information of a channel diff --git a/zpay32/invoice.go b/zpay32/invoice.go index dbb991e6f..8444e3eea 100644 --- a/zpay32/invoice.go +++ b/zpay32/invoice.go @@ -324,7 +324,7 @@ func (invoice *Invoice) MinFinalCLTVExpiry() uint64 { return *invoice.minFinalCLTVExpiry } - return DefaultFinalCLTVDelta + return DefaultAssumedFinalCLTVDelta } // validateInvoice does a sanity check of the provided Invoice, making sure it