package record import ( "bytes" "encoding/binary" "io" "github.com/btcsuite/btcd/btcec/v2" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/tlv" ) // BlindedRouteData contains the information that is included in a blinded // route encrypted data blob that is created by the recipient to provide // forwarding information. type BlindedRouteData struct { // ShortChannelID is the channel ID of the next hop. ShortChannelID tlv.RecordT[tlv.TlvType2, lnwire.ShortChannelID] // NextBlindingOverride is a blinding point that should be switched // in for the next hop. This is used to combine two blinded paths into // one (which primarily is used in onion messaging, but in theory // could be used for payments as well). NextBlindingOverride tlv.OptionalRecordT[tlv.TlvType8, *btcec.PublicKey] // RelayInfo provides the relay parameters for the hop. RelayInfo tlv.RecordT[tlv.TlvType10, PaymentRelayInfo] // Constraints provides the payment relay constraints for the hop. Constraints tlv.OptionalRecordT[tlv.TlvType12, PaymentConstraints] // Features is the set of features the payment requires. Features tlv.OptionalRecordT[tlv.TlvType14, lnwire.FeatureVector] } // NewBlindedRouteData creates the data that's provided for hops within a // blinded route. func NewBlindedRouteData(chanID lnwire.ShortChannelID, blindingOverride *btcec.PublicKey, relayInfo PaymentRelayInfo, constraints *PaymentConstraints, features *lnwire.FeatureVector) *BlindedRouteData { info := &BlindedRouteData{ ShortChannelID: tlv.NewRecordT[tlv.TlvType2](chanID), RelayInfo: tlv.NewRecordT[tlv.TlvType10](relayInfo), } if blindingOverride != nil { info.NextBlindingOverride = tlv.SomeRecordT( tlv.NewPrimitiveRecord[tlv.TlvType8](blindingOverride)) } if constraints != nil { info.Constraints = tlv.SomeRecordT( tlv.NewRecordT[tlv.TlvType12](*constraints)) } if features != nil { info.Features = tlv.SomeRecordT( tlv.NewRecordT[tlv.TlvType14](*features), ) } return info } // DecodeBlindedRouteData decodes the data provided within a blinded route. func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) { var ( d BlindedRouteData blindingOverride = d.NextBlindingOverride.Zero() constraints = d.Constraints.Zero() features = d.Features.Zero() ) var tlvRecords lnwire.ExtraOpaqueData if err := lnwire.ReadElements(r, &tlvRecords); err != nil { return nil, err } typeMap, err := tlvRecords.ExtractRecords( &d.ShortChannelID, &blindingOverride, &d.RelayInfo.Val, &constraints, &features, ) if err != nil { return nil, err } val, ok := typeMap[d.NextBlindingOverride.TlvType()] if ok && val == nil { d.NextBlindingOverride = tlv.SomeRecordT(blindingOverride) } if val, ok := typeMap[d.Constraints.TlvType()]; ok && val == nil { d.Constraints = tlv.SomeRecordT(constraints) } if val, ok := typeMap[d.Features.TlvType()]; ok && val == nil { d.Features = tlv.SomeRecordT(features) } return &d, nil } // EncodeBlindedRouteData encodes the blinded route data provided. func EncodeBlindedRouteData(data *BlindedRouteData) ([]byte, error) { var ( e lnwire.ExtraOpaqueData recordProducers = make([]tlv.RecordProducer, 0, 5) ) recordProducers = append(recordProducers, &data.ShortChannelID) data.NextBlindingOverride.WhenSome(func(pk tlv.RecordT[tlv.TlvType8, *btcec.PublicKey]) { recordProducers = append(recordProducers, &pk) }) recordProducers = append(recordProducers, &data.RelayInfo.Val) data.Constraints.WhenSome(func(cs tlv.RecordT[tlv.TlvType12, PaymentConstraints]) { recordProducers = append(recordProducers, &cs) }) data.Features.WhenSome(func(f tlv.RecordT[tlv.TlvType14, lnwire.FeatureVector]) { recordProducers = append(recordProducers, &f) }) if err := e.PackRecords(recordProducers...); err != nil { return nil, err } return e[:], nil } // PaymentRelayInfo describes the relay policy for a blinded path. type PaymentRelayInfo struct { // CltvExpiryDelta is the expiry delta for the payment. CltvExpiryDelta uint16 // FeeRate is the fee rate that will be charged per millionth of a // satoshi. FeeRate uint32 // BaseFee is the per-htlc fee charged. BaseFee uint32 } // newPaymentRelayRecord creates a tlv.Record that encodes the payment relay // (type 10) type for an encrypted blob payload. func (i *PaymentRelayInfo) Record() tlv.Record { return tlv.MakeDynamicRecord( 10, &i, func() uint64 { // uint16 + uint32 + tuint32 return 2 + 4 + tlv.SizeTUint32(i.BaseFee) }, encodePaymentRelay, decodePaymentRelay, ) } func encodePaymentRelay(w io.Writer, val interface{}, buf *[8]byte) error { if t, ok := val.(**PaymentRelayInfo); ok { relayInfo := *t // Just write our first 6 bytes directly. binary.BigEndian.PutUint16(buf[:2], relayInfo.CltvExpiryDelta) binary.BigEndian.PutUint32(buf[2:6], relayInfo.FeeRate) if _, err := w.Write(buf[0:6]); err != nil { return err } // We can safely reuse buf here because we overwrite its // contents. return tlv.ETUint32(w, &relayInfo.BaseFee, buf) } return tlv.NewTypeForEncodingErr(val, "**hop.PaymentRelayInfo") } func decodePaymentRelay(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { if t, ok := val.(**PaymentRelayInfo); ok && l <= 10 { scratch := make([]byte, l) n, err := io.ReadFull(r, scratch) if err != nil { return err } // We expect at least 6 bytes, because we have 2 bytes for // cltv delta and 4 bytes for fee rate. if n < 6 { return tlv.NewTypeForDecodingErr(val, "*hop.paymentRelayInfo", uint64(n), 6) } relayInfo := *t relayInfo.CltvExpiryDelta = binary.BigEndian.Uint16( scratch[0:2], ) relayInfo.FeeRate = binary.BigEndian.Uint32(scratch[2:6]) // To be able to re-use the DTUint32 function we create a // buffer with just the bytes holding the variable length u32. // If the base fee is zero, this will be an empty buffer, which // is okay. b := bytes.NewBuffer(scratch[6:]) return tlv.DTUint32(b, &relayInfo.BaseFee, buf, l-6) } return tlv.NewTypeForDecodingErr(val, "*hop.paymentRelayInfo", l, 10) } // PaymentConstraints is a set of restrictions on a payment. type PaymentConstraints struct { // MaxCltvExpiry is the maximum expiry height for the payment. MaxCltvExpiry uint32 // HtlcMinimumMsat is the minimum htlc size for the payment. HtlcMinimumMsat lnwire.MilliSatoshi } func (p *PaymentConstraints) Record() tlv.Record { return tlv.MakeDynamicRecord( 12, &p, func() uint64 { // uint32 + tuint64. return 4 + tlv.SizeTUint64(uint64( p.HtlcMinimumMsat, )) }, encodePaymentConstraints, decodePaymentConstraints, ) } func encodePaymentConstraints(w io.Writer, val interface{}, buf *[8]byte) error { if c, ok := val.(**PaymentConstraints); ok { constraints := *c binary.BigEndian.PutUint32(buf[:4], constraints.MaxCltvExpiry) if _, err := w.Write(buf[:4]); err != nil { return err } // We can safely re-use buf here because we overwrite its // contents. htlcMsat := uint64(constraints.HtlcMinimumMsat) return tlv.ETUint64(w, &htlcMsat, buf) } return tlv.NewTypeForEncodingErr(val, "**PaymentConstraints") } func decodePaymentConstraints(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { if c, ok := val.(**PaymentConstraints); ok && l <= 12 { scratch := make([]byte, l) n, err := io.ReadFull(r, scratch) if err != nil { return err } // We expect at least 4 bytes for our uint32. if n < 4 { return tlv.NewTypeForDecodingErr(val, "*paymentConstraints", uint64(n), 4) } payConstraints := *c payConstraints.MaxCltvExpiry = binary.BigEndian.Uint32( scratch[:4], ) // This could be empty if our minimum is zero, that's okay. var ( b = bytes.NewBuffer(scratch[4:]) minHtlc uint64 ) err = tlv.DTUint64(b, &minHtlc, buf, l-4) if err != nil { return err } payConstraints.HtlcMinimumMsat = lnwire.MilliSatoshi(minHtlc) return nil } return tlv.NewTypeForDecodingErr(val, "**PaymentConstraints", l, l) }