Merge pull request #8735 from ellemouton/rb-receives

[2/4] Route Blinding Receives: Receive and send to a single blinded path in an invoice.
This commit is contained in:
Olaoluwa Osuntokun 2024-07-29 19:00:06 -07:00 committed by GitHub
commit 9decf80a68
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 5634 additions and 1884 deletions

View file

@ -67,12 +67,12 @@ var (
// ErrValueMismatch is returned if we try to register a non-MPP attempt
// with an amount that doesn't match the payment amount.
ErrValueMismatch = errors.New("attempted value doesn't match payment" +
ErrValueMismatch = errors.New("attempted value doesn't match payment " +
"amount")
// ErrValueExceedsAmt is returned if we try to register an attempt that
// would take the total sent amount above the payment amount.
ErrValueExceedsAmt = errors.New("attempted value exceeds payment" +
ErrValueExceedsAmt = errors.New("attempted value exceeds payment " +
"amount")
// ErrNonMPPayment is returned if we try to register an MPP attempt for
@ -83,6 +83,17 @@ var (
// a payment that already has an MPP attempt registered.
ErrMPPayment = errors.New("payment has MPP attempts")
// ErrMPPRecordInBlindedPayment is returned if we try to register an
// attempt with an MPP record for a payment to a blinded path.
ErrMPPRecordInBlindedPayment = errors.New("blinded payment cannot " +
"contain MPP records")
// ErrBlindedPaymentTotalAmountMismatch is returned if we try to
// register an HTLC shard to a blinded route where the total amount
// doesn't match existing shards.
ErrBlindedPaymentTotalAmountMismatch = errors.New("blinded path " +
"total amount mismatch")
// ErrMPPPaymentAddrMismatch is returned if we try to register an MPP
// shard where the payment address doesn't match existing shards.
ErrMPPPaymentAddrMismatch = errors.New("payment address mismatch")
@ -96,7 +107,7 @@ var (
// attempt to a payment that has at least one of its HTLCs settled.
ErrPaymentPendingSettled = errors.New("payment has settled htlcs")
// ErrPaymentAlreadyFailed is returned when we try to add a new attempt
// ErrPaymentPendingFailed is returned when we try to add a new attempt
// to a payment that already has a failure reason.
ErrPaymentPendingFailed = errors.New("payment has failure reason")
@ -334,12 +345,48 @@ func (p *PaymentControl) RegisterAttempt(paymentHash lntypes.Hash,
return err
}
// If the final hop has encrypted data, then we know this is a
// blinded payment. In blinded payments, MPP records are not set
// for split payments and the recipient is responsible for using
// a consistent PathID across the various encrypted data
// payloads that we received from them for this payment. All we
// need to check is that the total amount field for each HTLC
// in the split payment is correct.
isBlinded := len(attempt.Route.FinalHop().EncryptedData) != 0
// Make sure any existing shards match the new one with regards
// to MPP options.
mpp := attempt.Route.FinalHop().MPP
// MPP records should not be set for attempts to blinded paths.
if isBlinded && mpp != nil {
return ErrMPPRecordInBlindedPayment
}
for _, h := range payment.InFlightHTLCs() {
hMpp := h.Route.FinalHop().MPP
// If this is a blinded payment, then no existing HTLCs
// should have MPP records.
if isBlinded && hMpp != nil {
return ErrMPPRecordInBlindedPayment
}
// If this is a blinded payment, then we just need to
// check that the TotalAmtMsat field for this shard
// is equal to that of any other shard in the same
// payment.
if isBlinded {
if attempt.Route.FinalHop().TotalAmtMsat !=
h.Route.FinalHop().TotalAmtMsat {
//nolint:lll
return ErrBlindedPaymentTotalAmountMismatch
}
continue
}
switch {
// We tried to register a non-MPP attempt for a MPP
// payment.
@ -367,9 +414,10 @@ func (p *PaymentControl) RegisterAttempt(paymentHash lntypes.Hash,
}
// If this is a non-MPP attempt, it must match the total amount
// exactly.
// exactly. Note that a blinded payment is considered an MPP
// attempt.
amt := attempt.Route.ReceiverAmt()
if mpp == nil && amt != payment.Info.Value {
if !isBlinded && mpp == nil && amt != payment.Info.Value {
return ErrValueMismatch
}

View file

@ -82,6 +82,14 @@ var addInvoiceCommand = cli.Command{
Usage: "creates an AMP invoice. If true, preimage " +
"should not be set.",
},
cli.BoolFlag{
Name: "blind",
Usage: "creates an invoice that contains blinded " +
"paths. Note that invoices with blinded " +
"paths will be signed using a random " +
"ephemeral key so as not to reveal the real " +
"node ID of this node.",
},
},
Action: actionDecorator(addInvoice),
}
@ -127,6 +135,11 @@ func addInvoice(ctx *cli.Context) error {
return fmt.Errorf("unable to parse description_hash: %w", err)
}
if ctx.IsSet("private") && ctx.IsSet("blind") {
return fmt.Errorf("cannot include both route hints and " +
"blinded paths in the same invoice")
}
invoice := &lnrpc.Invoice{
Memo: ctx.String("memo"),
RPreimage: preimage,
@ -138,6 +151,7 @@ func addInvoice(ctx *cli.Context) error {
CltvExpiry: ctx.Uint64("cltv_expiry_delta"),
Private: ctx.Bool("private"),
IsAmp: ctx.Bool("amp"),
Blind: ctx.Bool("blind"),
}
resp, err := client.AddInvoice(ctxc, invoice)

View file

@ -681,6 +681,15 @@ func DefaultConfig() Config {
Invoices: &lncfg.Invoices{
HoldExpiryDelta: lncfg.DefaultHoldInvoiceExpiryDelta,
},
Routing: &lncfg.Routing{
BlindedPaths: lncfg.BlindedPaths{
MinNumRealHops: lncfg.DefaultMinNumRealBlindedPathHops,
NumHops: lncfg.DefaultNumBlindedPathHops,
MaxNumPaths: lncfg.DefaultMaxNumBlindedPaths,
PolicyIncreaseMultiplier: lncfg.DefaultBlindedPathPolicyIncreaseMultiplier,
PolicyDecreaseMultiplier: lncfg.DefaultBlindedPathPolicyDecreaseMultiplier,
},
},
MaxOutgoingCltvExpiry: htlcswitch.DefaultMaxOutgoingCltvExpiry,
MaxChannelFeeAllocation: htlcswitch.DefaultMaxLinkFeeAllocation,
MaxCommitFeeRateAnchors: lnwallet.DefaultAnchorsCommitMaxFeeRateSatPerVByte,
@ -1656,18 +1665,6 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser,
return nil, mkErr("error parsing gossip syncer: %v", err)
}
// Log a warning if our expiry delta is not greater than our incoming
// broadcast delta. We do not fail here because this value may be set
// to zero to intentionally keep lnd's behavior unchanged from when we
// didn't auto-cancel these invoices.
if cfg.Invoices.HoldExpiryDelta <= lncfg.DefaultIncomingBroadcastDelta {
ltndLog.Warnf("Invoice hold expiry delta: %v <= incoming "+
"delta: %v, accepted hold invoices will force close "+
"channels if they are not canceled manually",
cfg.Invoices.HoldExpiryDelta,
lncfg.DefaultIncomingBroadcastDelta)
}
// If the experimental protocol options specify any protocol messages
// that we want to handle as custom messages, set them now.
customMsg := cfg.ProtocolOptions.CustomMessageOverrides()
@ -1690,6 +1687,8 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser,
cfg.RemoteSigner,
cfg.Sweeper,
cfg.Htlcswitch,
cfg.Invoices,
cfg.Routing,
)
if err != nil {
return nil, err

View file

@ -117,6 +117,11 @@
* [Groundwork](https://github.com/lightningnetwork/lnd/pull/8752) in preparation
for implementing route blinding receives.
* [Generate and send to](https://github.com/lightningnetwork/lnd/pull/8735) an
invoice with blinded paths. With this, the `--blind` flag can be used with
the `lncli addinvoice` command to instruct LND to include blinded paths in the
invoice.
## Testing
## Database

View file

@ -92,4 +92,7 @@ var defaultSetDesc = setDesc{
SetInit: {}, // I
SetNodeAnn: {}, // N
},
lnwire.Bolt11BlindedPathsOptional: {
SetInvoice: {}, // I
},
}

View file

@ -82,8 +82,8 @@ var deps = depDesc{
lnwire.RouteBlindingOptional: {
lnwire.TLVOnionPayloadOptional: {},
},
lnwire.RouteBlindingRequired: {
lnwire.TLVOnionPayloadRequired: {},
lnwire.Bolt11BlindedPathsOptional: {
lnwire.RouteBlindingOptional: {},
},
}

View file

@ -128,6 +128,8 @@ func newManager(cfg Config, desc setDesc) (*Manager, error) {
raw.Unset(lnwire.MPPRequired)
raw.Unset(lnwire.RouteBlindingOptional)
raw.Unset(lnwire.RouteBlindingRequired)
raw.Unset(lnwire.Bolt11BlindedPathsOptional)
raw.Unset(lnwire.Bolt11BlindedPathsRequired)
raw.Unset(lnwire.AMPOptional)
raw.Unset(lnwire.AMPRequired)
raw.Unset(lnwire.KeysendOptional)
@ -187,6 +189,8 @@ func newManager(cfg Config, desc setDesc) (*Manager, error) {
if cfg.NoRouteBlinding {
raw.Unset(lnwire.RouteBlindingOptional)
raw.Unset(lnwire.RouteBlindingRequired)
raw.Unset(lnwire.Bolt11BlindedPathsOptional)
raw.Unset(lnwire.Bolt11BlindedPathsRequired)
}
for _, custom := range cfg.CustomFeatures[set] {
if custom > set.Maximum() {

2
go.mod
View file

@ -32,7 +32,7 @@ require (
github.com/kkdai/bstream v1.0.0
github.com/lightninglabs/neutrino v0.16.1-0.20240425105051-602843d34ffd
github.com/lightninglabs/neutrino/cache v1.1.2
github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f
github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb
github.com/lightningnetwork/lnd/cert v1.2.2
github.com/lightningnetwork/lnd/clock v1.1.1
github.com/lightningnetwork/lnd/fn v1.2.0

4
go.sum
View file

@ -444,8 +444,8 @@ github.com/lightninglabs/neutrino/cache v1.1.2 h1:C9DY/DAPaPxbFC+xNNEI/z1SJY9GS3
github.com/lightninglabs/neutrino/cache v1.1.2/go.mod h1:XJNcgdOw1LQnanGjw8Vj44CvguYA25IMKjWFZczwZuo=
github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display h1:pRdza2wleRN1L2fJXd6ZoQ9ZegVFTAb2bOQfruJPKcY=
github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f h1:Pua7+5TcFEJXIIZ1I2YAUapmbcttmLj4TTi786bIi3s=
github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI=
github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb h1:yfM05S8DXKhuCBp5qSMZdtSwvJ+GFzl94KbXMNB1JDY=
github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI=
github.com/lightningnetwork/lnd/cert v1.2.2 h1:71YK6hogeJtxSxw2teq3eGeuy4rHGKcFf0d0Uy4qBjI=
github.com/lightningnetwork/lnd/cert v1.2.2/go.mod h1:jQmFn/Ez4zhDgq2hnYSw8r35bqGVxViXhX6Cd7HXM6U=
github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0=

View file

@ -8,7 +8,6 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
bitcoinCfg "github.com/btcsuite/btcd/chaincfg"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/htlcswitch"
@ -87,7 +86,7 @@ func initTestExtracter() {
func newOnionProcessor(t *testing.T) *hop.OnionProcessor {
sphinxRouter := sphinx.NewRouter(
&keychain.PrivKeyECDH{PrivKey: sphinxPrivKey},
&bitcoinCfg.SimNetParams, sphinx.NewMemoryReplayLog(),
sphinx.NewMemoryReplayLog(),
)
if err := sphinxRouter.Start(); err != nil {

View file

@ -1,6 +1,7 @@
package hop
import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightningnetwork/lnd/lnwire"
)
@ -27,4 +28,9 @@ type ForwardingInfo struct {
// node in UpdateAddHtlc. This field is set if the htlc is part of a
// blinded route.
NextBlinding lnwire.BlindingPointRecord
// PathID is a secret identifier that the creator of a blinded path
// sets for itself to ensure that the blinded path has been used in the
// correct context.
PathID *chainhash.Hash
}

View file

@ -8,6 +8,7 @@ import (
"sync"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
@ -109,7 +110,7 @@ type Iterator interface {
// sphinxHopIterator is the Sphinx implementation of hop iterator which uses
// onion routing to encode the payment route in such a way so that node might
// see only the next hop in the route..
// see only the next hop in the route.
type sphinxHopIterator struct {
// ogPacket is the original packet from which the processed packet is
// derived.
@ -123,20 +124,31 @@ type sphinxHopIterator struct {
// blindingKit contains the elements required to process hops that are
// part of a blinded route.
blindingKit BlindingKit
// rHash holds the payment hash for this payment. This is needed for
// when a new hop iterator is constructed.
rHash []byte
// router holds the router which can be used to decrypt onion payloads.
// This is required for peeling of dummy hops in a blinded path where
// the same node will iteratively need to unwrap the onion.
router *sphinx.Router
}
// makeSphinxHopIterator converts a processed packet returned from a sphinx
// router and converts it into an hop iterator for usage in the link. A
// blinding kit is passed through for the link to obtain forwarding information
// for blinded routes.
func makeSphinxHopIterator(ogPacket *sphinx.OnionPacket,
packet *sphinx.ProcessedPacket,
blindingKit BlindingKit) *sphinxHopIterator {
func makeSphinxHopIterator(router *sphinx.Router, ogPacket *sphinx.OnionPacket,
packet *sphinx.ProcessedPacket, blindingKit BlindingKit,
rHash []byte) *sphinxHopIterator {
return &sphinxHopIterator{
router: router,
ogPacket: ogPacket,
processedPacket: packet,
blindingKit: blindingKit,
rHash: rHash,
}
}
@ -173,53 +185,7 @@ func (r *sphinxHopIterator) HopPayload() (*Payload, RouteRole, error) {
// Otherwise, if this is the TLV payload, then we'll make a new stream
// to decode only what we need to make routing decisions.
case sphinx.PayloadTLV:
isFinal := r.processedPacket.Action == sphinx.ExitNode
payload, parsed, err := ParseTLVPayload(
bytes.NewReader(r.processedPacket.Payload.Payload),
)
if err != nil {
// If we couldn't even parse our payload then we do
// a best-effort of determining our role in a blinded
// route, accepting that we can't know whether we
// were the introduction node (as the payload
// is not parseable).
routeRole := RouteRoleCleartext
if r.blindingKit.UpdateAddBlinding.IsSome() {
routeRole = RouteRoleRelaying
}
return nil, routeRole, err
}
// Now that we've parsed our payload we can determine which
// role we're playing in the route.
_, payloadBlinding := parsed[record.BlindingPointOnionType]
routeRole := NewRouteRole(
r.blindingKit.UpdateAddBlinding.IsSome(),
payloadBlinding,
)
if err := ValidateTLVPayload(
parsed, isFinal,
r.blindingKit.UpdateAddBlinding.IsSome(),
); err != nil {
return nil, routeRole, err
}
// If we had an encrypted data payload present, pull out our
// forwarding info from the blob.
if payload.encryptedData != nil {
fwdInfo, err := r.blindingKit.DecryptAndValidateFwdInfo(
payload, isFinal, parsed,
)
if err != nil {
return nil, routeRole, err
}
payload.FwdInfo = *fwdInfo
}
return payload, routeRole, nil
return extractTLVPayload(r)
default:
return nil, RouteRoleCleartext,
@ -228,6 +194,294 @@ func (r *sphinxHopIterator) HopPayload() (*Payload, RouteRole, error) {
}
}
// extractTLVPayload parses the hop payload and assumes that it uses the TLV
// format. It returns the parsed payload along with the RouteRole that this hop
// plays given the contents of the payload.
func extractTLVPayload(r *sphinxHopIterator) (*Payload, RouteRole, error) {
isFinal := r.processedPacket.Action == sphinx.ExitNode
// Initial payload parsing and validation
payload, routeRole, recipientData, err := parseAndValidateSenderPayload(
r.processedPacket.Payload.Payload, isFinal,
r.blindingKit.UpdateAddBlinding.IsSome(),
)
if err != nil {
return nil, routeRole, err
}
// If the payload contained no recipient data, then we can exit now.
if !recipientData {
return payload, routeRole, nil
}
return parseAndValidateRecipientData(r, payload, isFinal, routeRole)
}
// parseAndValidateRecipientData decrypts the payload from the recipient and
// then continues handling and validation based on if we are a forwarding node
// in this blinded path or the final destination node.
func parseAndValidateRecipientData(r *sphinxHopIterator, payload *Payload,
isFinal bool, routeRole RouteRole) (*Payload, RouteRole, error) {
// Decrypt and validate the blinded route data
routeData, blindingPoint, err := decryptAndValidateBlindedRouteData(
r, payload,
)
if err != nil {
return nil, routeRole, err
}
// This is the final node in the blinded route.
if isFinal {
return deriveBlindedRouteFinalHopForwardingInfo(
routeData, payload, routeRole,
)
}
// Else, we are a forwarding node in this blinded path.
return deriveBlindedRouteForwardingInfo(
r, routeData, payload, routeRole, blindingPoint,
)
}
// deriveBlindedRouteFinalHopForwardingInfo extracts the PathID from the
// routeData and constructs the ForwardingInfo accordingly.
func deriveBlindedRouteFinalHopForwardingInfo(
routeData *record.BlindedRouteData, payload *Payload,
routeRole RouteRole) (*Payload, RouteRole, error) {
var pathID *chainhash.Hash
routeData.PathID.WhenSome(func(r tlv.RecordT[tlv.TlvType6, []byte]) {
var id chainhash.Hash
copy(id[:], r.Val)
pathID = &id
})
if pathID == nil {
return nil, routeRole, ErrInvalidPayload{
Type: tlv.Type(6),
Violation: InsufficientViolation,
}
}
payload.FwdInfo = ForwardingInfo{
PathID: pathID,
}
return payload, routeRole, nil
}
// deriveBlindedRouteForwardingInfo uses the parsed BlindedRouteData from the
// recipient to derive the ForwardingInfo for the payment.
func deriveBlindedRouteForwardingInfo(r *sphinxHopIterator,
routeData *record.BlindedRouteData, payload *Payload,
routeRole RouteRole, blindingPoint *btcec.PublicKey) (*Payload,
RouteRole, error) {
relayInfo, err := routeData.RelayInfo.UnwrapOrErr(
fmt.Errorf("relay info not set for non-final blinded hop"),
)
if err != nil {
return nil, routeRole, err
}
fwdAmt, err := calculateForwardingAmount(
r.blindingKit.IncomingAmount, relayInfo.Val.BaseFee,
relayInfo.Val.FeeRate,
)
if err != nil {
return nil, routeRole, err
}
nextEph, err := routeData.NextBlindingOverride.UnwrapOrFuncErr(
func() (tlv.RecordT[tlv.TlvType8, *btcec.PublicKey], error) {
next, err := r.blindingKit.Processor.NextEphemeral(
blindingPoint,
)
if err != nil {
return routeData.NextBlindingOverride.Zero(),
err
}
return tlv.NewPrimitiveRecord[tlv.TlvType8](next), nil
})
if err != nil {
return nil, routeRole, err
}
// If the payload signals that the following hop is a dummy hop, then
// we will iteratively peel the dummy hop until we reach the final
// payload.
if checkForDummyHop(routeData, r.router.OnionPublicKey()) {
return peelBlindedPathDummyHop(
r, uint32(relayInfo.Val.CltvExpiryDelta), fwdAmt,
routeRole, nextEph,
)
}
nextSCID, err := routeData.ShortChannelID.UnwrapOrErr(
fmt.Errorf("next SCID not set for non-final blinded hop"),
)
if err != nil {
return nil, routeRole, err
}
payload.FwdInfo = ForwardingInfo{
NextHop: nextSCID.Val,
AmountToForward: fwdAmt,
OutgoingCTLV: r.blindingKit.IncomingCltv - uint32(
relayInfo.Val.CltvExpiryDelta,
),
// Remap from blinding override type to blinding point type.
NextBlinding: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
nextEph.Val,
),
),
}
return payload, routeRole, nil
}
// checkForDummyHop returns whether the given BlindedRouteData packet indicates
// the presence of a dummy hop.
func checkForDummyHop(routeData *record.BlindedRouteData,
routerPubKey *btcec.PublicKey) bool {
var isDummy bool
routeData.NextNodeID.WhenSome(
func(r tlv.RecordT[tlv.TlvType4, *btcec.PublicKey]) {
isDummy = r.Val.IsEqual(routerPubKey)
},
)
return isDummy
}
// peelBlindedPathDummyHop packages the next onion packet and then constructs
// a new hop iterator using our router and then proceeds to process the next
// packet. This can only be done for blinded route dummy hops since we expect
// to be the final hop on the path.
func peelBlindedPathDummyHop(r *sphinxHopIterator, cltvExpiryDelta uint32,
fwdAmt lnwire.MilliSatoshi, routeRole RouteRole,
nextEph tlv.RecordT[tlv.TlvType8, *btcec.PublicKey]) (*Payload,
RouteRole, error) {
onionPkt := r.processedPacket.NextPacket
sphinxPacket, err := r.router.ReconstructOnionPacket(
onionPkt, r.rHash, sphinx.WithBlindingPoint(nextEph.Val),
)
if err != nil {
return nil, routeRole, err
}
iterator := makeSphinxHopIterator(
r.router, onionPkt, sphinxPacket, BlindingKit{
Processor: r.router,
UpdateAddBlinding: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType]( //nolint:lll
nextEph.Val,
),
),
IncomingAmount: fwdAmt,
IncomingCltv: r.blindingKit.IncomingCltv -
cltvExpiryDelta,
}, r.rHash,
)
return extractTLVPayload(iterator)
}
// decryptAndValidateBlindedRouteData decrypts the encrypted payload from the
// payment recipient using a blinding key. The incoming HTLC amount and CLTV
// values are then verified against the policy values from the recipient.
func decryptAndValidateBlindedRouteData(r *sphinxHopIterator,
payload *Payload) (*record.BlindedRouteData, *btcec.PublicKey, error) {
blindingPoint, err := r.blindingKit.getBlindingPoint(
payload.blindingPoint,
)
if err != nil {
return nil, nil, err
}
decrypted, err := r.blindingKit.Processor.DecryptBlindedHopData(
blindingPoint, payload.encryptedData,
)
if err != nil {
return nil, nil, fmt.Errorf("decrypt blinded data: %w", err)
}
buf := bytes.NewBuffer(decrypted)
routeData, err := record.DecodeBlindedRouteData(buf)
if err != nil {
return nil, nil, fmt.Errorf("%w: %w", ErrDecodeFailed, err)
}
err = ValidateBlindedRouteData(
routeData, r.blindingKit.IncomingAmount,
r.blindingKit.IncomingCltv,
)
if err != nil {
return nil, nil, err
}
return routeData, blindingPoint, nil
}
// parseAndValidateSenderPayload parses the payload bytes received from the
// onion constructor (the sender) and validates that various fields have been
// set. It also uses the presence of a blinding key in either the
// update_add_htlc message or in the payload to determine the RouteRole.
// The RouteRole is returned even if an error is returned. The boolean return
// value indicates that the sender payload includes encrypted data from the
// recipient that should be parsed.
func parseAndValidateSenderPayload(payloadBytes []byte, isFinalHop,
updateAddBlindingSet bool) (*Payload, RouteRole, bool, error) {
// Extract TLVs from the packet constructor (the sender).
payload, parsed, err := ParseTLVPayload(bytes.NewReader(payloadBytes))
if err != nil {
// If we couldn't even parse our payload then we do a
// best-effort of determining our role in a blinded route,
// accepting that we can't know whether we were the introduction
// node (as the payload is not parseable).
routeRole := RouteRoleCleartext
if updateAddBlindingSet {
routeRole = RouteRoleRelaying
}
return nil, routeRole, false, err
}
// Now that we've parsed our payload we can determine which role we're
// playing in the route.
_, payloadBlinding := parsed[record.BlindingPointOnionType]
routeRole := NewRouteRole(updateAddBlindingSet, payloadBlinding)
// Validate the presence of the various payload fields we received from
// the sender.
err = ValidateTLVPayload(parsed, isFinalHop, updateAddBlindingSet)
if err != nil {
return nil, routeRole, false, err
}
// If there is no encrypted data from the receiver then return the
// payload as is since the forwarding info would have been received
// from the sender.
if payload.encryptedData == nil {
return payload, routeRole, false, nil
}
// Validate the presence of various fields in the sender payload given
// that we now know that this is a hop with instructions from the
// recipient.
err = ValidatePayloadWithBlinded(isFinalHop, parsed)
if err != nil {
return payload, routeRole, true, err
}
return payload, routeRole, true, nil
}
// ExtractErrorEncrypter decodes and returns the ErrorEncrypter for this hop,
// along with a failure code to signal if the decoding was successful. The
// ErrorEncrypter is used to encrypt errors back to the sender in the event that
@ -324,118 +578,6 @@ func (b *BlindingKit) getBlindingPoint(payloadBlinding *btcec.PublicKey) (
}
}
// DecryptAndValidateFwdInfo performs all operations required to decrypt and
// validate a blinded route.
func (b *BlindingKit) DecryptAndValidateFwdInfo(payload *Payload,
isFinalHop bool, payloadParsed map[tlv.Type][]byte) (
*ForwardingInfo, error) {
// We expect this function to be called when we have encrypted data
// present, and expect validation to already have ensured that a
// blinding key is set either in the payload or the
// update_add_htlc message.
blindingPoint, err := b.getBlindingPoint(payload.blindingPoint)
if err != nil {
return nil, err
}
decrypted, err := b.Processor.DecryptBlindedHopData(
blindingPoint, payload.encryptedData,
)
if err != nil {
return nil, fmt.Errorf("decrypt blinded "+
"data: %w", err)
}
buf := bytes.NewBuffer(decrypted)
routeData, err := record.DecodeBlindedRouteData(buf)
if err != nil {
return nil, fmt.Errorf("%w: %w",
ErrDecodeFailed, err)
}
// Validate the contents of the payload against the values we've
// just pulled out of the encrypted data blob.
err = ValidatePayloadWithBlinded(isFinalHop, payloadParsed)
if err != nil {
return nil, err
}
// Validate the data in the blinded route against our incoming htlc's
// information.
if err := ValidateBlindedRouteData(
routeData, b.IncomingAmount, b.IncomingCltv,
); err != nil {
return nil, err
}
// Exit early if this onion is for the exit hop of the route since
// route blinding receives are not yet supported.
if isFinalHop {
return nil, fmt.Errorf("being the final hop in a blinded " +
"path is not yet supported")
}
// At this point, we know we are a forwarding node for this onion
// and so we expect the relay info and next SCID fields to be set.
relayInfo, err := routeData.RelayInfo.UnwrapOrErr(
fmt.Errorf("relay info not set for non-final blinded hop"),
)
if err != nil {
return nil, err
}
nextSCID, err := routeData.ShortChannelID.UnwrapOrErr(
fmt.Errorf("next SCID not set for non-final blinded hop"),
)
if err != nil {
return nil, err
}
fwdAmt, err := calculateForwardingAmount(
b.IncomingAmount, relayInfo.Val.BaseFee, relayInfo.Val.FeeRate,
)
if err != nil {
return nil, err
}
// If we have an override for the blinding point for the next node,
// we'll just use it without tweaking (the sender intended to switch
// out directly for this blinding point). Otherwise, we'll tweak our
// blinding point to get the next ephemeral key.
nextEph, err := routeData.NextBlindingOverride.UnwrapOrFuncErr(
func() (tlv.RecordT[tlv.TlvType8,
*btcec.PublicKey], error) {
next, err := b.Processor.NextEphemeral(blindingPoint)
if err != nil {
// Return a zero record because we expect the
// error to be checked.
return routeData.NextBlindingOverride.Zero(),
err
}
return tlv.NewPrimitiveRecord[tlv.TlvType8](next), nil
},
)
if err != nil {
return nil, err
}
return &ForwardingInfo{
NextHop: nextSCID.Val,
AmountToForward: fwdAmt,
OutgoingCTLV: b.IncomingCltv - uint32(
relayInfo.Val.CltvExpiryDelta,
),
// Remap from blinding override type to blinding point type.
NextBlinding: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
nextEph.Val),
),
}, nil
}
// calculateForwardingAmount calculates the amount to forward for a blinded
// hop based on the incoming amount and forwarding parameters.
//
@ -465,11 +607,11 @@ func (b *BlindingKit) DecryptAndValidateFwdInfo(payload *Payload,
// ceil(a/b) = (a + b - 1)/(b).
//
//nolint:lll,dupword
func calculateForwardingAmount(incomingAmount lnwire.MilliSatoshi, baseFee,
func calculateForwardingAmount(incomingAmount, baseFee lnwire.MilliSatoshi,
proportionalFee uint32) (lnwire.MilliSatoshi, error) {
// Sanity check to prevent overflow.
if incomingAmount < lnwire.MilliSatoshi(baseFee) {
if incomingAmount < baseFee {
return 0, fmt.Errorf("incoming amount: %v < base fee: %v",
incomingAmount, baseFee)
}
@ -558,12 +700,14 @@ func (p *OnionProcessor) ReconstructHopIterator(r io.Reader, rHash []byte,
return nil, err
}
return makeSphinxHopIterator(onionPkt, sphinxPacket, BlindingKit{
Processor: p.router,
UpdateAddBlinding: blindingInfo.BlindingKey,
IncomingAmount: blindingInfo.IncomingAmt,
IncomingCltv: blindingInfo.IncomingExpiry,
}), nil
return makeSphinxHopIterator(p.router, onionPkt, sphinxPacket,
BlindingKit{
Processor: p.router,
UpdateAddBlinding: blindingInfo.BlindingKey,
IncomingAmount: blindingInfo.IncomingAmt,
IncomingCltv: blindingInfo.IncomingExpiry,
}, rHash,
), nil
}
// DecodeHopIteratorRequest encapsulates all date necessary to process an onion
@ -741,12 +885,12 @@ func (p *OnionProcessor) DecodeHopIterators(id []byte,
// Finally, construct a hop iterator from our processed sphinx
// packet, simultaneously caching the original onion packet.
resp.HopIterator = makeSphinxHopIterator(
&onionPkts[i], &packets[i], BlindingKit{
p.router, &onionPkts[i], &packets[i], BlindingKit{
Processor: p.router,
UpdateAddBlinding: reqs[i].BlindingPoint,
IncomingAmount: reqs[i].IncomingAmount,
IncomingCltv: reqs[i].IncomingCltv,
},
}, reqs[i].RHash,
)
}

View file

@ -111,7 +111,7 @@ func TestForwardingAmountCalc(t *testing.T) {
tests := []struct {
name string
incomingAmount lnwire.MilliSatoshi
baseFee uint32
baseFee lnwire.MilliSatoshi
proportional uint32
forwardAmount lnwire.MilliSatoshi
expectErr bool
@ -177,11 +177,11 @@ func (m *mockProcessor) NextEphemeral(*btcec.PublicKey) (*btcec.PublicKey,
return nil, nil
}
// TestDecryptAndValidateFwdInfo tests deriving forwarding info using a
// TestParseAndValidateRecipientData tests deriving forwarding info using a
// blinding kit. This test does not cover assertions on the calculations of
// forwarding information, because this is covered in a test dedicated to those
// calculations.
func TestDecryptAndValidateFwdInfo(t *testing.T) {
func TestParseAndValidateRecipientData(t *testing.T) {
t.Parallel()
// Encode valid blinding data that we'll fake decrypting for our test.
@ -206,6 +206,9 @@ func TestDecryptAndValidateFwdInfo(t *testing.T) {
// Mocked error.
errDecryptFailed := errors.New("could not decrypt")
nodeKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
tests := []struct {
name string
data []byte
@ -282,12 +285,19 @@ func TestDecryptAndValidateFwdInfo(t *testing.T) {
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](testCase.updateAddBlinding),
)
}
_, err := kit.DecryptAndValidateFwdInfo(
&Payload{
iterator := &sphinxHopIterator{
blindingKit: kit,
router: sphinx.NewRouter(
&sphinx.PrivKeyECDH{PrivKey: nodeKey},
sphinx.NewMemoryReplayLog(),
),
}
_, _, err = parseAndValidateRecipientData(
iterator, &Payload{
encryptedData: testCase.data,
blindingPoint: testCase.payloadBlinding,
}, false,
make(map[tlv.Type][]byte),
}, false, RouteRoleCleartext,
)
require.ErrorIs(t, err, testCase.expectedErr)
})

View file

@ -6,6 +6,7 @@ import (
"io"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
@ -408,6 +409,12 @@ func (h *Payload) BlindingPoint() *btcec.PublicKey {
return h.blindingPoint
}
// PathID returns the path ID that was encoded in the final hop payload of a
// blinded payment.
func (h *Payload) PathID() *chainhash.Hash {
return h.FwdInfo.PathID
}
// Metadata returns the additional data that is sent along with the
// payment to the payee.
func (h *Payload) Metadata() []byte {
@ -460,10 +467,6 @@ func getMinRequiredViolation(set tlv.TypeMap) *tlv.Type {
// the route "expires" and a malicious party does not have endless opportunity
// to probe the blinded route and compare it to updated channel policies in
// the network.
//
// Note that this function only validates blinded route data for forwarding
// nodes, as LND does not yet support receiving via a blinded route (which has
// different validation rules).
func ValidateBlindedRouteData(blindedData *record.BlindedRouteData,
incomingAmount lnwire.MilliSatoshi, incomingTimelock uint32) error {

View file

@ -3774,9 +3774,6 @@ func (l *channelLink) sendHTLCError(pd *lnwallet.PaymentDescriptor,
// that we're not part of a blinded route and an error encrypter that'll be
// used if we are the introduction node and need to present an error as if
// we're the failing party.
//
// Note: this function does not yet handle special error cases for receiving
// nodes in blinded paths, as LND does not support blinded receives.
func (l *channelLink) sendIncomingHTLCFailureMsg(htlcIndex uint64,
e hop.ErrorEncrypter,
originalFailure lnwire.OpaqueReason) error {

View file

@ -4,6 +4,7 @@ import (
"context"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
@ -105,6 +106,14 @@ type Payload interface {
// Metadata returns the additional data that is sent along with the
// payment to the payee.
Metadata() []byte
// PathID returns the path ID encoded in the payload of a blinded
// payment.
PathID() *chainhash.Hash
// TotalAmtMsat returns the total amount sent to the final hop, as set
// by the payee.
TotalAmtMsat() lnwire.MilliSatoshi
}
// InvoiceQuery represents a query to the invoice database. The query allows a

View file

@ -902,6 +902,8 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
mpp: payload.MultiPath(),
amp: payload.AMPRecord(),
metadata: payload.Metadata(),
pathID: payload.PathID(),
totalAmtMsat: payload.TotalAmtMsat(),
}
switch {

View file

@ -30,6 +30,8 @@ type mockPayload struct {
amp *record.AMP
customRecords record.CustomSet
metadata []byte
pathID *chainhash.Hash
totalAmtMsat lnwire.MilliSatoshi
}
func (p *mockPayload) MultiPath() *record.MPP {
@ -40,6 +42,14 @@ func (p *mockPayload) AMPRecord() *record.AMP {
return p.amp
}
func (p *mockPayload) PathID() *chainhash.Hash {
return p.pathID
}
func (p *mockPayload) TotalAmtMsat() lnwire.MilliSatoshi {
return p.totalAmtMsat
}
func (p *mockPayload) CustomRecords() record.CustomSet {
// This function should always return a map instance, but for mock
// configuration we do accept nil.

View file

@ -1,9 +1,11 @@
package invoices
import (
"bytes"
"encoding/hex"
"errors"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightningnetwork/lnd/amp"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
@ -23,12 +25,17 @@ type invoiceUpdateCtx struct {
mpp *record.MPP
amp *record.AMP
metadata []byte
pathID *chainhash.Hash
totalAmtMsat lnwire.MilliSatoshi
}
// invoiceRef returns an identifier that can be used to lookup or update the
// invoice this HTLC is targeting.
func (i *invoiceUpdateCtx) invoiceRef() InvoiceRef {
switch {
case i.pathID != nil:
return InvoiceRefByHashAndAddr(i.hash, *i.pathID)
case i.amp != nil && i.mpp != nil:
payAddr := i.mpp.PaymentAddr()
return InvoiceRefByAddr(payAddr)
@ -130,7 +137,7 @@ func updateInvoice(ctx *invoiceUpdateCtx, inv *Invoice) (
// If no MPP payload was provided, then we expect this to be a keysend,
// or a payment to an invoice created before we started to require the
// MPP payload.
if ctx.mpp == nil {
if ctx.mpp == nil && ctx.pathID == nil {
return updateLegacy(ctx, inv)
}
@ -158,12 +165,27 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc,
setID := ctx.setID()
var (
totalAmt = ctx.totalAmtMsat
paymentAddr []byte
)
// If an MPP record is present, then the payment address and total
// payment amount is extracted from it. Otherwise, the pathID is used
// to extract the payment address.
if ctx.mpp != nil {
totalAmt = ctx.mpp.TotalMsat()
payAddr := ctx.mpp.PaymentAddr()
paymentAddr = payAddr[:]
} else {
paymentAddr = ctx.pathID[:]
}
// Start building the accept descriptor.
acceptDesc := &HtlcAcceptDesc{
Amt: ctx.amtPaid,
Expiry: ctx.expiry,
AcceptHeight: ctx.currentHeight,
MppTotalAmt: ctx.mpp.TotalMsat(),
MppTotalAmt: totalAmt,
CustomRecords: ctx.customRecords,
}
@ -184,18 +206,18 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc,
}
// Check the payment address that authorizes the payment.
if ctx.mpp.PaymentAddr() != inv.Terms.PaymentAddr {
if !bytes.Equal(paymentAddr, inv.Terms.PaymentAddr[:]) {
return nil, ctx.failRes(ResultAddressMismatch), nil
}
// Don't accept zero-valued sets.
if ctx.mpp.TotalMsat() == 0 {
if totalAmt == 0 {
return nil, ctx.failRes(ResultHtlcSetTotalTooLow), nil
}
// Check that the total amt of the htlc set is high enough. In case this
// is a zero-valued invoice, it will always be enough.
if ctx.mpp.TotalMsat() < inv.Terms.Value {
if totalAmt < inv.Terms.Value {
return nil, ctx.failRes(ResultHtlcSetTotalTooLow), nil
}
@ -204,7 +226,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc,
// Check whether total amt matches other htlcs in the set.
var newSetTotal lnwire.MilliSatoshi
for _, htlc := range htlcSet {
if ctx.mpp.TotalMsat() != htlc.MppTotalAmt {
if totalAmt != htlc.MppTotalAmt {
return nil, ctx.failRes(ResultHtlcSetTotalMismatch), nil
}
@ -238,7 +260,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *Invoice) (*InvoiceUpdateDesc,
}
// If the invoice cannot be settled yet, only record the htlc.
setComplete := newSetTotal >= ctx.mpp.TotalMsat()
setComplete := newSetTotal >= totalAmt
if !setComplete {
return &update, ctx.acceptRes(resultPartialAccepted), nil
}

View file

@ -563,8 +563,8 @@ var allTestCases = []*lntest.TestCase{
TestFunc: testQueryBlindedRoutes,
},
{
Name: "forward blinded",
TestFunc: testForwardBlindedRoute,
Name: "route blinding invoices",
TestFunc: testBlindedRouteInvoices,
},
{
Name: "receiver blinded error",
@ -586,6 +586,14 @@ var allTestCases = []*lntest.TestCase{
Name: "on chain to blinded",
TestFunc: testErrorHandlingOnChainFailure,
},
{
Name: "mpp to single blinded path",
TestFunc: testMPPToSingleBlindedPath,
},
{
Name: "route blinding dummy hops",
TestFunc: testBlindedRouteDummyHops,
},
{
Name: "removetx",
TestFunc: testRemoveTx,

View file

@ -214,24 +214,24 @@ func testMultiHopPayments(ht *lntest.HarnessTest) {
// We expect Carol to have successful forwards and settles for
// her sends.
ht.AssertHtlcEvents(
carolEvents, numPayments, 0, numPayments,
carolEvents, numPayments, 0, numPayments, 0,
routerrpc.HtlcEvent_SEND,
)
// Dave and Alice should both have forwards and settles for
// their role as forwarding nodes.
ht.AssertHtlcEvents(
daveEvents, numPayments, 0, numPayments,
daveEvents, numPayments, 0, numPayments, 0,
routerrpc.HtlcEvent_FORWARD,
)
ht.AssertHtlcEvents(
aliceEvents, numPayments, 0, numPayments,
aliceEvents, numPayments, 0, numPayments, 0,
routerrpc.HtlcEvent_FORWARD,
)
// Bob should only have settle events for his receives.
ht.AssertHtlcEvents(
bobEvents, 0, 0, numPayments, routerrpc.HtlcEvent_RECEIVE,
bobEvents, 0, 0, numPayments, 0, routerrpc.HtlcEvent_RECEIVE,
)
// Finally, close all channels.

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,38 @@
package lncfg
// DefaultHoldInvoiceExpiryDelta defines the number of blocks before the expiry
// height of a hold invoice's htlc that lnd will automatically cancel the
// invoice to prevent the channel from force closing. This value *must* be
// greater than DefaultIncomingBroadcastDelta to prevent force closes.
const DefaultHoldInvoiceExpiryDelta = DefaultIncomingBroadcastDelta + 2
const (
// DefaultHoldInvoiceExpiryDelta defines the number of blocks before the
// expiry height of a hold invoice's htlc that lnd will automatically
// cancel the invoice to prevent the channel from force closing. This
// value *must* be greater than DefaultIncomingBroadcastDelta to prevent
// force closes.
DefaultHoldInvoiceExpiryDelta = DefaultIncomingBroadcastDelta + 2
// DefaultMinNumRealBlindedPathHops is the minimum number of _real_
// hops to include in a blinded payment path. This doesn't include our
// node (the destination node), so if the minimum is 1, then the path
// will contain at minimum our node along with an introduction node hop.
DefaultMinNumRealBlindedPathHops = 1
// DefaultNumBlindedPathHops is the number of hops to include in a
// blinded payment path. If paths shorter than this number are found,
// then dummy hops are used to pad the path to this length.
DefaultNumBlindedPathHops = 2
// DefaultMaxNumBlindedPaths is the maximum number of different blinded
// payment paths to include in an invoice.
DefaultMaxNumBlindedPaths = 3
// DefaultBlindedPathPolicyIncreaseMultiplier is the default multiplier
// used to increase certain blinded hop policy values in order to add
// a probing buffer.
DefaultBlindedPathPolicyIncreaseMultiplier = 1.1
// DefaultBlindedPathPolicyDecreaseMultiplier is the default multiplier
// used to decrease certain blinded hop policy values in order to add a
// probing buffer.
DefaultBlindedPathPolicyDecreaseMultiplier = 0.9
)
// Invoices holds the configuration options for invoices.
//
@ -12,3 +40,21 @@ const DefaultHoldInvoiceExpiryDelta = DefaultIncomingBroadcastDelta + 2
type Invoices struct {
HoldExpiryDelta uint32 `long:"holdexpirydelta" description:"The number of blocks before a hold invoice's htlc expires that the invoice should be canceled to prevent a force close. Force closes will not be prevented if this value is not greater than DefaultIncomingBroadcastDelta."`
}
// Validate checks that the various invoice config options are sane.
//
// NOTE: this is part of the Validator interface.
func (i *Invoices) Validate() error {
// Log a warning if our expiry delta is not greater than our incoming
// broadcast delta. We do not fail here because this value may be set
// to zero to intentionally keep lnd's behavior unchanged from when we
// didn't auto-cancel these invoices.
if i.HoldExpiryDelta <= DefaultIncomingBroadcastDelta {
log.Warnf("Invoice hold expiry delta: %v <= incoming "+
"delta: %v, accepted hold invoices will force close "+
"channels if they are not canceled manually",
i.HoldExpiryDelta, DefaultIncomingBroadcastDelta)
}
return nil
}

32
lncfg/log.go Normal file
View file

@ -0,0 +1,32 @@
package lncfg
import (
"github.com/btcsuite/btclog"
"github.com/lightningnetwork/lnd/build"
)
// log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the caller
// requests it.
var log btclog.Logger
// Subsystem defines the logging code for this subsystem.
const Subsystem = "CNFG"
// The default amount of logging is none.
func init() {
UseLogger(build.NewSubLogger(Subsystem, nil))
}
// DisableLog disables all library log output. Logging output is disabled
// by default until UseLogger is called.
func DisableLog() {
UseLogger(btclog.Disabled)
}
// UseLogger uses a specified Logger to output package logging info.
// This should be used in preference to SetLogWriter if the caller is also
// using btclog.
func UseLogger(logger btclog.Logger) {
log = logger
}

View file

@ -1,5 +1,7 @@
package lncfg
import "fmt"
// Routing holds the configuration options for routing.
//
//nolint:lll
@ -7,4 +9,42 @@ type Routing struct {
AssumeChannelValid bool `long:"assumechanvalid" description:"DEPRECATED: Skip checking channel spentness during graph validation. This speedup comes at the risk of using an unvalidated view of the network for routing. (default: false)" hidden:"true"`
StrictZombiePruning bool `long:"strictgraphpruning" description:"If true, then the graph will be pruned more aggressively for zombies. In practice this means that edges with a single stale edge will be considered a zombie."`
BlindedPaths BlindedPaths `group:"blinding" namespace:"blinding"`
}
// BlindedPaths holds the configuration options for blinded path construction.
//
//nolint:lll
type BlindedPaths struct {
MinNumRealHops uint8 `long:"min-num-real-hops" description:"The minimum number of real hops to include in a blinded path. This doesn't include our node, so if the minimum is 1, then the path will contain at minimum our node along with an introduction node hop. If it is zero then the shortest path will use our node as an introduction node."`
NumHops uint8 `long:"num-hops" description:"The number of hops to include in a blinded path. This doesn't include our node, so if it is 1, then the path will contain our node along with an introduction node or dummy node hop. If paths shorter than NumHops is found, then they will be padded using dummy hops."`
MaxNumPaths uint8 `long:"max-num-paths" description:"The maximum number of blinded paths to select and add to an invoice."`
PolicyIncreaseMultiplier float64 `long:"policy-increase-multiplier" description:"The amount by which to increase certain policy values of hops on a blinded path in order to add a probing buffer."`
PolicyDecreaseMultiplier float64 `long:"policy-decrease-multiplier" description:"The amount by which to decrease certain policy values of hops on a blinded path in order to add a probing buffer."`
}
// Validate checks that the various routing config options are sane.
//
// NOTE: this is part of the Validator interface.
func (r *Routing) Validate() error {
if r.BlindedPaths.MinNumRealHops > r.BlindedPaths.NumHops {
return fmt.Errorf("the minimum number of real hops in a " +
"blinded path must be smaller than or equal to the " +
"number of hops expected to be included in each path")
}
if r.BlindedPaths.PolicyIncreaseMultiplier < 1 {
return fmt.Errorf("the blinded route policy increase " +
"multiplier must be greater than or equal to 1")
}
if r.BlindedPaths.PolicyDecreaseMultiplier > 1 ||
r.BlindedPaths.PolicyDecreaseMultiplier < 0 {
return fmt.Errorf("the blinded route policy decrease " +
"multiplier must be in the range (0,1]")
}
return nil
}

View file

@ -12,8 +12,10 @@ import (
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models"
@ -23,6 +25,8 @@ import (
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/netann"
"github.com/lightningnetwork/lnd/routing"
"github.com/lightningnetwork/lnd/routing/blindedpath"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/zpay32"
)
@ -84,6 +88,36 @@ type AddInvoiceConfig struct {
// GetAlias allows the peer's alias SCID to be retrieved for private
// option_scid_alias channels.
GetAlias func(lnwire.ChannelID) (lnwire.ShortChannelID, error)
// BestHeight returns the current best block height that this node is
// aware of.
BestHeight func() (uint32, error)
// QueryBlindedRoutes can be used to generate a few routes to this node
// that can then be used in the construction of a blinded payment path.
QueryBlindedRoutes func(lnwire.MilliSatoshi) ([]*route.Route, error)
// BlindedRoutePolicyIncrMultiplier is the amount by which policy values
// for hops in a blinded route will be bumped to avoid easy probing. For
// example, a multiplier of 1.1 will bump all appropriate the values
// (base fee, fee rate, CLTV delta and min HLTC) by 10%.
BlindedRoutePolicyIncrMultiplier float64
// BlindedRoutePolicyDecrMultiplier is the amount by which appropriate
// policy values for hops in a blinded route will be decreased to avoid
// easy probing. For example, a multiplier of 0.9 will reduce
// appropriate values (like maximum HTLC) by 10%.
BlindedRoutePolicyDecrMultiplier float64
// MinNumBlindedPathHops is the minimum number of hops that a blinded
// path should be. Dummy hops will be used to pad any route with a
// length less than this.
MinNumBlindedPathHops uint8
// DefaultDummyHopPolicy holds the default policy values to use for
// dummy hops in a blinded path in the case where they cant be derived
// through other means.
DefaultDummyHopPolicy *blindedpath.BlindedHopPolicy
}
// AddInvoiceData contains the required data to create a new invoice.
@ -134,6 +168,11 @@ type AddInvoiceData struct {
// NOTE: Preimage should always be set to nil when this value is true.
Amp bool
// Blind signals that this invoice should disguise the location of the
// recipient by adding blinded payment paths to the invoice instead of
// revealing the destination node's real pub key.
Blind bool
// RouteHints are optional route hints that can each be individually
// used to assist in reaching the invoice's destination.
RouteHints [][]zpay32.HopHint
@ -238,6 +277,11 @@ func (d *AddInvoiceData) mppPaymentHashAndPreimage() (*lntypes.Preimage,
func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
invoice *AddInvoiceData) (*lntypes.Hash, *invoices.Invoice, error) {
if invoice.Amp && invoice.Blind {
return nil, nil, fmt.Errorf("AMP invoices with blinded paths " +
"are not yet supported")
}
paymentPreimage, paymentHash, err := invoice.paymentHashAndPreimage()
if err != nil {
return nil, nil, err
@ -309,10 +353,9 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
options = append(options, zpay32.FallbackAddr(addr))
}
var expiry time.Duration
switch {
// If expiry is set, specify it. If it is not provided, no expiry time
// will be explicitly added to this payment request, which will imply
// the default 3600 seconds.
// An invoice expiry has been provided by the caller.
case invoice.Expiry > 0:
// We'll ensure that the specified expiry is restricted to sane
@ -327,19 +370,19 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
float64(expSeconds), maxExpiry.Seconds())
}
expiry := time.Duration(invoice.Expiry) * time.Second
options = append(options, zpay32.Expiry(expiry))
expiry = time.Duration(invoice.Expiry) * time.Second
// If no custom expiry is provided, use the default MPP expiry.
case !invoice.Amp:
options = append(options, zpay32.Expiry(DefaultInvoiceExpiry))
expiry = DefaultInvoiceExpiry
// Otherwise, use the default AMP expiry.
default:
defaultExpiry := zpay32.Expiry(DefaultAMPInvoiceExpiry)
options = append(options, defaultExpiry)
expiry = DefaultAMPInvoiceExpiry
}
options = append(options, zpay32.Expiry(expiry))
// If the description hash is set, then we add it do the list of
// options. If not, use the memo field as the payment request
// description.
@ -353,15 +396,16 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
options = append(options, zpay32.Description(invoice.Memo))
}
// We'll use our current default CLTV value unless one was specified as
// an option on the command line when creating an invoice.
switch {
case invoice.CltvExpiry > routing.MaxCLTVDelta:
if invoice.CltvExpiry > routing.MaxCLTVDelta {
return nil, nil, fmt.Errorf("CLTV delta of %v is too large, "+
"max accepted is: %v", invoice.CltvExpiry,
math.MaxUint16)
}
case invoice.CltvExpiry != 0:
// We'll use our current default CLTV value unless one was specified as
// an option on the command line when creating an invoice.
cltvExpiryDelta := uint64(cfg.DefaultCLTVExpiry)
if invoice.CltvExpiry != 0 {
// Disallow user-chosen final CLTV deltas below the required
// minimum.
if invoice.CltvExpiry < routing.MinCLTVDelta {
@ -370,13 +414,14 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
invoice.CltvExpiry, routing.MinCLTVDelta)
}
options = append(options,
zpay32.CLTVExpiry(invoice.CltvExpiry))
cltvExpiryDelta = invoice.CltvExpiry
}
default:
// TODO(roasbeef): assumes set delta between versions
defaultCLTVExpiry := uint64(cfg.DefaultCLTVExpiry)
options = append(options, zpay32.CLTVExpiry(defaultCLTVExpiry))
// Only include a final CLTV expiry delta if this is not a blinded
// invoice. In a blinded invoice, this value will be added to the total
// blinded route CLTV delta value
if !invoice.Blind {
options = append(options, zpay32.CLTVExpiry(cltvExpiryDelta))
}
// We make sure that the given invoice routing hints number is within
@ -388,6 +433,11 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
// Include route hints if needed.
if len(invoice.RouteHints) > 0 || invoice.Private {
if invoice.Blind {
return nil, nil, fmt.Errorf("can't set both hop " +
"hints and add blinded payment paths")
}
// Validate provided hop hints.
for _, hint := range invoice.RouteHints {
if len(hint) == 0 {
@ -430,14 +480,71 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
}
options = append(options, zpay32.Features(invoiceFeatures))
// Generate and set a random payment address for this invoice. If the
// Generate and set a random payment address for this payment. If the
// sender understands payment addresses, this can be used to avoid
// intermediaries probing the receiver.
// intermediaries probing the receiver. If the invoice does not have
// blinded paths, then this will be encoded in the invoice itself.
// Otherwise, it will instead be embedded in the encrypted recipient
// data of blinded paths. In the blinded path case, this will be used
// for the PathID.
var paymentAddr [32]byte
if _, err := rand.Read(paymentAddr[:]); err != nil {
return nil, nil, err
}
options = append(options, zpay32.PaymentAddr(paymentAddr))
if invoice.Blind {
// Use the 10-min-per-block assumption to get a rough estimate
// of the number of blocks until the invoice expires. We want
// to make sure that the blinded path definitely does not expire
// before the invoice does, and so we add a healthy buffer.
invoiceExpiry := uint32(expiry.Minutes() / 10)
blindedPathExpiry := invoiceExpiry * 2
// Add BlockPadding to the finalCltvDelta so that the receiving
// node does not reject the HTLC if some blocks are mined while
// the payment is in-flight. Note that unlike vanilla invoices,
// with blinded paths, the recipient is responsible for adding
// this block padding instead of the sender.
finalCLTVDelta := uint32(cltvExpiryDelta)
finalCLTVDelta += uint32(routing.BlockPadding)
//nolint:lll
paths, err := blindedpath.BuildBlindedPaymentPaths(
&blindedpath.BuildBlindedPathCfg{
FindRoutes: cfg.QueryBlindedRoutes,
FetchChannelEdgesByID: cfg.Graph.FetchChannelEdgesByID,
FetchOurOpenChannels: cfg.ChanDB.FetchAllOpenChannels,
PathID: paymentAddr[:],
ValueMsat: invoice.Value,
BestHeight: cfg.BestHeight,
MinFinalCLTVExpiryDelta: finalCLTVDelta,
BlocksUntilExpiry: blindedPathExpiry,
AddPolicyBuffer: func(
p *blindedpath.BlindedHopPolicy) (
*blindedpath.BlindedHopPolicy, error) {
//nolint:lll
return blindedpath.AddPolicyBuffer(
p, cfg.BlindedRoutePolicyIncrMultiplier,
cfg.BlindedRoutePolicyDecrMultiplier,
)
},
MinNumHops: cfg.MinNumBlindedPathHops,
DefaultDummyHopPolicy: cfg.DefaultDummyHopPolicy,
},
)
if err != nil {
return nil, nil, err
}
for _, path := range paths {
options = append(options, zpay32.WithBlindedPaymentPath(
path,
))
}
} else {
options = append(options, zpay32.PaymentAddr(paymentAddr))
}
// Create and encode the payment request as a bech32 (zpay32) string.
creationDate := time.Now()
@ -450,7 +557,27 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
payReqString, err := payReq.Encode(zpay32.MessageSigner{
SignCompact: func(msg []byte) ([]byte, error) {
return cfg.NodeSigner.SignMessageCompact(msg, false)
// For an invoice without a blinded path, the main node
// key is used to sign the invoice so that the sender
// can derive the true pub key of the recipient.
if !invoice.Blind {
return cfg.NodeSigner.SignMessageCompact(
msg, false,
)
}
// For an invoice with a blinded path, we use an
// ephemeral key to sign the invoice since we don't want
// the sender to be able to know the real pub key of
// the recipient.
ephemKey, err := btcec.NewPrivateKey()
if err != nil {
return nil, err
}
return ecdsa.SignCompact(
ephemKey, chainhash.HashB(msg), true,
)
},
})
if err != nil {

View file

@ -15,6 +15,16 @@ import (
"github.com/stretchr/testify/require"
)
var (
pubkeyBytes, _ = hex.DecodeString(
"598ec453728e0ffe0ae2f5e174243cf58f2" +
"a3f2c83d2457b43036db568b11093",
)
pubKeyY = new(btcec.FieldVal)
_ = pubKeyY.SetByteSlice(pubkeyBytes)
pubkey = btcec.NewPublicKey(new(btcec.FieldVal).SetInt(4), pubKeyY)
)
type hopHintsConfigMock struct {
t *testing.T
mock.Mock
@ -84,16 +94,6 @@ func (h *hopHintsConfigMock) FetchChannelEdgesByID(chanID uint64) (
// getTestPubKey returns a valid parsed pub key to be used in our tests.
func getTestPubKey() *btcec.PublicKey {
pubkeyBytes, _ := hex.DecodeString(
"598ec453728e0ffe0ae2f5e174243cf58f2" +
"a3f2c83d2457b43036db568b11093",
)
pubKeyY := new(btcec.FieldVal)
_ = pubKeyY.SetByteSlice(pubkeyBytes)
pubkey := btcec.NewPublicKey(
new(btcec.FieldVal).SetInt(4),
pubKeyY,
)
return pubkey
}

View file

@ -578,6 +578,10 @@
},
"description": "Maps a 32-byte hex-encoded set ID to the sub-invoice AMP state for the\ngiven set ID. This field is always populated for AMP invoices, and can be\nused along side LookupInvoice to obtain the HTLC information related to a\ngiven sub-invoice.\nNote: Output only, don't specify for creating an invoice.",
"title": "[EXPERIMENTAL]:"
},
"blind": {
"type": "boolean",
"description": "Signals that the invoice should include blinded paths to hide the true\nidentity of the recipient."
}
}
},

File diff suppressed because it is too large Load diff

View file

@ -3836,6 +3836,12 @@ message Invoice {
Note: Output only, don't specify for creating an invoice.
*/
map<string, AMPInvoiceState> amp_invoice_state = 28;
/*
Signals that the invoice should include blinded paths to hide the true
identity of the recipient.
*/
bool blind = 29;
}
enum InvoiceHTLCState {

View file

@ -5490,6 +5490,10 @@
},
"description": "Maps a 32-byte hex-encoded set ID to the sub-invoice AMP state for the\ngiven set ID. This field is always populated for AMP invoices, and can be\nused along side LookupInvoice to obtain the HTLC information related to a\ngiven sub-invoice.\nNote: Output only, don't specify for creating an invoice.",
"title": "[EXPERIMENTAL]:"
},
"blind": {
"type": "boolean",
"description": "Signals that the invoice should include blinded paths to hide the true\nidentity of the recipient."
}
}
},

View file

@ -999,6 +999,32 @@ func (r *RouterBackend) extractIntentFromSendRequest(
payIntent.PaymentAddr = payAddr
payIntent.PaymentRequest = []byte(rpcPayReq.PaymentRequest)
payIntent.Metadata = payReq.Metadata
if len(payReq.BlindedPaymentPaths) > 0 {
// NOTE: Currently we only choose a single payment path.
// This will be updated in a future PR to handle
// multiple blinded payment paths.
path := payReq.BlindedPaymentPaths[0]
if len(path.Hops) == 0 {
return nil, fmt.Errorf("a blinded payment " +
"must have at least 1 hop")
}
finalHop := path.Hops[len(path.Hops)-1]
payIntent.BlindedPayment = MarshalBlindedPayment(path)
// Replace the target node with the blinded public key
// of the blinded path's final node.
copy(
payIntent.Target[:],
finalHop.BlindedNodePub.SerializeCompressed(),
)
if !path.Features.IsEmpty() {
payIntent.DestFeatures = path.Features.Clone()
}
}
} else {
// Otherwise, If the payment request field was not specified
// (and a custom route wasn't specified), construct the payment
@ -1137,6 +1163,26 @@ func (r *RouterBackend) extractIntentFromSendRequest(
return payIntent, nil
}
// MarshalBlindedPayment marshals a zpay32.BLindedPaymentPath into a
// routing.BlindedPayment.
func MarshalBlindedPayment(
path *zpay32.BlindedPaymentPath) *routing.BlindedPayment {
return &routing.BlindedPayment{
BlindedPath: &sphinx.BlindedPath{
IntroductionPoint: path.Hops[0].BlindedNodePub,
BlindingPoint: path.FirstEphemeralBlindingPoint,
BlindedHops: path.Hops,
},
BaseFee: path.FeeBaseMsat,
ProportionalFeeRate: path.FeeRate,
CltvExpiryDelta: path.CltvExpiryDelta,
HtlcMinimum: path.HTLCMinMsat,
HtlcMaximum: path.HTLCMaxMsat,
Features: path.Features,
}
}
// unmarshallRouteHints unmarshalls a list of route hints.
func unmarshallRouteHints(rpcRouteHints []*lnrpc.RouteHint) (
[][]zpay32.HopHint, error) {

View file

@ -2218,12 +2218,12 @@ func (h *HarnessTest) AssertFeeReport(hn *node.HarnessNode,
//
// TODO(yy): needs refactor to reduce its complexity.
func (h *HarnessTest) AssertHtlcEvents(client rpc.HtlcEventsClient,
fwdCount, fwdFailCount, settleCount int,
fwdCount, fwdFailCount, settleCount, linkFailCount int,
userType routerrpc.HtlcEvent_EventType) []*routerrpc.HtlcEvent {
var forwards, forwardFails, settles int
var forwards, forwardFails, settles, linkFails int
numEvents := fwdCount + fwdFailCount + settleCount
numEvents := fwdCount + fwdFailCount + settleCount + linkFailCount
events := make([]*routerrpc.HtlcEvent, 0)
// It's either the userType or the unknown type.
@ -2256,6 +2256,9 @@ func (h *HarnessTest) AssertHtlcEvents(client rpc.HtlcEventsClient,
settles++
}
case *routerrpc.HtlcEvent_LinkFailEvent:
linkFails++
default:
require.Fail(h, "assert event fail",
"unexpected event: %T", event.Event)
@ -2266,6 +2269,7 @@ func (h *HarnessTest) AssertHtlcEvents(client rpc.HtlcEventsClient,
require.Equal(h, fwdFailCount, forwardFails,
"num of forward fails mismatch")
require.Equal(h, settleCount, settles, "num of settles mismatch")
require.Equal(h, linkFailCount, linkFails, "num of link fails mismatch")
return events
}

View file

@ -223,7 +223,7 @@ const (
// able and willing to accept keysend payments.
KeysendOptional = 55
// ScriptEnforcedLeaseOptional is an optional feature bit that signals
// ScriptEnforcedLeaseRequired is a required feature bit that signals
// that the node requires channels having zero-fee second-level HTLC
// transactions, which also imply anchor commitments, along with an
// additional CLTV constraint of a channel lease's expiration height
@ -241,18 +241,17 @@ const (
// TODO: Decide on actual feature bit value.
ScriptEnforcedLeaseOptional FeatureBit = 2023
// SimpleTaprootChannelsRequredFinal is a required bit that indicates
// SimpleTaprootChannelsRequiredFinal is a required bit that indicates
// the node is able to create taproot-native channels. This is the
// final feature bit to be used once the channel type is finalized.
SimpleTaprootChannelsRequiredFinal = 80
// SimpleTaprootChannelsOptionalFinal is an optional bit that indicates
// the node is able to create taproot-native channels. This is the
// final
// feature bit to be used once the channel type is finalized.
// final feature bit to be used once the channel type is finalized.
SimpleTaprootChannelsOptionalFinal = 81
// SimpleTaprootChannelsRequredStaging is a required bit that indicates
// SimpleTaprootChannelsRequiredStaging is a required bit that indicates
// the node is able to create taproot-native channels. This is a
// feature bit used in the wild while the channel type is still being
// finalized.
@ -260,11 +259,20 @@ const (
// SimpleTaprootChannelsOptionalStaging is an optional bit that
// indicates the node is able to create taproot-native channels. This
// is a feature
// bit used in the wild while the channel type is still being
// finalized.
// is a feature bit used in the wild while the channel type is still
// being finalized.
SimpleTaprootChannelsOptionalStaging = 181
// Bolt11BlindedPathsRequired is a required feature bit that indicates
// that the node is able to understand the blinded path tagged field in
// a BOLT 11 invoice.
Bolt11BlindedPathsRequired = 260
// Bolt11BlindedPathsOptional is an optional feature bit that indicates
// that the node is able to understand the blinded path tagged field in
// a BOLT 11 invoice.
Bolt11BlindedPathsOptional = 261
// MaxBolt11Feature is the maximum feature bit value allowed in bolt 11
// invoices.
//
@ -331,6 +339,8 @@ var Features = map[FeatureBit]string{
SimpleTaprootChannelsOptionalFinal: "simple-taproot-chans",
SimpleTaprootChannelsRequiredStaging: "simple-taproot-chans-x",
SimpleTaprootChannelsOptionalStaging: "simple-taproot-chans-x",
Bolt11BlindedPathsOptional: "bolt-11-blinded-paths",
Bolt11BlindedPathsRequired: "bolt-11-blinded-paths",
}
// RawFeatureVector represents a set of feature bits as defined in BOLT-09. A

2
log.go
View file

@ -22,6 +22,7 @@ import (
"github.com/lightningnetwork/lnd/healthcheck"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
"github.com/lightningnetwork/lnd/lnrpc/devrpc"
@ -181,6 +182,7 @@ func SetupLoggers(root *build.RotatingLogWriter, interceptor signal.Interceptor)
AddSubLogger(root, rpcwallet.Subsystem, interceptor, rpcwallet.UseLogger)
AddSubLogger(root, peersrpc.Subsystem, interceptor, peersrpc.UseLogger)
AddSubLogger(root, graph.Subsystem, interceptor, graph.UseLogger)
AddSubLogger(root, lncfg.Subsystem, interceptor, lncfg.UseLogger)
}
// AddSubLogger is a helper method to conveniently create and register the

View file

@ -10,6 +10,13 @@ import (
"github.com/lightningnetwork/lnd/tlv"
)
// AverageDummyHopPayloadSize is the size of a standard blinded path dummy hop
// payload. In most cases, this is larger than the other payload types and so
// to make sure that a sender cannot use this fact to know if a dummy hop is
// present or not, we'll make sure to always pad all payloads to at least this
// size.
const AverageDummyHopPayloadSize = 51
// BlindedRouteData contains the information that is included in a blinded
// route encrypted data blob that is created by the recipient to provide
// forwarding information.
@ -22,6 +29,11 @@ type BlindedRouteData struct {
// ShortChannelID is the channel ID of the next hop.
ShortChannelID tlv.OptionalRecordT[tlv.TlvType2, lnwire.ShortChannelID]
// NextNodeID is the node ID of the next node on the path. In the
// context of blinded path payments, this is used to indicate the
// presence of dummy hops that need to be peeled from the onion.
NextNodeID tlv.OptionalRecordT[tlv.TlvType4, *btcec.PublicKey]
// PathID is a secret set of bytes that the blinded path creator will
// set so that they can check the value on decryption to ensure that the
// path they created was used for the intended purpose.
@ -98,6 +110,26 @@ func NewFinalHopBlindedRouteData(constraints *PaymentConstraints,
return &data
}
// NewDummyHopRouteData creates the data that's provided for any hop preceding
// a dummy hop. The presence of such a payload indicates to the reader that
// they are the intended recipient and should peel the remainder of the onion.
func NewDummyHopRouteData(ourPubKey *btcec.PublicKey,
relayInfo PaymentRelayInfo,
constraints PaymentConstraints) *BlindedRouteData {
return &BlindedRouteData{
NextNodeID: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType4](ourPubKey),
),
RelayInfo: tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType10](relayInfo),
),
Constraints: tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType12](constraints),
),
}
}
// DecodeBlindedRouteData decodes the data provided within a blinded route.
func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) {
var (
@ -105,6 +137,7 @@ func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) {
padding = d.Padding.Zero()
scid = d.ShortChannelID.Zero()
nextNodeID = d.NextNodeID.Zero()
pathID = d.PathID.Zero()
blindingOverride = d.NextBlindingOverride.Zero()
relayInfo = d.RelayInfo.Zero()
@ -118,8 +151,8 @@ func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) {
}
typeMap, err := tlvRecords.ExtractRecords(
&padding, &scid, &pathID, &blindingOverride, &relayInfo,
&constraints, &features,
&padding, &scid, &nextNodeID, &pathID, &blindingOverride,
&relayInfo, &constraints, &features,
)
if err != nil {
return nil, err
@ -134,6 +167,10 @@ func DecodeBlindedRouteData(r io.Reader) (*BlindedRouteData, error) {
d.ShortChannelID = tlv.SomeRecordT(scid)
}
if val, ok := typeMap[d.NextNodeID.TlvType()]; ok && val == nil {
d.NextNodeID = tlv.SomeRecordT(nextNodeID)
}
if val, ok := typeMap[d.PathID.TlvType()]; ok && val == nil {
d.PathID = tlv.SomeRecordT(pathID)
}
@ -175,6 +212,12 @@ func EncodeBlindedRouteData(data *BlindedRouteData) ([]byte, error) {
recordProducers = append(recordProducers, &scid)
})
data.NextNodeID.WhenSome(func(f tlv.RecordT[tlv.TlvType4,
*btcec.PublicKey]) {
recordProducers = append(recordProducers, &f)
})
data.PathID.WhenSome(func(pathID tlv.RecordT[tlv.TlvType6, []byte]) {
recordProducers = append(recordProducers, &pathID)
})
@ -232,8 +275,8 @@ type PaymentRelayInfo struct {
// satoshi.
FeeRate uint32
// BaseFee is the per-htlc fee charged.
BaseFee uint32
// BaseFee is the per-htlc fee charged in milli-satoshis.
BaseFee lnwire.MilliSatoshi
}
// Record creates a tlv.Record that encodes the payment relay (type 10) type for
@ -242,7 +285,7 @@ func (i *PaymentRelayInfo) Record() tlv.Record {
return tlv.MakeDynamicRecord(
10, &i, func() uint64 {
// uint16 + uint32 + tuint32
return 2 + 4 + tlv.SizeTUint32(i.BaseFee)
return 2 + 4 + tlv.SizeTUint32(uint32(i.BaseFee))
}, encodePaymentRelay, decodePaymentRelay,
)
}
@ -258,9 +301,11 @@ func encodePaymentRelay(w io.Writer, val interface{}, buf *[8]byte) error {
return err
}
baseFee := uint32(relayInfo.BaseFee)
// We can safely reuse buf here because we overwrite its
// contents.
return tlv.ETUint32(w, &relayInfo.BaseFee, buf)
return tlv.ETUint32(w, &baseFee, buf)
}
return tlv.NewTypeForEncodingErr(val, "**hop.PaymentRelayInfo")
@ -297,7 +342,15 @@ func decodePaymentRelay(r io.Reader, val interface{}, buf *[8]byte,
// is okay.
b := bytes.NewBuffer(scratch[6:])
return tlv.DTUint32(b, &relayInfo.BaseFee, buf, l-6)
var baseFee uint32
err = tlv.DTUint32(b, &baseFee, buf, l-6)
if err != nil {
return err
}
relayInfo.BaseFee = lnwire.MilliSatoshi(baseFee)
return nil
}
return tlv.NewTypeForDecodingErr(val, "*hop.paymentRelayInfo", l, 10)

View file

@ -37,7 +37,7 @@ func TestBlindedDataEncoding(t *testing.T) {
tests := []struct {
name string
baseFee uint32
baseFee lnwire.MilliSatoshi
htlcMin lnwire.MilliSatoshi
features *lnwire.FeatureVector
constraints bool
@ -171,6 +171,42 @@ func TestBlindedDataFinalHopEncoding(t *testing.T) {
}
}
// TestDummyHopBlindedDataEncoding tests the encoding and decoding of a blinded
// data blob intended for hops preceding a dummy hop in a blinded path. These
// hops provide the reader with a signal that the next hop may be a dummy hop.
func TestDummyHopBlindedDataEncoding(t *testing.T) {
t.Parallel()
priv, err := btcec.NewPrivateKey()
require.NoError(t, err)
info := PaymentRelayInfo{
FeeRate: 2,
CltvExpiryDelta: 3,
BaseFee: 30,
}
constraints := PaymentConstraints{
MaxCltvExpiry: 4,
HtlcMinimumMsat: 100,
}
routeData := NewDummyHopRouteData(priv.PubKey(), info, constraints)
encoded, err := EncodeBlindedRouteData(routeData)
require.NoError(t, err)
// Assert the size of an average dummy hop payload in case we need to
// update this constant in future.
require.Len(t, encoded, AverageDummyHopPayloadSize)
b := bytes.NewBuffer(encoded)
decodedData, err := DecodeBlindedRouteData(b)
require.NoError(t, err)
require.Equal(t, routeData, decodedData)
}
// TestBlindedRouteDataPadding tests the PadBy method of BlindedRouteData.
func TestBlindedRouteDataPadding(t *testing.T) {
newBlindedRouteData := func() *BlindedRouteData {

View file

@ -0,0 +1,985 @@
package blindedpath
import (
"bytes"
"errors"
"fmt"
"math"
"sort"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/tlv"
"github.com/lightningnetwork/lnd/zpay32"
)
const (
// oneMillion is a constant used frequently in fee rate calculations.
oneMillion = uint32(1_000_000)
)
// errInvalidBlindedPath indicates that the chosen real path is not usable as
// a blinded path.
var errInvalidBlindedPath = errors.New("the chosen path results in an " +
"unusable blinded path")
// BuildBlindedPathCfg defines the various resources and configuration values
// required to build a blinded payment path to this node.
type BuildBlindedPathCfg struct {
// FindRoutes returns a set of routes to us that can be used for the
// construction of blinded paths. These routes will consist of real
// nodes advertising the route blinding feature bit. They may be of
// various lengths and may even contain only a single hop. Any route
// shorter than MinNumHops will be padded with dummy hops during route
// construction.
FindRoutes func(value lnwire.MilliSatoshi) ([]*route.Route, error)
// FetchChannelEdgesByID attempts to look up the two directed edges for
// the channel identified by the channel ID.
FetchChannelEdgesByID func(chanID uint64) (*models.ChannelEdgeInfo,
*models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error)
// FetchOurOpenChannels fetches this node's set of open channels.
FetchOurOpenChannels func() ([]*channeldb.OpenChannel, error)
// BestHeight can be used to fetch the best block height that this node
// is aware of.
BestHeight func() (uint32, error)
// AddPolicyBuffer is a function that can be used to alter the policy
// values of the given channel edge. The main reason for doing this is
// to add a safety buffer so that if the node makes small policy changes
// during the lifetime of the blinded path, then the path remains valid
// and so probing is more difficult. Note that this will only be called
// for the policies of real nodes and won't be applied to
// DefaultDummyHopPolicy.
AddPolicyBuffer func(policy *BlindedHopPolicy) (*BlindedHopPolicy,
error)
// PathID is the secret data to embed in the blinded path data that we
// will receive back as the recipient. This is the equivalent of the
// payment address used in normal payments. It lets the recipient check
// that the path is being used in the correct context.
PathID []byte
// ValueMsat is the payment amount in milli-satoshis that must be
// routed. This will be used for selecting appropriate routes to use for
// the blinded path.
ValueMsat lnwire.MilliSatoshi
// MinFinalCLTVExpiryDelta is the minimum CLTV delta that the recipient
// requires for the final hop of the payment.
//
// NOTE that the caller is responsible for adding additional block
// padding to this value to account for blocks being mined while the
// payment is in-flight.
MinFinalCLTVExpiryDelta uint32
// BlocksUntilExpiry is the number of blocks that this blinded path
// should remain valid for.
BlocksUntilExpiry uint32
// MinNumHops is the minimum number of hops that each blinded path
// should be. If the number of hops in a path returned by FindRoutes is
// less than this number, then dummy hops will be post-fixed to the
// route.
MinNumHops uint8
// DefaultDummyHopPolicy holds the policy values that should be used for
// dummy hops in the cases where it cannot be derived via other means
// such as averaging the policy values of other hops on the path. This
// would happen in the case where the introduction node is also the
// introduction node. If these default policy values are used, then
// the MaxHTLCMsat value must be carefully chosen.
DefaultDummyHopPolicy *BlindedHopPolicy
}
// BuildBlindedPaymentPaths uses the passed config to construct a set of blinded
// payment paths that can be added to the invoice.
func BuildBlindedPaymentPaths(cfg *BuildBlindedPathCfg) (
[]*zpay32.BlindedPaymentPath, error) {
if cfg.MinFinalCLTVExpiryDelta >= cfg.BlocksUntilExpiry {
return nil, fmt.Errorf("blinded path CLTV expiry delta (%d) "+
"must be greater than the minimum final CLTV expiry "+
"delta (%d)", cfg.BlocksUntilExpiry,
cfg.MinFinalCLTVExpiryDelta)
}
// Find some appropriate routes for the value to be routed. This will
// return a set of routes made up of real nodes.
routes, err := cfg.FindRoutes(cfg.ValueMsat)
if err != nil {
return nil, err
}
if len(routes) == 0 {
return nil, fmt.Errorf("could not find any routes to self to " +
"use for blinded route construction")
}
// Not every route returned will necessarily result in a usable blinded
// path and so the number of paths returned might be less than the
// number of real routes returned by FindRoutes above.
paths := make([]*zpay32.BlindedPaymentPath, 0, len(routes))
// For each route returned, we will construct the associated blinded
// payment path.
for _, route := range routes {
path, err := buildBlindedPaymentPath(
cfg, extractCandidatePath(route),
)
if errors.Is(err, errInvalidBlindedPath) {
log.Debugf("Not using route (%s) as a blinded path "+
"since it resulted in an invalid blinded path",
route)
continue
}
if err != nil {
return nil, err
}
paths = append(paths, path)
}
if len(paths) == 0 {
return nil, fmt.Errorf("could not build any blinded paths")
}
return paths, nil
}
// buildBlindedPaymentPath takes a route from an introduction node to this node
// and uses the given config to convert it into a blinded payment path.
func buildBlindedPaymentPath(cfg *BuildBlindedPathCfg, path *candidatePath) (
*zpay32.BlindedPaymentPath, error) {
// Pad the given route with dummy hops until the minimum number of hops
// is met.
err := path.padWithDummyHops(cfg.MinNumHops)
if err != nil {
return nil, err
}
hops, minHTLC, maxHTLC, err := collectRelayInfo(cfg, path)
if err != nil {
return nil, fmt.Errorf("could not collect blinded path relay "+
"info: %w", err)
}
relayInfo := make([]*record.PaymentRelayInfo, len(hops))
for i, hop := range hops {
relayInfo[i] = hop.relayInfo
}
// Using the collected relay info, we can calculate the aggregated
// policy values for the route.
baseFee, feeRate, cltvDelta := calcBlindedPathPolicies(
relayInfo, uint16(cfg.MinFinalCLTVExpiryDelta),
)
currentHeight, err := cfg.BestHeight()
if err != nil {
return nil, err
}
// The next step is to calculate the payment constraints to communicate
// to each hop and to package up the hop info for each hop. We will
// handle the final hop first since its payload looks a bit different,
// and then we will iterate backwards through the remaining hops.
//
// Note that the +1 here is required because the route won't have the
// introduction node included in the "Hops". But since we want to create
// payloads for all the hops as well as the introduction node, we add 1
// here to get the full hop length along with the introduction node.
hopDataSet := make([]*hopData, 0, len(path.hops)+1)
// Determine the maximum CLTV expiry for the destination node.
cltvExpiry := currentHeight + cfg.BlocksUntilExpiry +
cfg.MinFinalCLTVExpiryDelta
constraints := &record.PaymentConstraints{
MaxCltvExpiry: cltvExpiry,
HtlcMinimumMsat: minHTLC,
}
// If the blinded route has only a source node (introduction node) and
// no hops, then the destination node is also the source node.
finalHopPubKey := path.introNode
if len(path.hops) > 0 {
finalHopPubKey = path.hops[len(path.hops)-1].pubKey
}
// For the final hop, we only send it the path ID and payment
// constraints.
info, err := buildFinalHopRouteData(
finalHopPubKey, cfg.PathID, constraints,
)
if err != nil {
return nil, err
}
hopDataSet = append(hopDataSet, info)
// Iterate through the remaining (non-final) hops, back to front.
for i := len(hops) - 1; i >= 0; i-- {
hop := hops[i]
cltvExpiry += uint32(hop.relayInfo.CltvExpiryDelta)
constraints = &record.PaymentConstraints{
MaxCltvExpiry: cltvExpiry,
HtlcMinimumMsat: minHTLC,
}
var info *hopData
if hop.nextHopIsDummy {
info, err = buildDummyRouteData(
hop.hopPubKey, hop.relayInfo, constraints,
)
} else {
info, err = buildHopRouteData(
hop.hopPubKey, hop.nextSCID, hop.relayInfo,
constraints,
)
}
if err != nil {
return nil, err
}
hopDataSet = append(hopDataSet, info)
}
// Sort the hop info list in reverse order so that the data for the
// introduction node is first.
sort.Slice(hopDataSet, func(i, j int) bool {
return j < i
})
// Add padding to each route data instance until the encrypted data
// blobs are all the same size.
paymentPath, _, err := padHopInfo(
hopDataSet, true, record.AverageDummyHopPayloadSize,
)
if err != nil {
return nil, err
}
// Derive an ephemeral session key.
sessionKey, err := btcec.NewPrivateKey()
if err != nil {
return nil, err
}
// Encrypt the hop info.
blindedPath, err := sphinx.BuildBlindedPath(sessionKey, paymentPath)
if err != nil {
return nil, err
}
if len(blindedPath.BlindedHops) < 1 {
return nil, fmt.Errorf("blinded path must have at least one " +
"hop")
}
// Overwrite the introduction point's blinded pub key with the real
// pub key since then we can use this more compact format in the
// invoice without needing to encode the un-used blinded node pub key of
// the intro node.
blindedPath.BlindedHops[0].BlindedNodePub =
blindedPath.IntroductionPoint
// Now construct a z32 blinded path.
return &zpay32.BlindedPaymentPath{
FeeBaseMsat: uint32(baseFee),
FeeRate: feeRate,
CltvExpiryDelta: cltvDelta,
HTLCMinMsat: uint64(minHTLC),
HTLCMaxMsat: uint64(maxHTLC),
Features: lnwire.EmptyFeatureVector(),
FirstEphemeralBlindingPoint: blindedPath.BlindingPoint,
Hops: blindedPath.BlindedHops,
}, nil
}
// hopRelayInfo packages together the relay info to send to hop on a blinded
// path along with the pub key of that hop and the SCID that the hop should
// forward the payment on to.
type hopRelayInfo struct {
hopPubKey route.Vertex
nextSCID lnwire.ShortChannelID
relayInfo *record.PaymentRelayInfo
nextHopIsDummy bool
}
// collectRelayInfo collects the relay policy rules for each relay hop on the
// route and applies any policy buffers.
//
// For the blinded route:
//
// C --chan(CB)--> B --chan(BA)--> A
//
// where C is the introduction node, the route.Route struct we are given will
// have SourcePubKey set to C's pub key, and then it will have the following
// route.Hops:
//
// - PubKeyBytes: B, ChannelID: chan(CB)
// - PubKeyBytes: A, ChannelID: chan(BA)
//
// We, however, want to collect the channel policies for the following PubKey
// and ChannelID pairs:
//
// - PubKey: C, ChannelID: chan(CB)
// - PubKey: B, ChannelID: chan(BA)
//
// Therefore, when we go through the route and its hops to collect policies, our
// index for collecting public keys will be trailing that of the channel IDs by
// 1.
//
// For any dummy hops on the route, this function also decides what to use as
// policy values for the dummy hops. If there are other real hops, then the
// dummy hop policy values are derived by taking the average of the real
// policy values. If there are no real hops (in other words we are the
// introduction node), then we use some default routing values and we use the
// average of our channel capacities for the MaxHTLC value.
func collectRelayInfo(cfg *BuildBlindedPathCfg, path *candidatePath) (
[]*hopRelayInfo, lnwire.MilliSatoshi, lnwire.MilliSatoshi, error) {
var (
// The first pub key is that of the introduction node.
hopSource = path.introNode
// A collection of the policy values of real hops on the path.
policies = make(map[uint64]*BlindedHopPolicy)
hasDummyHops bool
)
// On this first iteration, we just collect policy values of the real
// hops on the path.
for _, hop := range path.hops {
// Once we have hit a dummy hop, all hops after will be dummy
// hops too.
if hop.isDummy {
hasDummyHops = true
break
}
// For real hops, retrieve the channel policy for this hop's
// channel ID in the direction pointing away from the hopSource
// node.
policy, err := getNodeChannelPolicy(
cfg, hop.channelID, hopSource,
)
if err != nil {
return nil, 0, 0, err
}
policies[hop.channelID] = policy
// This hop's pub key will be the policy creator for the next
// hop.
hopSource = hop.pubKey
}
var (
dummyHopPolicy *BlindedHopPolicy
err error
)
// If the path does have dummy hops, we need to decide which policy
// values to use for these hops.
if hasDummyHops {
dummyHopPolicy, err = computeDummyHopPolicy(
cfg.DefaultDummyHopPolicy, cfg.FetchOurOpenChannels,
policies,
)
if err != nil {
return nil, 0, 0, err
}
}
// We iterate through the hops one more time. This time it is to
// buffer the policy values, collect the payment relay info to send to
// each hop, and to compute the min and max HTLC values for the path.
var (
hops = make([]*hopRelayInfo, 0, len(path.hops))
minHTLC lnwire.MilliSatoshi
maxHTLC lnwire.MilliSatoshi
)
// The first pub key is that of the introduction node.
hopSource = path.introNode
for _, hop := range path.hops {
var (
policy = dummyHopPolicy
ok bool
err error
)
if !hop.isDummy {
policy, ok = policies[hop.channelID]
if !ok {
return nil, 0, 0, fmt.Errorf("no cached "+
"policy found for channel ID: %d",
hop.channelID)
}
}
policy, err = cfg.AddPolicyBuffer(policy)
if err != nil {
return nil, 0, 0, err
}
// If this is the first policy we are collecting, then use this
// policy to set the base values for min/max htlc.
if len(hops) == 0 {
minHTLC = policy.MinHTLCMsat
maxHTLC = policy.MaxHTLCMsat
} else {
if policy.MinHTLCMsat > minHTLC {
minHTLC = policy.MinHTLCMsat
}
if policy.MaxHTLCMsat < maxHTLC {
maxHTLC = policy.MaxHTLCMsat
}
}
// From the policy values for this hop, we can collect the
// payment relay info that we will send to this hop.
hops = append(hops, &hopRelayInfo{
hopPubKey: hopSource,
nextSCID: lnwire.NewShortChanIDFromInt(hop.channelID),
relayInfo: &record.PaymentRelayInfo{
FeeRate: policy.FeeRate,
BaseFee: policy.BaseFee,
CltvExpiryDelta: policy.CLTVExpiryDelta,
},
nextHopIsDummy: hop.isDummy,
})
// This hop's pub key will be the policy creator for the next
// hop.
hopSource = hop.pubKey
}
// It can happen that there is no HTLC-range overlap between the various
// hops along the path. We return errInvalidBlindedPath to indicate that
// this route was not usable
if minHTLC > maxHTLC {
return nil, 0, 0, fmt.Errorf("%w: resulting blinded path min "+
"HTLC value is larger than the resulting max HTLC "+
"value", errInvalidBlindedPath)
}
return hops, minHTLC, maxHTLC, nil
}
// buildDummyRouteData constructs the record.BlindedRouteData struct for the
// given a hop in a blinded route where the following hop is a dummy hop.
func buildDummyRouteData(node route.Vertex, relayInfo *record.PaymentRelayInfo,
constraints *record.PaymentConstraints) (*hopData, error) {
nodeID, err := btcec.ParsePubKey(node[:])
if err != nil {
return nil, err
}
return &hopData{
data: record.NewDummyHopRouteData(
nodeID, *relayInfo, *constraints,
),
nodeID: nodeID,
}, nil
}
// computeDummyHopPolicy determines policy values to use for a dummy hop on a
// blinded path. If other real policy values exist, then we use the average of
// those values for the dummy hop policy values. Otherwise, in the case were
// there are no real policy values due to this node being the introduction node,
// we use the provided default policy values, and we get the average capacity of
// this node's channels to compute a MaxHTLC value.
func computeDummyHopPolicy(defaultPolicy *BlindedHopPolicy,
fetchOurChannels func() ([]*channeldb.OpenChannel, error),
policies map[uint64]*BlindedHopPolicy) (*BlindedHopPolicy, error) {
numPolicies := len(policies)
// If there are no real policies to calculate an average policy from,
// then we use the default. The only thing we need to calculate here
// though is the MaxHTLC value.
if numPolicies == 0 {
chans, err := fetchOurChannels()
if err != nil {
return nil, err
}
if len(chans) == 0 {
return nil, fmt.Errorf("node has no channels to " +
"receive on")
}
// Calculate the average channel capacity and use this as the
// MaxHTLC value.
var maxHTLC btcutil.Amount
for _, c := range chans {
maxHTLC += c.Capacity
}
maxHTLC = btcutil.Amount(float64(maxHTLC) / float64(len(chans)))
return &BlindedHopPolicy{
CLTVExpiryDelta: defaultPolicy.CLTVExpiryDelta,
FeeRate: defaultPolicy.FeeRate,
BaseFee: defaultPolicy.BaseFee,
MinHTLCMsat: defaultPolicy.MinHTLCMsat,
MaxHTLCMsat: lnwire.NewMSatFromSatoshis(maxHTLC),
}, nil
}
var avgPolicy BlindedHopPolicy
for _, policy := range policies {
avgPolicy.MinHTLCMsat += policy.MinHTLCMsat
avgPolicy.MaxHTLCMsat += policy.MaxHTLCMsat
avgPolicy.BaseFee += policy.BaseFee
avgPolicy.FeeRate += policy.FeeRate
avgPolicy.CLTVExpiryDelta += policy.CLTVExpiryDelta
}
avgPolicy.MinHTLCMsat = lnwire.MilliSatoshi(
float64(avgPolicy.MinHTLCMsat) / float64(numPolicies),
)
avgPolicy.MaxHTLCMsat = lnwire.MilliSatoshi(
float64(avgPolicy.MaxHTLCMsat) / float64(numPolicies),
)
avgPolicy.BaseFee = lnwire.MilliSatoshi(
float64(avgPolicy.BaseFee) / float64(numPolicies),
)
avgPolicy.FeeRate = uint32(
float64(avgPolicy.FeeRate) / float64(numPolicies),
)
avgPolicy.CLTVExpiryDelta = uint16(
float64(avgPolicy.CLTVExpiryDelta) / float64(numPolicies),
)
return &avgPolicy, nil
}
// buildHopRouteData constructs the record.BlindedRouteData struct for the given
// non-final hop on a blinded path and packages it with the node's ID.
func buildHopRouteData(node route.Vertex, scid lnwire.ShortChannelID,
relayInfo *record.PaymentRelayInfo,
constraints *record.PaymentConstraints) (*hopData, error) {
// Wrap up the data we want to send to this hop.
blindedRouteHopData := record.NewNonFinalBlindedRouteData(
scid, nil, *relayInfo, constraints, nil,
)
nodeID, err := btcec.ParsePubKey(node[:])
if err != nil {
return nil, err
}
return &hopData{
data: blindedRouteHopData,
nodeID: nodeID,
}, nil
}
// buildFinalHopRouteData constructs the record.BlindedRouteData struct for the
// final hop and packages it with the real node ID of the node it is intended
// for.
func buildFinalHopRouteData(node route.Vertex, pathID []byte,
constraints *record.PaymentConstraints) (*hopData, error) {
blindedRouteHopData := record.NewFinalHopBlindedRouteData(
constraints, pathID,
)
nodeID, err := btcec.ParsePubKey(node[:])
if err != nil {
return nil, err
}
return &hopData{
data: blindedRouteHopData,
nodeID: nodeID,
}, nil
}
// getNodeChanPolicy fetches the routing policy info for the given channel and
// node pair.
func getNodeChannelPolicy(cfg *BuildBlindedPathCfg, chanID uint64,
nodeID route.Vertex) (*BlindedHopPolicy, error) {
// Attempt to fetch channel updates for the given channel. We will have
// at most two updates for a given channel.
_, update1, update2, err := cfg.FetchChannelEdgesByID(chanID)
if err != nil {
return nil, err
}
// Now we need to determine which of the updates was created by the
// node in question. We know the update is the correct one if the
// "ToNode" for the fetched policy is _not_ equal to the node ID in
// question.
var policy *models.ChannelEdgePolicy
switch {
case update1 != nil && !bytes.Equal(update1.ToNode[:], nodeID[:]):
policy = update1
case update2 != nil && !bytes.Equal(update2.ToNode[:], nodeID[:]):
policy = update2
default:
return nil, fmt.Errorf("no channel updates found from node "+
"%s for channel %d", nodeID, chanID)
}
return &BlindedHopPolicy{
CLTVExpiryDelta: policy.TimeLockDelta,
FeeRate: uint32(policy.FeeProportionalMillionths),
BaseFee: policy.FeeBaseMSat,
MinHTLCMsat: policy.MinHTLC,
MaxHTLCMsat: policy.MaxHTLC,
}, nil
}
// candidatePath holds all the information about a route to this node that we
// need in order to build a blinded route.
type candidatePath struct {
introNode route.Vertex
finalNodeID route.Vertex
hops []*blindedPathHop
}
// padWithDummyHops will append n dummy hops to the candidatePath hop set. The
// pub key for the dummy hop will be the same as the pub key for the final hop
// of the path. That way, the final hop will be able to decrypt the data
// encrypted for each dummy hop.
func (c *candidatePath) padWithDummyHops(n uint8) error {
for len(c.hops) < int(n) {
c.hops = append(c.hops, &blindedPathHop{
pubKey: c.finalNodeID,
isDummy: true,
})
}
return nil
}
// blindedPathHop holds the information we need to know about a hop in a route
// in order to use it in the construction of a blinded path.
type blindedPathHop struct {
// pubKey is the real pub key of a node on a blinded path.
pubKey route.Vertex
// channelID is the channel along which the previous hop should forward
// their HTLC in order to reach this hop.
channelID uint64
// isDummy is true if this hop is an appended dummy hop.
isDummy bool
}
// extractCandidatePath extracts the data it needs from the given route.Route in
// order to construct a candidatePath.
func extractCandidatePath(path *route.Route) *candidatePath {
var (
hops = make([]*blindedPathHop, len(path.Hops))
finalNode = path.SourcePubKey
)
for i, hop := range path.Hops {
hops[i] = &blindedPathHop{
pubKey: hop.PubKeyBytes,
channelID: hop.ChannelID,
}
if i == len(path.Hops)-1 {
finalNode = hop.PubKeyBytes
}
}
return &candidatePath{
introNode: path.SourcePubKey,
finalNodeID: finalNode,
hops: hops,
}
}
// BlindedHopPolicy holds the set of relay policy values to use for a channel
// in a blinded path.
type BlindedHopPolicy struct {
CLTVExpiryDelta uint16
FeeRate uint32
BaseFee lnwire.MilliSatoshi
MinHTLCMsat lnwire.MilliSatoshi
MaxHTLCMsat lnwire.MilliSatoshi
}
// AddPolicyBuffer constructs the bufferedChanPolicies for a path hop by taking
// its actual policy values and multiplying them by the given multipliers.
// The base fee, fee rate and minimum HTLC msat values are adjusted via the
// incMultiplier while the maximum HTLC msat value is adjusted via the
// decMultiplier. If adjustments of the HTLC values no longer make sense
// then the original HTLC value is used.
func AddPolicyBuffer(policy *BlindedHopPolicy, incMultiplier,
decMultiplier float64) (*BlindedHopPolicy, error) {
if incMultiplier < 1 {
return nil, fmt.Errorf("blinded path policy increase " +
"multiplier must be greater than or equal to 1")
}
if decMultiplier < 0 || decMultiplier > 1 {
return nil, fmt.Errorf("blinded path policy decrease " +
"multiplier must be in the range [0;1]")
}
var (
minHTLCMsat = lnwire.MilliSatoshi(
float64(policy.MinHTLCMsat) * incMultiplier,
)
maxHTLCMsat = lnwire.MilliSatoshi(
float64(policy.MaxHTLCMsat) * decMultiplier,
)
)
// Make sure the new minimum is not more than the original maximum.
// If it is, then just stick to the original minimum.
if minHTLCMsat > policy.MaxHTLCMsat {
minHTLCMsat = policy.MinHTLCMsat
}
// Make sure the new maximum is not less than the original minimum.
// If it is, then just stick to the original maximum.
if maxHTLCMsat < policy.MinHTLCMsat {
maxHTLCMsat = policy.MaxHTLCMsat
}
// Also ensure that the new htlc bounds make sense. If the new minimum
// is greater than the new maximum, then just let both to their original
// values.
if minHTLCMsat > maxHTLCMsat {
minHTLCMsat = policy.MinHTLCMsat
maxHTLCMsat = policy.MaxHTLCMsat
}
return &BlindedHopPolicy{
CLTVExpiryDelta: uint16(
float64(policy.CLTVExpiryDelta) * incMultiplier,
),
FeeRate: uint32(
float64(policy.FeeRate) * incMultiplier,
),
BaseFee: lnwire.MilliSatoshi(
float64(policy.BaseFee) * incMultiplier,
),
MinHTLCMsat: minHTLCMsat,
MaxHTLCMsat: maxHTLCMsat,
}, nil
}
// calcBlindedPathPolicies computes the accumulated policy values for the path.
// These values include the total base fee, the total proportional fee and the
// total CLTV delta. This function assumes that all the passed relay infos have
// already been adjusted with a buffer to account for easy probing attacks.
func calcBlindedPathPolicies(relayInfo []*record.PaymentRelayInfo,
ourMinFinalCLTVDelta uint16) (lnwire.MilliSatoshi, uint32, uint16) {
var (
totalFeeBase lnwire.MilliSatoshi
totalFeeProp uint32
totalCLTV = ourMinFinalCLTVDelta
)
// Use the algorithms defined in BOLT 4 to calculate the accumulated
// relay fees for the route:
//nolint:lll
// https://github.com/lightning/bolts/blob/db278ab9b2baa0b30cfe79fb3de39280595938d3/04-onion-routing.md?plain=1#L255
for i := len(relayInfo) - 1; i >= 0; i-- {
info := relayInfo[i]
totalFeeBase = calcNextTotalBaseFee(
totalFeeBase, info.BaseFee, info.FeeRate,
)
totalFeeProp = calcNextTotalFeeRate(totalFeeProp, info.FeeRate)
totalCLTV += info.CltvExpiryDelta
}
return totalFeeBase, totalFeeProp, totalCLTV
}
// calcNextTotalBaseFee takes the current total accumulated base fee of a
// blinded path at hop `n` along with the fee rate and base fee of the hop at
// `n+1` and uses these to calculate the accumulated base fee at hop `n+1`.
func calcNextTotalBaseFee(currentTotal, hopBaseFee lnwire.MilliSatoshi,
hopFeeRate uint32) lnwire.MilliSatoshi {
numerator := (uint32(hopBaseFee) * oneMillion) +
(uint32(currentTotal) * (oneMillion + hopFeeRate)) +
oneMillion - 1
return lnwire.MilliSatoshi(numerator / oneMillion)
}
// calculateNextTotalFeeRate takes the current total accumulated fee rate of a
// blinded path at hop `n` along with the fee rate of the hop at `n+1` and uses
// these to calculate the accumulated fee rate at hop `n+1`.
func calcNextTotalFeeRate(currentTotal, hopFeeRate uint32) uint32 {
numerator := (currentTotal+hopFeeRate)*oneMillion +
currentTotal*hopFeeRate + oneMillion - 1
return numerator / oneMillion
}
// hopData packages the record.BlindedRouteData for a hop on a blinded path with
// the real node ID of that hop.
type hopData struct {
data *record.BlindedRouteData
nodeID *btcec.PublicKey
}
// padStats can be used to keep track of various pieces of data that we collect
// during a call to padHopInfo. This is useful for logging and for test
// assertions.
type padStats struct {
minPayloadSize int
maxPayloadSize int
finalPaddedSize int
numIterations int
}
// padHopInfo iterates over a set of record.BlindedRouteData and adds padding
// where needed until the resulting encrypted data blobs are all the same size.
// This may take a few iterations due to the fact that a TLV field is used to
// add this padding. For example, if we want to add a 1 byte padding to a
// record.BlindedRouteData when it does not yet have any padding, then adding
// a 1 byte padding will actually add 3 bytes due to the bytes required when
// adding the initial type and length bytes. However, on the next iteration if
// we again add just 1 byte, then only a single byte will be added. The same
// iteration is required for padding values on the BigSize encoding bucket
// edges. The number of iterations that this function takes is also returned for
// testing purposes. If prePad is true, then zero byte padding is added to each
// payload that does not yet have padding. This will save some iterations for
// the majority of cases. minSize can be used to specify a minimum size that all
// payloads should be.
func padHopInfo(hopInfo []*hopData, prePad bool, minSize int) (
[]*sphinx.HopInfo, *padStats, error) {
var (
paymentPath = make([]*sphinx.HopInfo, len(hopInfo))
stats padStats
)
// Pre-pad each payload with zero byte padding (if it does not yet have
// padding) to save a couple of iterations in the majority of cases.
if prePad {
for _, info := range hopInfo {
if info.data.Padding.IsSome() {
continue
}
info.data.PadBy(0)
}
}
for {
stats.numIterations++
// On each iteration of the loop, we first determine the
// current largest encoded data blob size. This will be the
// size we aim to get the others to match.
var (
maxLen = minSize
minLen = math.MaxInt8
)
for i, hop := range hopInfo {
plainText, err := record.EncodeBlindedRouteData(
hop.data,
)
if err != nil {
return nil, nil, err
}
if len(plainText) > maxLen {
maxLen = len(plainText)
// Update the stats to take note of this new
// max since this may be the final max that all
// payloads will be padded to.
stats.finalPaddedSize = maxLen
}
if len(plainText) < minLen {
minLen = len(plainText)
}
paymentPath[i] = &sphinx.HopInfo{
NodePub: hop.nodeID,
PlainText: plainText,
}
}
// If this is our first iteration, then we take note of the min
// and max lengths of the payloads pre-padding for logging
// later.
if stats.numIterations == 1 {
stats.minPayloadSize = minLen
stats.maxPayloadSize = maxLen
}
// Now we iterate over them again and determine which ones we
// need to add padding to.
var numEqual int
for i, hop := range hopInfo {
plainText := paymentPath[i].PlainText
// If the plaintext length is equal to the desired
// length, then we can continue. We use numEqual to
// keep track of how many have the same length.
if len(plainText) == maxLen {
numEqual++
continue
}
// If we previously added padding to this hop, we keep
// the length of that initial padding too.
var existingPadding int
hop.data.Padding.WhenSome(
func(p tlv.RecordT[tlv.TlvType1, []byte]) {
existingPadding = len(p.Val)
},
)
// Add some padding bytes to the hop.
hop.data.PadBy(
existingPadding + maxLen - len(plainText),
)
}
// If all the payloads have the same length, we can exit the
// loop.
if numEqual == len(hopInfo) {
break
}
}
log.Debugf("Finished padding %d blinded path payloads to %d bytes "+
"each where the pre-padded min and max sizes were %d and %d "+
"bytes respectively", len(hopInfo), stats.finalPaddedSize,
stats.minPayloadSize, stats.maxPayloadSize)
return paymentPath, &stats, nil
}

View file

@ -0,0 +1,992 @@
package blindedpath
import (
"bytes"
"encoding/hex"
"fmt"
"math/rand"
"reflect"
"testing"
"testing/quick"
"github.com/btcsuite/btcd/btcec/v2"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/tlv"
"github.com/stretchr/testify/require"
)
var (
pubkeyBytes, _ = hex.DecodeString(
"598ec453728e0ffe0ae2f5e174243cf58f2" +
"a3f2c83d2457b43036db568b11093",
)
pubKeyY = new(btcec.FieldVal)
_ = pubKeyY.SetByteSlice(pubkeyBytes)
pubkey = btcec.NewPublicKey(new(btcec.FieldVal).SetInt(4), pubKeyY)
)
// TestApplyBlindedPathPolicyBuffer tests blinded policy adjustments.
func TestApplyBlindedPathPolicyBuffer(t *testing.T) {
tests := []struct {
name string
policyIn *BlindedHopPolicy
expectedOut *BlindedHopPolicy
incMultiplier float64
decMultiplier float64
expectedError string
}{
{
name: "invalid increase multiplier",
incMultiplier: 0,
expectedError: "blinded path policy increase " +
"multiplier must be greater than or equal to 1",
},
{
name: "decrease multiplier too small",
incMultiplier: 1,
decMultiplier: -1,
expectedError: "blinded path policy decrease " +
"multiplier must be in the range [0;1]",
},
{
name: "decrease multiplier too big",
incMultiplier: 1,
decMultiplier: 2,
expectedError: "blinded path policy decrease " +
"multiplier must be in the range [0;1]",
},
{
name: "no change",
incMultiplier: 1,
decMultiplier: 1,
policyIn: &BlindedHopPolicy{
CLTVExpiryDelta: 1,
MinHTLCMsat: 2,
MaxHTLCMsat: 3,
BaseFee: 4,
FeeRate: 5,
},
expectedOut: &BlindedHopPolicy{
CLTVExpiryDelta: 1,
MinHTLCMsat: 2,
MaxHTLCMsat: 3,
BaseFee: 4,
FeeRate: 5,
},
},
{
name: "buffer up by 100% and down by 50%",
incMultiplier: 2,
decMultiplier: 0.5,
policyIn: &BlindedHopPolicy{
CLTVExpiryDelta: 10,
MinHTLCMsat: 20,
MaxHTLCMsat: 300,
BaseFee: 40,
FeeRate: 50,
},
expectedOut: &BlindedHopPolicy{
CLTVExpiryDelta: 20,
MinHTLCMsat: 40,
MaxHTLCMsat: 150,
BaseFee: 80,
FeeRate: 100,
},
},
{
name: "new HTLC minimum larger than OG " +
"maximum",
incMultiplier: 2,
decMultiplier: 1,
policyIn: &BlindedHopPolicy{
CLTVExpiryDelta: 10,
MinHTLCMsat: 20,
MaxHTLCMsat: 30,
BaseFee: 40,
FeeRate: 50,
},
expectedOut: &BlindedHopPolicy{
CLTVExpiryDelta: 20,
MinHTLCMsat: 20,
MaxHTLCMsat: 30,
BaseFee: 80,
FeeRate: 100,
},
},
{
name: "new HTLC maximum smaller than OG " +
"minimum",
incMultiplier: 1,
decMultiplier: 0.5,
policyIn: &BlindedHopPolicy{
CLTVExpiryDelta: 10,
MinHTLCMsat: 20,
MaxHTLCMsat: 30,
BaseFee: 40,
FeeRate: 50,
},
expectedOut: &BlindedHopPolicy{
CLTVExpiryDelta: 10,
MinHTLCMsat: 20,
MaxHTLCMsat: 30,
BaseFee: 40,
FeeRate: 50,
},
},
{
name: "new HTLC minimum and maximums are not " +
"compatible",
incMultiplier: 2,
decMultiplier: 0.5,
policyIn: &BlindedHopPolicy{
CLTVExpiryDelta: 10,
MinHTLCMsat: 30,
MaxHTLCMsat: 100,
BaseFee: 40,
FeeRate: 50,
},
expectedOut: &BlindedHopPolicy{
CLTVExpiryDelta: 20,
MinHTLCMsat: 30,
MaxHTLCMsat: 100,
BaseFee: 80,
FeeRate: 100,
},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
bufferedPolicy, err := AddPolicyBuffer(
test.policyIn, test.incMultiplier,
test.decMultiplier,
)
if test.expectedError != "" {
require.ErrorContains(
t, err, test.expectedError,
)
return
}
require.Equal(t, test.expectedOut, bufferedPolicy)
})
}
}
// TestBlindedPathAccumulatedPolicyCalc tests the logic for calculating the
// accumulated routing policies of a blinded route against an example mentioned
// in the spec document:
// https://github.com/lightning/bolts/blob/master/proposals/route-blinding.md
func TestBlindedPathAccumulatedPolicyCalc(t *testing.T) {
t.Parallel()
// In the spec example, the blinded route is:
// Carol -> Bob -> Alice
// And Alice chooses the following buffered policy for both the C->B
// and B->A edges.
nodePolicy := &record.PaymentRelayInfo{
FeeRate: 500,
BaseFee: 100,
CltvExpiryDelta: 144,
}
hopPolicies := []*record.PaymentRelayInfo{
nodePolicy,
nodePolicy,
}
// Alice's minimum final expiry delta is chosen to be 12.
aliceMinFinalExpDelta := uint16(12)
totalBase, totalRate, totalCLTVDelta := calcBlindedPathPolicies(
hopPolicies, aliceMinFinalExpDelta,
)
require.Equal(t, lnwire.MilliSatoshi(201), totalBase)
require.EqualValues(t, 1001, totalRate)
require.EqualValues(t, 300, totalCLTVDelta)
}
// TestPadBlindedHopInfo asserts that the padding of blinded hop data is done
// correctly and that it takes the expected number of iterations.
func TestPadBlindedHopInfo(t *testing.T) {
tests := []struct {
name string
expectedIterations int
expectedFinalSize int
// We will use the PathID field of BlindedRouteData to set an
// initial payload size. The ints in this list represent the
// size of each PathID.
pathIDs []int
// existingPadding is a map from entry index (based on the
// pathIDs set) to the number of pre-existing padding bytes to
// add.
existingPadding map[int]int
// prePad is true if all the hop payloads should be pre-padded
// with a zero length TLV Padding field.
prePad bool
// minPayloadSize can be used to set the minimum number of bytes
// that the resulting records should be.
minPayloadSize int
}{
{
// If there is only one entry, then no padding is
// expected.
name: "single entry",
expectedIterations: 1,
pathIDs: []int{10},
// The final size will be 12 since the path ID is 10
// bytes, and it will be prefixed by type and value
// bytes.
expectedFinalSize: 12,
},
{
// Same as the above example but with a minimum final
// size specified.
name: "single entry with min size",
expectedIterations: 2,
pathIDs: []int{10},
minPayloadSize: 500,
expectedFinalSize: 504,
},
{
// All the payloads are the same size from the get go
// meaning that no padding is expected.
name: "all start equal",
expectedIterations: 1,
pathIDs: []int{10, 10, 10},
// The final size will be 12 since the path ID is 10
// bytes, and it will be prefixed by type and value
// bytes.
expectedFinalSize: 12,
},
{
// If the blobs differ by 1 byte it will take 4
// iterations:
// 1) padding of 1 is added to entry 2 which will
// increase its size by 3 bytes since padding does
// not yet exist for it.
// 2) Now entry 1 will be short 2 bytes. It will be
// padded by 2 bytes but again since it is a new
// padding field, 4 bytes are added.
// 3) Finally, entry 2 is padded by 1 extra. Since it
// already does have a padding field, this does end
// up adding only 1 extra byte.
// 4) The fourth iteration determines that all are now
// the same size.
name: "differ by 1 - no pre-padding",
expectedIterations: 4,
pathIDs: []int{4, 3},
expectedFinalSize: 10,
},
{
// By pre-padding the payloads with a zero byte padding,
// we can reduce the number of iterations quite a bit.
name: "differ by 1 - with pre-padding",
expectedIterations: 2,
pathIDs: []int{4, 3},
expectedFinalSize: 8,
prePad: true,
},
{
name: "existing padding and diff of 1",
expectedIterations: 2,
pathIDs: []int{10, 11},
// By adding some existing padding, the type and length
// field for the padding are already accounted for in
// the first iteration, and so we only expect two
// iterations to get the payloads to match size here:
// one for adding a single extra byte to the smaller
// payload and another for confirming the sizes match.
existingPadding: map[int]int{0: 1, 1: 1},
expectedFinalSize: 16,
},
{
// In this test, we test a BigSize bucket shift. We do
// this by setting the initial path ID's of both entries
// to a 0 size which means the total encoding of those
// will be 2 bytes (to encode the type and length). Then
// for the initial padding, we let the first entry be
// 253 bytes long which is just long enough to be in
// the second BigSize bucket which uses 3 bytes to
// encode the value length. We make the second entry
// 252 bytes which still puts it in the first bucket
// which uses 1 byte for the length. The difference in
// overall packet size will be 3 bytes (the first entry
// has 2 more length bytes and 1 more value byte). So
// the function will try to pad the second entry by 3
// bytes (iteration 1). This will however result in the
// second entry shifting to the second BigSize bucket
// meaning it will gain an additional 2 bytes for the
// new length encoding meaning that overall it gains 5
// bytes in size. This will result in another iteration
// which will result in padding the first entry with an
// extra 2 bytes to meet the second entry's new size
// (iteration 2). One more iteration (3) is then done
// to confirm that all entries are now the same size.
name: "big size bucket shift",
expectedIterations: 3,
// We make the path IDs large enough so that
pathIDs: []int{0, 0},
existingPadding: map[int]int{0: 253, 1: 252},
expectedFinalSize: 261,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
// If the test includes existing padding, then make sure
// that the number of existing padding entries is equal
// to the number of PathID entries.
if test.existingPadding != nil {
require.Len(t, test.existingPadding,
len(test.pathIDs))
}
hopDataSet := make([]*hopData, len(test.pathIDs))
for i, l := range test.pathIDs {
pathID := tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType6](
make([]byte, l),
),
)
data := &record.BlindedRouteData{
PathID: pathID,
}
if test.existingPadding != nil {
//nolint:lll
padding := tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType1](
make([]byte, test.existingPadding[i]),
),
)
data.Padding = padding
}
hopDataSet[i] = &hopData{data: data}
}
hopInfo, stats, err := padHopInfo(
hopDataSet, test.prePad, test.minPayloadSize,
)
require.NoError(t, err)
require.Equal(t, test.expectedIterations,
stats.numIterations)
require.Equal(t, test.expectedFinalSize,
stats.finalPaddedSize)
// We expect all resulting blobs to be the same size.
for _, info := range hopInfo {
require.Len(
t, info.PlainText,
test.expectedFinalSize,
)
}
})
}
}
// TestPadBlindedHopInfoBlackBox tests the padHopInfo function via the
// quick.Check testing function. It generates a random set of hopData and
// asserts that the resulting padded set always has the same encoded length.
func TestPadBlindedHopInfoBlackBox(t *testing.T) {
fn := func(data hopDataList) bool {
resultList, _, err := padHopInfo(data, true, 0)
require.NoError(t, err)
// There should be a resulting sphinx.HopInfo struct for each
// hopData passed to the padHopInfo function.
if len(resultList) != len(data) {
return false
}
// There is nothing left to check if input set was empty to
// start with.
if len(data) == 0 {
return true
}
// Now, assert that the encoded size of each item is the same.
// Get the size of the first item as a base point.
payloadSize := len(resultList[0].PlainText)
// All the other entries should have the same encoded size.
for i := 1; i < len(resultList); i++ {
if len(resultList[i].PlainText) != payloadSize {
return false
}
}
return true
}
require.NoError(t, quick.Check(fn, nil))
}
type hopDataList []*hopData
// Generate returns a random instance of the hopDataList type.
//
// NOTE: this is part of the quick.Generate interface.
func (h hopDataList) Generate(rand *rand.Rand, size int) reflect.Value {
data := make(hopDataList, rand.Intn(size))
for i := 0; i < len(data); i++ {
data[i] = &hopData{
data: genBlindedRouteData(rand),
nodeID: pubkey,
}
}
return reflect.ValueOf(data)
}
// A compile-time check to ensure that hopDataList implements the
// quick.Generator interface.
var _ quick.Generator = (*hopDataList)(nil)
// sometimesDo calls the given function with a 50% probability.
func sometimesDo(fn func(), rand *rand.Rand) {
if rand.Intn(1) == 0 {
return
}
fn()
}
// genBlindedRouteData generates a random record.BlindedRouteData object.
func genBlindedRouteData(rand *rand.Rand) *record.BlindedRouteData {
var data record.BlindedRouteData
sometimesDo(func() {
data.Padding = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType1](
make([]byte, rand.Intn(1000000)),
),
)
}, rand)
sometimesDo(func() {
data.ShortChannelID = tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType2](lnwire.ShortChannelID{
BlockHeight: rand.Uint32(),
TxIndex: rand.Uint32(),
TxPosition: uint16(rand.Uint32()),
}),
)
}, rand)
sometimesDo(func() {
data.NextNodeID = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType4](pubkey),
)
}, rand)
sometimesDo(func() {
data.PathID = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType6](
make([]byte, rand.Intn(100)),
),
)
}, rand)
sometimesDo(func() {
data.NextBlindingOverride = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType8](pubkey),
)
}, rand)
sometimesDo(func() {
data.RelayInfo = tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType10](record.PaymentRelayInfo{
CltvExpiryDelta: uint16(rand.Uint32()),
FeeRate: rand.Uint32(),
BaseFee: lnwire.MilliSatoshi(
rand.Uint32(),
),
}),
)
}, rand)
sometimesDo(func() {
data.Constraints = tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType12](record.PaymentConstraints{
MaxCltvExpiry: rand.Uint32(),
HtlcMinimumMsat: lnwire.MilliSatoshi(
rand.Uint32(),
),
}),
)
}, rand)
return &data
}
// TestBuildBlindedPath tests the logic for constructing a blinded path against
// an example mentioned in this spec document:
// https://github.com/lightning/bolts/blob/master/proposals/route-blinding.md
// This example does not use any dummy hops.
func TestBuildBlindedPath(t *testing.T) {
// Alice chooses the following path to herself for blinded path
// construction:
// Carol -> Bob -> Alice.
// Let's construct the corresponding route.Route for this which will be
// returned from the `FindRoutes` config callback.
var (
privC, pkC = btcec.PrivKeyFromBytes([]byte{1})
privB, pkB = btcec.PrivKeyFromBytes([]byte{2})
privA, pkA = btcec.PrivKeyFromBytes([]byte{3})
carol = route.NewVertex(pkC)
bob = route.NewVertex(pkB)
alice = route.NewVertex(pkA)
chanCB = uint64(1)
chanBA = uint64(2)
)
realRoute := &route.Route{
SourcePubKey: carol,
Hops: []*route.Hop{
{
PubKeyBytes: bob,
ChannelID: chanCB,
},
{
PubKeyBytes: alice,
ChannelID: chanBA,
},
},
}
realPolicies := map[uint64]*models.ChannelEdgePolicy{
chanCB: {
ChannelID: chanCB,
ToNode: bob,
},
chanBA: {
ChannelID: chanBA,
ToNode: alice,
},
}
paths, err := BuildBlindedPaymentPaths(&BuildBlindedPathCfg{
FindRoutes: func(_ lnwire.MilliSatoshi) ([]*route.Route,
error) {
return []*route.Route{realRoute}, nil
},
FetchChannelEdgesByID: func(chanID uint64) (
*models.ChannelEdgeInfo, *models.ChannelEdgePolicy,
*models.ChannelEdgePolicy, error) {
return nil, realPolicies[chanID], nil, nil
},
BestHeight: func() (uint32, error) {
return 1000, nil
},
// In the spec example, all the policies get replaced with
// the same static values.
AddPolicyBuffer: func(_ *BlindedHopPolicy) (
*BlindedHopPolicy, error) {
return &BlindedHopPolicy{
FeeRate: 500,
BaseFee: 100,
CLTVExpiryDelta: 144,
MinHTLCMsat: 1000,
MaxHTLCMsat: lnwire.MaxMilliSatoshi,
}, nil
},
PathID: []byte{1, 2, 3},
ValueMsat: 1000,
MinFinalCLTVExpiryDelta: 12,
BlocksUntilExpiry: 200,
})
require.NoError(t, err)
require.Len(t, paths, 1)
path := paths[0]
// Check that all the accumulated policy values are correct.
require.EqualValues(t, 201, path.FeeBaseMsat)
require.EqualValues(t, 1001, path.FeeRate)
require.EqualValues(t, 300, path.CltvExpiryDelta)
require.EqualValues(t, 1000, path.HTLCMinMsat)
require.EqualValues(t, lnwire.MaxMilliSatoshi, path.HTLCMaxMsat)
// Now we check the hops.
require.Len(t, path.Hops, 3)
// Assert that all the encrypted recipient blobs have been padded such
// that they are all the same size.
require.Len(t, path.Hops[0].CipherText, len(path.Hops[1].CipherText))
require.Len(t, path.Hops[1].CipherText, len(path.Hops[2].CipherText))
// The first hop, should have the real pub key of the introduction
// node: Carol.
hop := path.Hops[0]
require.True(t, hop.BlindedNodePub.IsEqual(pkC))
// As Carol, let's decode the hop data and assert that all expected
// values have been included.
var (
blindingPoint = path.FirstEphemeralBlindingPoint
data *record.BlindedRouteData
)
// Check that Carol's info is correct.
data, blindingPoint = decryptAndDecodeHopData(
t, privC, blindingPoint, hop.CipherText,
)
require.Equal(
t, lnwire.NewShortChanIDFromInt(chanCB),
data.ShortChannelID.UnwrapOrFail(t).Val,
)
require.Equal(t, record.PaymentRelayInfo{
CltvExpiryDelta: 144,
FeeRate: 500,
BaseFee: 100,
}, data.RelayInfo.UnwrapOrFail(t).Val)
require.Equal(t, record.PaymentConstraints{
MaxCltvExpiry: 1500,
HtlcMinimumMsat: 1000,
}, data.Constraints.UnwrapOrFail(t).Val)
// Check that all Bob's info is correct.
hop = path.Hops[1]
data, blindingPoint = decryptAndDecodeHopData(
t, privB, blindingPoint, hop.CipherText,
)
require.Equal(
t, lnwire.NewShortChanIDFromInt(chanBA),
data.ShortChannelID.UnwrapOrFail(t).Val,
)
require.Equal(t, record.PaymentRelayInfo{
CltvExpiryDelta: 144,
FeeRate: 500,
BaseFee: 100,
}, data.RelayInfo.UnwrapOrFail(t).Val)
require.Equal(t, record.PaymentConstraints{
MaxCltvExpiry: 1356,
HtlcMinimumMsat: 1000,
}, data.Constraints.UnwrapOrFail(t).Val)
// Check that all Alice's info is correct.
hop = path.Hops[2]
data, _ = decryptAndDecodeHopData(
t, privA, blindingPoint, hop.CipherText,
)
require.True(t, data.ShortChannelID.IsNone())
require.True(t, data.RelayInfo.IsNone())
require.Equal(t, record.PaymentConstraints{
MaxCltvExpiry: 1212,
HtlcMinimumMsat: 1000,
}, data.Constraints.UnwrapOrFail(t).Val)
require.Equal(t, []byte{1, 2, 3}, data.PathID.UnwrapOrFail(t).Val)
}
// TestBuildBlindedPathWithDummyHops tests the construction of a blinded path
// which includes dummy hops.
func TestBuildBlindedPathWithDummyHops(t *testing.T) {
// Alice chooses the following path to herself for blinded path
// construction:
// Carol -> Bob -> Alice.
// Let's construct the corresponding route.Route for this which will be
// returned from the `FindRoutes` config callback.
var (
privC, pkC = btcec.PrivKeyFromBytes([]byte{1})
privB, pkB = btcec.PrivKeyFromBytes([]byte{2})
privA, pkA = btcec.PrivKeyFromBytes([]byte{3})
carol = route.NewVertex(pkC)
bob = route.NewVertex(pkB)
alice = route.NewVertex(pkA)
chanCB = uint64(1)
chanBA = uint64(2)
)
realRoute := &route.Route{
SourcePubKey: carol,
Hops: []*route.Hop{
{
PubKeyBytes: bob,
ChannelID: chanCB,
},
{
PubKeyBytes: alice,
ChannelID: chanBA,
},
},
}
realPolicies := map[uint64]*models.ChannelEdgePolicy{
chanCB: {
ChannelID: chanCB,
ToNode: bob,
},
chanBA: {
ChannelID: chanBA,
ToNode: alice,
},
}
paths, err := BuildBlindedPaymentPaths(&BuildBlindedPathCfg{
FindRoutes: func(_ lnwire.MilliSatoshi) ([]*route.Route,
error) {
return []*route.Route{realRoute}, nil
},
FetchChannelEdgesByID: func(chanID uint64) (
*models.ChannelEdgeInfo, *models.ChannelEdgePolicy,
*models.ChannelEdgePolicy, error) {
policy, ok := realPolicies[chanID]
if !ok {
return nil, nil, nil,
fmt.Errorf("edge not found")
}
return nil, policy, nil, nil
},
BestHeight: func() (uint32, error) {
return 1000, nil
},
// In the spec example, all the policies get replaced with
// the same static values.
AddPolicyBuffer: func(_ *BlindedHopPolicy) (
*BlindedHopPolicy, error) {
return &BlindedHopPolicy{
FeeRate: 500,
BaseFee: 100,
CLTVExpiryDelta: 144,
MinHTLCMsat: 1000,
MaxHTLCMsat: lnwire.MaxMilliSatoshi,
}, nil
},
PathID: []byte{1, 2, 3},
ValueMsat: 1000,
MinFinalCLTVExpiryDelta: 12,
BlocksUntilExpiry: 200,
// By setting the minimum number of hops to 4, we force 2 dummy
// hops to be added to the real route.
MinNumHops: 4,
DefaultDummyHopPolicy: &BlindedHopPolicy{
CLTVExpiryDelta: 50,
FeeRate: 100,
BaseFee: 100,
MinHTLCMsat: 1000,
MaxHTLCMsat: lnwire.MaxMilliSatoshi,
},
})
require.NoError(t, err)
require.Len(t, paths, 1)
path := paths[0]
// Check that all the accumulated policy values are correct.
require.EqualValues(t, 403, path.FeeBaseMsat)
require.EqualValues(t, 2003, path.FeeRate)
require.EqualValues(t, 588, path.CltvExpiryDelta)
require.EqualValues(t, 1000, path.HTLCMinMsat)
require.EqualValues(t, lnwire.MaxMilliSatoshi, path.HTLCMaxMsat)
// Now we check the hops.
require.Len(t, path.Hops, 5)
// Assert that all the encrypted recipient blobs have been padded such
// that they are all the same size.
require.Len(t, path.Hops[0].CipherText, len(path.Hops[1].CipherText))
require.Len(t, path.Hops[1].CipherText, len(path.Hops[2].CipherText))
require.Len(t, path.Hops[2].CipherText, len(path.Hops[3].CipherText))
require.Len(t, path.Hops[3].CipherText, len(path.Hops[4].CipherText))
// The first hop, should have the real pub key of the introduction
// node: Carol.
hop := path.Hops[0]
require.True(t, hop.BlindedNodePub.IsEqual(pkC))
// As Carol, let's decode the hop data and assert that all expected
// values have been included.
var (
blindingPoint = path.FirstEphemeralBlindingPoint
data *record.BlindedRouteData
)
// Check that Carol's info is correct.
data, blindingPoint = decryptAndDecodeHopData(
t, privC, blindingPoint, hop.CipherText,
)
require.Equal(
t, lnwire.NewShortChanIDFromInt(chanCB),
data.ShortChannelID.UnwrapOrFail(t).Val,
)
require.Equal(t, record.PaymentRelayInfo{
CltvExpiryDelta: 144,
FeeRate: 500,
BaseFee: 100,
}, data.RelayInfo.UnwrapOrFail(t).Val)
require.Equal(t, record.PaymentConstraints{
MaxCltvExpiry: 1788,
HtlcMinimumMsat: 1000,
}, data.Constraints.UnwrapOrFail(t).Val)
// Check that all Bob's info is correct.
hop = path.Hops[1]
data, blindingPoint = decryptAndDecodeHopData(
t, privB, blindingPoint, hop.CipherText,
)
require.Equal(
t, lnwire.NewShortChanIDFromInt(chanBA),
data.ShortChannelID.UnwrapOrFail(t).Val,
)
require.Equal(t, record.PaymentRelayInfo{
CltvExpiryDelta: 144,
FeeRate: 500,
BaseFee: 100,
}, data.RelayInfo.UnwrapOrFail(t).Val)
require.Equal(t, record.PaymentConstraints{
MaxCltvExpiry: 1644,
HtlcMinimumMsat: 1000,
}, data.Constraints.UnwrapOrFail(t).Val)
// Check that all Alice's info is correct. The payload should contain
// a next_node_id field that is equal to Alice's public key. This
// indicates to Alice that she should continue peeling the onion.
hop = path.Hops[2]
data, blindingPoint = decryptAndDecodeHopData(
t, privA, blindingPoint, hop.CipherText,
)
require.True(t, data.ShortChannelID.IsNone())
require.True(t, data.RelayInfo.IsSome())
require.True(t, data.Constraints.IsSome())
require.Equal(t, pkA, data.NextNodeID.UnwrapOrFail(t).Val)
// Alice should be able to decrypt the next payload with her private
// key. This next payload is yet another dummy hop.
hop = path.Hops[3]
data, blindingPoint = decryptAndDecodeHopData(
t, privA, blindingPoint, hop.CipherText,
)
require.True(t, data.ShortChannelID.IsNone())
require.True(t, data.RelayInfo.IsSome())
require.True(t, data.Constraints.IsSome())
require.Equal(t, pkA, data.NextNodeID.UnwrapOrFail(t).Val)
// Unwrapping one more time should reveal the final hop info for Alice.
hop = path.Hops[4]
data, _ = decryptAndDecodeHopData(
t, privA, blindingPoint, hop.CipherText,
)
require.True(t, data.ShortChannelID.IsNone())
require.True(t, data.RelayInfo.IsNone())
require.Equal(t, record.PaymentConstraints{
MaxCltvExpiry: 1212,
HtlcMinimumMsat: 1000,
}, data.Constraints.UnwrapOrFail(t).Val)
require.Equal(t, []byte{1, 2, 3}, data.PathID.UnwrapOrFail(t).Val)
}
// TestSingleHopBlindedPath tests that blinded path construction is done
// correctly for the case where the destination node is also the introduction
// node.
func TestSingleHopBlindedPath(t *testing.T) {
var (
_, pkC = btcec.PrivKeyFromBytes([]byte{1})
carol = route.NewVertex(pkC)
)
realRoute := &route.Route{
SourcePubKey: carol,
// No hops since Carol is both the introduction node and the
// final destination node.
Hops: []*route.Hop{},
}
paths, err := BuildBlindedPaymentPaths(&BuildBlindedPathCfg{
FindRoutes: func(_ lnwire.MilliSatoshi) ([]*route.Route,
error) {
return []*route.Route{realRoute}, nil
},
BestHeight: func() (uint32, error) {
return 1000, nil
},
PathID: []byte{1, 2, 3},
ValueMsat: 1000,
MinFinalCLTVExpiryDelta: 12,
BlocksUntilExpiry: 200,
})
require.NoError(t, err)
require.Len(t, paths, 1)
path := paths[0]
// Check that all the accumulated policy values are correct. Since this
// is a unique case where the destination node is also the introduction
// node, the accumulated fee and HTLC values should be zero and the
// CLTV expiry delta should be equal to Carol's MinFinalCLTVExpiryDelta.
require.EqualValues(t, 0, path.FeeBaseMsat)
require.EqualValues(t, 0, path.FeeRate)
require.EqualValues(t, 0, path.HTLCMinMsat)
require.EqualValues(t, 0, path.HTLCMaxMsat)
require.EqualValues(t, 12, path.CltvExpiryDelta)
}
func decryptAndDecodeHopData(t *testing.T, priv *btcec.PrivateKey,
ephem *btcec.PublicKey, cipherText []byte) (*record.BlindedRouteData,
*btcec.PublicKey) {
router := sphinx.NewRouter(
&keychain.PrivKeyECDH{PrivKey: priv}, nil,
)
decrypted, err := router.DecryptBlindedHopData(ephem, cipherText)
require.NoError(t, err)
buf := bytes.NewBuffer(decrypted)
routeData, err := record.DecodeBlindedRouteData(buf)
require.NoError(t, err)
nextEphem, err := router.NextEphemeral(ephem)
require.NoError(t, err)
return routeData, nextEphem
}

View file

@ -0,0 +1,31 @@
package blindedpath
import (
"github.com/btcsuite/btclog"
"github.com/lightningnetwork/lnd/build"
)
// log is a logger that is initialized with no output filters. This means the
// package will not perform any logging by default until the caller requests
// it.
var log btclog.Logger
const Subsystem = "BLPT"
// The default amount of logging is none.
func init() {
UseLogger(build.NewSubLogger(Subsystem, nil))
}
// DisableLog disables all library log output. Logging output is disabled by
// default until UseLogger is called.
func DisableLog() {
UseLogger(btclog.Disabled)
}
// UseLogger uses a specified Logger to output package logging info. This
// should be used in preference to SetLogWriter if the caller is also using
// btclog.
func UseLogger(logger btclog.Logger) {
log = logger
}

View file

@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"math"
"sort"
"time"
"github.com/btcsuite/btcd/btcutil"
@ -1100,6 +1101,199 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
return pathEdges, distance[source].probability, nil
}
// blindedPathRestrictions are a set of constraints to adhere to when
// choosing a set of blinded paths to this node.
type blindedPathRestrictions struct {
// minNumHops is the minimum number of hops to include in a blinded
// path. This doesn't include our node, so if the minimum is 1, then
// the path will contain at minimum our node along with an introduction
// node hop. A minimum of 0 will include paths where this node is the
// introduction node and so should be used with caution.
minNumHops uint8
// maxNumHops is the maximum number of hops to include in a blinded
// path. This doesn't include our node, so if the maximum is 1, then
// the path will contain our node along with an introduction node hop.
maxNumHops uint8
}
// blindedHop holds the information about a hop we have selected for a blinded
// path.
type blindedHop struct {
vertex route.Vertex
edgePolicy *models.CachedEdgePolicy
edgeCapacity btcutil.Amount
}
// findBlindedPaths does a depth first search from the target node to find a set
// of blinded paths to the target node given the set of restrictions. This
// function will select and return any candidate path. A candidate path is a
// path to the target node with a size determined by the given hop number
// constraints where all the nodes on the path signal the route blinding feature
// _and_ the introduction node for the path has more than one public channel.
// Any filtering of paths based on payment value or success probabilities is
// left to the caller.
func findBlindedPaths(g Graph, target route.Vertex,
restrictions *blindedPathRestrictions) ([][]blindedHop, error) {
// Sanity check the restrictions.
if restrictions.minNumHops > restrictions.maxNumHops {
return nil, fmt.Errorf("maximum number of blinded path hops "+
"(%d) must be greater than or equal to the minimum "+
"number of hops (%d)", restrictions.maxNumHops,
restrictions.minNumHops)
}
// If the node is not the destination node, then it is required that the
// node advertise the route blinding feature-bit in order for it to be
// chosen as a node on the blinded path.
supportsRouteBlinding := func(node route.Vertex) (bool, error) {
if node == target {
return true, nil
}
features, err := g.FetchNodeFeatures(node)
if err != nil {
return false, err
}
return features.HasFeature(lnwire.RouteBlindingOptional), nil
}
// This function will have some recursion. We will spin out from the
// target node & append edges to the paths until we reach various exit
// conditions such as: The maxHops number being reached or reaching
// a node that doesn't have any other edges - in that final case, the
// whole path should be ignored.
paths, _, err := processNodeForBlindedPath(
g, target, supportsRouteBlinding, nil, restrictions,
)
if err != nil {
return nil, err
}
// Reverse each path so that the order is correct (from introduction
// node to last hop node) and then append this node on as the
// destination of each path.
orderedPaths := make([][]blindedHop, len(paths))
for i, path := range paths {
sort.Slice(path, func(i, j int) bool {
return j < i
})
orderedPaths[i] = append(path, blindedHop{vertex: target})
}
// Handle the special case that allows a blinded path with the
// introduction node as the destination node.
if restrictions.minNumHops == 0 {
singleHopPath := [][]blindedHop{{{vertex: target}}}
//nolint:makezero
orderedPaths = append(
orderedPaths, singleHopPath...,
)
}
return orderedPaths, err
}
// processNodeForBlindedPath is a recursive function that traverses the graph
// in a depth first manner searching for a set of blinded paths to the given
// node.
func processNodeForBlindedPath(g Graph, node route.Vertex,
supportsRouteBlinding func(vertex route.Vertex) (bool, error),
alreadyVisited map[route.Vertex]bool,
restrictions *blindedPathRestrictions) ([][]blindedHop, bool, error) {
// If we have already visited the maximum number of hops, then this path
// is complete and we can exit now.
if len(alreadyVisited) > int(restrictions.maxNumHops) {
return nil, false, nil
}
// If we have already visited this peer on this path, then we skip
// processing it again.
if alreadyVisited[node] {
return nil, false, nil
}
supports, err := supportsRouteBlinding(node)
if err != nil {
return nil, false, err
}
if !supports {
return nil, false, nil
}
// At this point, copy the alreadyVisited map.
visited := make(map[route.Vertex]bool, len(alreadyVisited))
for r := range alreadyVisited {
visited[r] = true
}
// Add this node the visited set.
visited[node] = true
var (
hopSets [][]blindedHop
chanCount int
)
// Now, iterate over the node's channels in search for paths to this
// node that can be used for blinded paths
err = g.ForEachNodeChannel(node,
func(channel *channeldb.DirectedChannel) error {
// Keep track of how many incoming channels this node
// has. We only use a node as an introduction node if it
// has channels other than the one that lead us to it.
chanCount++
// Process each channel peer to gather any paths that
// lead to the peer.
nextPaths, hasMoreChans, err := processNodeForBlindedPath( //nolint:lll
g, channel.OtherNode, supportsRouteBlinding,
visited, restrictions,
)
if err != nil {
return err
}
hop := blindedHop{
vertex: channel.OtherNode,
edgePolicy: channel.InPolicy,
edgeCapacity: channel.Capacity,
}
// For each of the paths returned, unwrap them and
// append this hop to them.
for _, path := range nextPaths {
hopSets = append(
hopSets,
append([]blindedHop{hop}, path...),
)
}
// If this node does have channels other than the one
// that lead to it, and if the hop count up to this node
// meets the minHop requirement, then we also add a
// path that starts at this node.
if hasMoreChans &&
len(visited) >= int(restrictions.minNumHops) {
hopSets = append(hopSets, []blindedHop{hop})
}
return nil
},
)
if err != nil {
return nil, false, err
}
return hopSets, chanCount > 1, nil
}
// getProbabilityBasedDist converts a weight into a distance that takes into
// account the success probability and the (virtual) cost of a failed payment
// attempt.

View file

@ -514,7 +514,8 @@ func (g *testGraphInstance) getLink(chanID lnwire.ShortChannelID) (
// not required and derived from the channel data. The goal is to keep
// instantiating a test channel graph as light weight as possible.
func createTestGraphFromChannels(t *testing.T, useCache bool,
testChannels []*testChannel, source string) (*testGraphInstance, error) {
testChannels []*testChannel, source string,
sourceFeatureBits ...lnwire.FeatureBit) (*testGraphInstance, error) {
// We'll use this fake address for the IP address of all the nodes in
// our tests. This value isn't needed for path finding so it doesn't
@ -578,10 +579,13 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
}
// Add the source node.
dbNode, err := addNodeWithAlias(source, lnwire.EmptyFeatureVector())
if err != nil {
return nil, err
}
dbNode, err := addNodeWithAlias(
source, lnwire.NewFeatureVector(
lnwire.NewRawFeatureVector(sourceFeatureBits...),
lnwire.Features,
),
)
require.NoError(t, err)
if err = graph.SetSourceNode(dbNode); err != nil {
return nil, err
@ -3031,10 +3035,11 @@ type pathFindingTestContext struct {
}
func newPathFindingTestContext(t *testing.T, useCache bool,
testChannels []*testChannel, source string) *pathFindingTestContext {
testChannels []*testChannel, source string,
sourceFeatureBits ...lnwire.FeatureBit) *pathFindingTestContext {
testGraphInstance, err := createTestGraphFromChannels(
t, useCache, testChannels, source,
t, useCache, testChannels, source, sourceFeatureBits...,
)
require.NoError(t, err, "unable to create graph")
@ -3077,6 +3082,12 @@ func (c *pathFindingTestContext) findPath(target route.Vertex,
)
}
func (c *pathFindingTestContext) findBlindedPaths(
restrictions *blindedPathRestrictions) ([][]blindedHop, error) {
return dbFindBlindedPaths(c.graph, restrictions)
}
func (c *pathFindingTestContext) assertPath(path []*unifiedEdge,
expected []uint64) {
@ -3134,6 +3145,22 @@ func dbFindPath(graph *channeldb.ChannelGraph,
return route, err
}
// dbFindBlindedPaths calls findBlindedPaths after getting a db transaction from
// the database graph.
func dbFindBlindedPaths(graph *channeldb.ChannelGraph,
restrictions *blindedPathRestrictions) ([][]blindedHop, error) {
sourceNode, err := graph.SourceNode()
if err != nil {
return nil, err
}
return findBlindedPaths(
newMockGraphSessionChanDB(graph), sourceNode.PubKeyBytes,
restrictions,
)
}
// TestBlindedRouteConstruction tests creation of a blinded route with the
// following topology:
//
@ -3506,3 +3533,162 @@ func TestLastHopPayloadSize(t *testing.T) {
})
}
}
// TestFindBlindedPaths tests that the findBlindedPaths function correctly
// selects a set of blinded paths to a destination node given various
// restrictions.
func TestFindBlindedPaths(t *testing.T) {
featuresWithRouteBlinding := lnwire.NewFeatureVector(
lnwire.NewRawFeatureVector(lnwire.RouteBlindingOptional),
lnwire.Features,
)
policyWithRouteBlinding := &testChannelPolicy{
Expiry: 144,
FeeRate: 400,
MinHTLC: 1,
MaxHTLC: 100000000,
Features: featuresWithRouteBlinding,
}
policyWithoutRouteBlinding := &testChannelPolicy{
Expiry: 144,
FeeRate: 400,
MinHTLC: 1,
MaxHTLC: 100000000,
}
// Set up the following graph where Dave will be our destination node.
// All the nodes except for A will signal the Route Blinding feature
// bit.
//
// A --- F
// | |
// G --- D --- B --- E
// | |
// C-----------/
//
testChannels := []*testChannel{
symmetricTestChannel(
"dave", "alice", 100000, policyWithoutRouteBlinding, 1,
),
symmetricTestChannel(
"dave", "bob", 100000, policyWithRouteBlinding, 2,
),
symmetricTestChannel(
"dave", "charlie", 100000, policyWithRouteBlinding, 3,
),
symmetricTestChannel(
"alice", "frank", 100000, policyWithRouteBlinding, 4,
),
symmetricTestChannel(
"bob", "frank", 100000, policyWithRouteBlinding, 5,
),
symmetricTestChannel(
"eve", "charlie", 100000, policyWithRouteBlinding, 6,
),
symmetricTestChannel(
"bob", "eve", 100000, policyWithRouteBlinding, 7,
),
symmetricTestChannel(
"dave", "george", 100000, policyWithRouteBlinding, 8,
),
}
ctx := newPathFindingTestContext(
t, true, testChannels, "dave", lnwire.RouteBlindingOptional,
)
// assertPaths checks that the set of selected paths contains all the
// expected paths.
assertPaths := func(paths [][]blindedHop, expectedPaths []string) {
require.Len(t, paths, len(expectedPaths))
actualPaths := make(map[string]bool)
for _, path := range paths {
var label string
for _, hop := range path {
label += ctx.aliasFromKey(hop.vertex) + ","
}
actualPaths[strings.TrimRight(label, ",")] = true
}
for _, path := range expectedPaths {
require.True(t, actualPaths[path])
}
}
// 1) Restrict the min & max path length such that we only include paths
// with one hop other than the destination hop.
paths, err := ctx.findBlindedPaths(&blindedPathRestrictions{
minNumHops: 1,
maxNumHops: 1,
})
require.NoError(t, err)
// We expect the B->D and C->D paths to be chosen.
// The A->D path is not chosen since A does not advertise the route
// blinding feature bit. The G->D path is not chosen since G does not
// have any other known channels.
assertPaths(paths, []string{
"bob,dave",
"charlie,dave",
})
// 2) Extend the search to include 2 hops other than the destination.
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
minNumHops: 1,
maxNumHops: 2,
})
require.NoError(t, err)
// We expect the following paths:
// - B, D
// - F, B, D
// - E, B, D
// - C, D
// - E, C, D
assertPaths(paths, []string{
"bob,dave",
"frank,bob,dave",
"eve,bob,dave",
"charlie,dave",
"eve,charlie,dave",
})
// 3) Extend the search even further and also increase the minimum path
// length.
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
minNumHops: 2,
maxNumHops: 3,
})
require.NoError(t, err)
// We expect the following paths:
// - F, B, D
// - E, B, D
// - E, C, D
// - B, E, C, D
// - C, E, B, D
assertPaths(paths, []string{
"frank,bob,dave",
"eve,bob,dave",
"eve,charlie,dave",
"bob,eve,charlie,dave",
"charlie,eve,bob,dave",
})
// 4) Finally, we will test the special case where the destination node
// is also the recipient.
paths, err = ctx.findBlindedPaths(&blindedPathRestrictions{
minNumHops: 0,
maxNumHops: 0,
})
require.NoError(t, err)
assertPaths(paths, []string{
"dave",
})
}

View file

@ -5,6 +5,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btclog"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models"
@ -205,6 +206,18 @@ func newPaymentSession(p *LightningPayment, selfNode route.Vertex,
return nil, err
}
if p.BlindedPayment != nil {
if len(edges) != 0 {
return nil, fmt.Errorf("cannot have both route hints " +
"and blinded path")
}
edges, err = p.BlindedPayment.toRouteHints()
if err != nil {
return nil, err
}
}
logPrefix := fmt.Sprintf("PaymentSession(%x):", p.Identifier())
return &paymentSession{
@ -325,8 +338,12 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
switch {
case err == errNoPathFound:
// Don't split if this is a legacy payment without mpp
// record.
if p.payment.PaymentAddr == nil {
// record. If it has a blinded path though, then we
// can split. Split payments to blinded paths won't have
// MPP records.
if p.payment.PaymentAddr == nil &&
p.payment.BlindedPayment == nil {
p.log.Debugf("not splitting because payment " +
"address is unspecified")
@ -344,7 +361,8 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
!destFeatures.HasFeature(lnwire.AMPOptional) {
p.log.Debug("not splitting because " +
"destination doesn't declare MPP or AMP")
"destination doesn't declare MPP or " +
"AMP")
return nil, errNoPathFound
}
@ -389,6 +407,11 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
return nil, err
}
var blindedPath *sphinx.BlindedPath
if p.payment.BlindedPayment != nil {
blindedPath = p.payment.BlindedPayment.BlindedPath
}
// With the next candidate path found, we'll attempt to turn
// this into a route by applying the time-lock and fee
// requirements.
@ -401,7 +424,7 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
records: p.payment.DestCustomRecords,
paymentAddr: p.payment.PaymentAddr,
metadata: p.payment.Metadata,
}, nil,
}, blindedPath,
)
if err != nil {
return nil, err

View file

@ -5,6 +5,7 @@ import (
"context"
"fmt"
"math"
"sort"
"sync"
"sync/atomic"
"time"
@ -675,6 +676,131 @@ func (r *ChannelRouter) FindRoute(req *RouteRequest) (*route.Route, float64,
return route, probability, nil
}
// probabilitySource defines the signature of a function that can be used to
// query the success probability of sending a given amount between the two
// given vertices.
type probabilitySource func(route.Vertex, route.Vertex, lnwire.MilliSatoshi,
btcutil.Amount) float64
// BlindedPathRestrictions are a set of constraints to adhere to when
// choosing a set of blinded paths to this node.
type BlindedPathRestrictions struct {
// MinDistanceFromIntroNode is the minimum number of _real_ (non-dummy)
// hops to include in a blinded path. Since we post-fix dummy hops, this
// is the minimum distance between our node and the introduction node
// of the path. This doesn't include our node, so if the minimum is 1,
// then the path will contain at minimum our node along with an
// introduction node hop.
MinDistanceFromIntroNode uint8
// NumHops is the number of hops that each blinded path should consist
// of. If paths are found with a number of hops less that NumHops, then
// dummy hops will be padded on to the route. This value doesn't
// include our node, so if the maximum is 1, then the path will contain
// our node along with an introduction node hop.
NumHops uint8
// MaxNumPaths is the maximum number of blinded paths to select.
MaxNumPaths uint8
}
// FindBlindedPaths finds a selection of paths to the destination node that can
// be used in blinded payment paths.
func (r *ChannelRouter) FindBlindedPaths(destination route.Vertex,
amt lnwire.MilliSatoshi, probabilitySrc probabilitySource,
restrictions *BlindedPathRestrictions) ([]*route.Route, error) {
// First, find a set of candidate paths given the destination node and
// path length restrictions.
paths, err := findBlindedPaths(
r.cfg.RoutingGraph, destination, &blindedPathRestrictions{
minNumHops: restrictions.MinDistanceFromIntroNode,
maxNumHops: restrictions.NumHops,
},
)
if err != nil {
return nil, err
}
// routeWithProbability groups a route with the probability of a
// payment of the given amount succeeding on that path.
type routeWithProbability struct {
route *route.Route
probability float64
}
// Iterate over all the candidate paths and determine the success
// probability of each path given the data we have about forwards
// between any two nodes on a path.
routes := make([]*routeWithProbability, 0, len(paths))
for _, path := range paths {
if len(path) < 1 {
return nil, fmt.Errorf("a blinded path must have at " +
"least one hop")
}
var (
introNode = path[0].vertex
prevNode = introNode
hops = make(
[]*route.Hop, 0, len(path)-1,
)
totalRouteProbability = float64(1)
)
// For each set of hops on the path, get the success probability
// of a forward between those two vertices and use that to
// update the overall route probability.
for j := 1; j < len(path); j++ {
probability := probabilitySrc(
prevNode, path[j].vertex, amt,
path[j-1].edgeCapacity,
)
totalRouteProbability *= probability
hops = append(hops, &route.Hop{
PubKeyBytes: path[j].vertex,
ChannelID: path[j-1].edgePolicy.ChannelID,
})
prevNode = path[j].vertex
}
// Don't bother adding a route if its success probability less
// minimum that can be assigned to any single pair.
if totalRouteProbability <= DefaultMinRouteProbability {
continue
}
routes = append(routes, &routeWithProbability{
route: &route.Route{
SourcePubKey: introNode,
Hops: hops,
},
probability: totalRouteProbability,
})
}
// Sort the routes based on probability.
sort.Slice(routes, func(i, j int) bool {
return routes[i].probability < routes[j].probability
})
// Now just choose the best paths up until the maximum number of allowed
// paths.
bestRoutes := make([]*route.Route, 0, restrictions.MaxNumPaths)
for _, route := range routes {
if len(bestRoutes) >= int(restrictions.MaxNumPaths) {
break
}
bestRoutes = append(bestRoutes, route.route)
}
return bestRoutes, nil
}
// generateNewSessionKey generates a new ephemeral private key to be used for a
// payment attempt.
func generateNewSessionKey() (*btcec.PrivateKey, error) {
@ -796,9 +922,19 @@ type LightningPayment struct {
// NOTE: This is optional unless required by the payment. When providing
// multiple routes, ensure the hop hints within each route are chained
// together and sorted in forward order in order to reach the
// destination successfully.
// destination successfully. This is mutually exclusive to the
// BlindedPayment field.
RouteHints [][]zpay32.HopHint
// BlindedPayment holds the information about a blinded path to the
// payment recipient. This is mutually exclusive to the RouteHints
// field.
//
// NOTE: a recipient may provide multiple blinded payment paths in the
// same invoice. Currently, LND will only attempt to use the first one.
// A future PR will handle multiple blinded payment paths.
BlindedPayment *BlindedPayment
// OutgoingChannelIDs is the list of channels that are allowed for the
// first hop. If nil, any channel may be used.
OutgoingChannelIDs []uint64

View file

@ -7,6 +7,7 @@ import (
"math"
"math/rand"
"net"
"strings"
"sync"
"sync/atomic"
"testing"
@ -2589,3 +2590,221 @@ func createChannelEdge(bitcoinKey1, bitcoinKey2 []byte,
return fundingTx, &chanUtxo, chanID, nil
}
// TestFindBlindedPathsWithMC tests that the FindBlindedPaths method correctly
// selects a set of blinded paths by using mission control data to select the
// paths with the highest success probability.
func TestFindBlindedPathsWithMC(t *testing.T) {
t.Parallel()
rbFeatureBits := []lnwire.FeatureBit{
lnwire.RouteBlindingOptional,
}
// Create the following graph and let all the nodes advertise support
// for blinded paths.
//
// C
// / \
// / \
// E -- A -- F -- D
// \ /
// \ /
// B
//
featuresWithRouteBlinding := lnwire.NewFeatureVector(
lnwire.NewRawFeatureVector(rbFeatureBits...), lnwire.Features,
)
policyWithRouteBlinding := &testChannelPolicy{
Expiry: 144,
FeeRate: 400,
MinHTLC: 1,
MaxHTLC: 100000000,
Features: featuresWithRouteBlinding,
}
testChannels := []*testChannel{
symmetricTestChannel(
"eve", "alice", 100000, policyWithRouteBlinding, 1,
),
symmetricTestChannel(
"alice", "charlie", 100000, policyWithRouteBlinding, 2,
),
symmetricTestChannel(
"alice", "bob", 100000, policyWithRouteBlinding, 3,
),
symmetricTestChannel(
"charlie", "dave", 100000, policyWithRouteBlinding, 4,
),
symmetricTestChannel(
"bob", "dave", 100000, policyWithRouteBlinding, 5,
),
symmetricTestChannel(
"alice", "frank", 100000, policyWithRouteBlinding, 6,
),
symmetricTestChannel(
"frank", "dave", 100000, policyWithRouteBlinding, 7,
),
}
testGraph, err := createTestGraphFromChannels(
t, true, testChannels, "dave", rbFeatureBits...,
)
require.NoError(t, err)
ctx := createTestCtxFromGraphInstance(t, 101, testGraph)
var (
alice = ctx.aliases["alice"]
bob = ctx.aliases["bob"]
charlie = ctx.aliases["charlie"]
dave = ctx.aliases["dave"]
eve = ctx.aliases["eve"]
frank = ctx.aliases["frank"]
)
// Create a mission control store which initially sets the success
// probability of each node pair to 1.
missionControl := map[route.Vertex]map[route.Vertex]float64{
eve: {alice: 1},
alice: {
charlie: 1,
bob: 1,
frank: 1,
},
charlie: {dave: 1},
bob: {dave: 1},
frank: {dave: 1},
}
// probabilitySrc is a helper that returns the mission control success
// probability of a forward between two vertices.
probabilitySrc := func(from route.Vertex, to route.Vertex,
amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 {
return missionControl[from][to]
}
// All the probabilities are set to 1. So if we restrict the path length
// to 2 and allow a max of 3 routes, then we expect three paths here.
routes, err := ctx.router.FindBlindedPaths(
dave, 1000, probabilitySrc, &BlindedPathRestrictions{
MinDistanceFromIntroNode: 2,
NumHops: 2,
MaxNumPaths: 3,
},
)
require.NoError(t, err)
require.Len(t, routes, 3)
// assertPaths checks that the resulting set of paths is equal to the
// expected set and that the order of the paths is correct.
assertPaths := func(paths []*route.Route, expectedPaths []string) {
require.Len(t, paths, len(expectedPaths))
var actualPaths []string
for _, path := range paths {
label := getAliasFromPubKey(
path.SourcePubKey, ctx.aliases,
) + ","
for _, hop := range path.Hops {
label += getAliasFromPubKey(
hop.PubKeyBytes, ctx.aliases,
) + ","
}
actualPaths = append(
actualPaths, strings.TrimRight(label, ","),
)
}
for i, path := range expectedPaths {
require.Equal(t, expectedPaths[i], path)
}
}
// Now, let's lower the MC probability of the B-D to 0.5 and F-D link to
// 0.25. We will leave the MaxNumPaths as 3 and so all paths should
// still be returned but the order should be:
// 1) A -> C -> D
// 2) A -> B -> D
// 3) A -> F -> D
missionControl[bob][dave] = 0.5
missionControl[frank][dave] = 0.25
routes, err = ctx.router.FindBlindedPaths(
dave, 1000, probabilitySrc, &BlindedPathRestrictions{
MinDistanceFromIntroNode: 2,
NumHops: 2,
MaxNumPaths: 3,
},
)
require.NoError(t, err)
assertPaths(routes, []string{
"alice,charlie,dave",
"alice,bob,dave",
"alice,frank,dave",
})
// Just to show that the above result was not a fluke, let's change
// the C->D link to be the weak one.
missionControl[charlie][dave] = 0.125
routes, err = ctx.router.FindBlindedPaths(
dave, 1000, probabilitySrc, &BlindedPathRestrictions{
MinDistanceFromIntroNode: 2,
NumHops: 2,
MaxNumPaths: 3,
},
)
require.NoError(t, err)
assertPaths(routes, []string{
"alice,bob,dave",
"alice,frank,dave",
"alice,charlie,dave",
})
// Change the MaxNumPaths to 1 to assert that only the best route is
// returned.
routes, err = ctx.router.FindBlindedPaths(
dave, 1000, probabilitySrc, &BlindedPathRestrictions{
MinDistanceFromIntroNode: 2,
NumHops: 2,
MaxNumPaths: 1,
},
)
require.NoError(t, err)
assertPaths(routes, []string{
"alice,bob,dave",
})
// Test the edge case where Dave, the recipient, is also the
// introduction node.
routes, err = ctx.router.FindBlindedPaths(
dave, 1000, probabilitySrc, &BlindedPathRestrictions{
MinDistanceFromIntroNode: 0,
NumHops: 0,
MaxNumPaths: 1,
},
)
require.NoError(t, err)
assertPaths(routes, []string{
"dave",
})
// Finally, we make one of the routes have a probability less than the
// minimum. This means we expect that route not to be chosen.
missionControl[charlie][dave] = DefaultMinRouteProbability
routes, err = ctx.router.FindBlindedPaths(
dave, 1000, probabilitySrc, &BlindedPathRestrictions{
MinDistanceFromIntroNode: 2,
NumHops: 2,
MaxNumPaths: 3,
},
)
require.NoError(t, err)
assertPaths(routes, []string{
"alice,bob,dave",
"alice,frank,dave",
})
}

View file

@ -75,6 +75,7 @@ import (
"github.com/lightningnetwork/lnd/peernotifier"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing"
"github.com/lightningnetwork/lnd/routing/blindedpath"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/rpcperms"
"github.com/lightningnetwork/lnd/signal"
@ -5104,6 +5105,7 @@ type rpcPaymentIntent struct {
paymentAddr *[32]byte
payReq []byte
metadata []byte
blindedPayment *routing.BlindedPayment
destCustomRecords record.CustomSet
@ -5239,6 +5241,32 @@ func (r *rpcServer) extractPaymentIntent(rpcPayReq *rpcPaymentRequest) (rpcPayme
payIntent.paymentAddr = payReq.PaymentAddr
payIntent.metadata = payReq.Metadata
if len(payReq.BlindedPaymentPaths) > 0 {
// NOTE: Currently we only choose a single payment path.
// This will be updated in a future PR to handle
// multiple blinded payment paths.
path := payReq.BlindedPaymentPaths[0]
if len(path.Hops) == 0 {
return payIntent, fmt.Errorf("a blinded " +
"payment must have at least 1 hop")
}
finalHop := path.Hops[len(path.Hops)-1]
payIntent.blindedPayment =
routerrpc.MarshalBlindedPayment(path)
// Replace the target node with the blinded public key
// of the blinded path's final node.
copy(
payIntent.dest[:],
finalHop.BlindedNodePub.SerializeCompressed(),
)
if !payReq.BlindedPaymentPaths[0].Features.IsEmpty() {
payIntent.destFeatures = path.Features.Clone()
}
}
if err := validateDest(payIntent.dest); err != nil {
return payIntent, err
}
@ -5393,6 +5421,7 @@ func (r *rpcServer) dispatchPaymentIntent(
DestFeatures: payIntent.destFeatures,
PaymentAddr: payIntent.paymentAddr,
Metadata: payIntent.metadata,
BlindedPayment: payIntent.blindedPayment,
// Don't enable multi-part payments on the main rpc.
// Users need to use routerrpc for that.
@ -5737,6 +5766,13 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
defaultDelta := r.cfg.Bitcoin.TimeLockDelta
blindingRestrictions := &routing.BlindedPathRestrictions{
MinDistanceFromIntroNode: r.server.cfg.Routing.BlindedPaths.
MinNumRealHops,
NumHops: r.server.cfg.Routing.BlindedPaths.NumHops,
MaxNumPaths: r.server.cfg.Routing.BlindedPaths.MaxNumPaths,
}
addInvoiceCfg := &invoicesrpc.AddInvoiceConfig{
AddInvoice: r.server.invoices.AddInvoice,
IsChannelActive: r.server.htlcSwitch.HasActiveLink,
@ -5746,12 +5782,56 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
ChanDB: r.server.chanStateDB,
Graph: r.server.graphDB,
GenInvoiceFeatures: func() *lnwire.FeatureVector {
return r.server.featureMgr.Get(feature.SetInvoice)
v := r.server.featureMgr.Get(feature.SetInvoice)
if invoice.Blind {
// If an invoice includes blinded paths, then a
// payment address is not required since we use
// the PathID in the final hop's encrypted data
// as equivalent to the payment address
v.Unset(lnwire.PaymentAddrRequired)
v.Set(lnwire.PaymentAddrOptional)
// The invoice payer will also need to
// understand the new BOLT 11 tagged field
// containing the blinded path, so we switch
// the bit to required.
v.Unset(lnwire.Bolt11BlindedPathsOptional)
v.Set(lnwire.Bolt11BlindedPathsRequired)
}
return v
},
GenAmpInvoiceFeatures: func() *lnwire.FeatureVector {
return r.server.featureMgr.Get(feature.SetInvoiceAmp)
},
GetAlias: r.server.aliasMgr.GetPeerAlias,
GetAlias: r.server.aliasMgr.GetPeerAlias,
BestHeight: r.server.cc.BestBlockTracker.BestHeight,
BlindedRoutePolicyIncrMultiplier: r.server.cfg.Routing.
BlindedPaths.PolicyIncreaseMultiplier,
BlindedRoutePolicyDecrMultiplier: r.server.cfg.Routing.
BlindedPaths.PolicyDecreaseMultiplier,
QueryBlindedRoutes: func(amt lnwire.MilliSatoshi) (
[]*route.Route, error) {
return r.server.chanRouter.FindBlindedPaths(
r.selfNode, amt,
r.server.missionControl.GetProbability,
blindingRestrictions,
)
},
MinNumBlindedPathHops: r.server.cfg.Routing.BlindedPaths.
NumHops,
DefaultDummyHopPolicy: &blindedpath.BlindedHopPolicy{
CLTVExpiryDelta: uint16(defaultDelta),
FeeRate: uint32(r.server.cfg.Bitcoin.FeeRate),
BaseFee: r.server.cfg.Bitcoin.BaseFee,
MinHTLCMsat: r.server.cfg.Bitcoin.MinHTLCIn,
// MaxHTLCMsat will be calculated on the fly by using
// the introduction node's channel's capacities.
MaxHTLCMsat: 0,
},
}
value, err := lnrpc.UnmarshallAmt(invoice.Value, invoice.ValueMsat)
@ -5774,6 +5854,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
Private: invoice.Private,
RouteHints: routeHints,
Amp: invoice.IsAmp,
Blind: invoice.Blind,
}
if invoice.RPreimage != nil {

View file

@ -1649,7 +1649,6 @@
; enough to prevent force closes.
; invoices.holdexpirydelta=12
[routing]
; DEPRECATED: This is now turned on by default for Neutrino (use
@ -1663,6 +1662,40 @@
; seen as being live from it's PoV.
; routing.strictgraphpruning=false
; The minimum number of real (non-dummy) blinded hops to select for a blinded
; path. This doesn't include our node, so if the maximum is 1, then the
; shortest paths will contain our node along with an introduction node hop.
; routing.blinding.min-num-real-hops=1
; The number of hops to include in a blinded path. This does not include
; our node, so if is is 1, then the path will at least contain our node along
; with an introduction node hop. If it is 0, then it will use this node as
; the introduction node. This number must be greater than or equal to the
; the number of real hops (invoices.blinding.min-num-real-hops). Any paths
; shorter than this number will be padded with dummy hops.
; routing.blinding.num-hops=2
; The maximum number of blinded paths to select and add to an invoice.
; routing.blinding.max-num-paths=3
; The amount by which to increase certain policy values of hops on a blinded
; path in order to add a probing buffer. The higher this multiplier, the more
; buffer is added to the policy values of hops along a blinded path meaning
; that if they were to increase their policy values before the blinded path
; expires, the better the chances that the path would still be valid meaning
; that the path is less prone to probing attacks. However, if the multiplier
; is too high, the resulting buffered fees might be too much for the payer.
; routing.blinding.policy-increase-multiplier=1.1
; The amount by which to decrease certain policy values of hops on a blinded
; path in order to add a probing buffer. The lower this multiplier, the more
; buffer is added to the policy values of hops along a blinded path meaning
; that if they were to increase their policy values before the blinded path
; expires, the better the chances that the path would still be valid meaning
; that the path is less prone to probing attacks. However, since this value
; is being applied to the MaxHTLC value of the route, the lower it is, the
; lower payment amount will need to be.
; routing.blinding.policy-decrease-multiplier=0.9
[sweeper]

View file

@ -518,9 +518,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
replayLog := htlcswitch.NewDecayedLog(
dbs.DecayedLogDB, cc.ChainNotifier,
)
sphinxRouter := sphinx.NewRouter(
nodeKeyECDH, cfg.ActiveNetParams.Params, replayLog,
)
sphinxRouter := sphinx.NewRouter(nodeKeyECDH, replayLog)
writeBufferPool := pool.NewWriteBuffer(
pool.DefaultWriteBufferGCInterval,

View file

@ -13,15 +13,6 @@ import (
)
const (
// relayInfoSize is the number of bytes that the relay info of a blinded
// payment will occupy.
// base fee: 4 bytes
// prop fee: 4 bytes
// cltv delta: 2 bytes
// min htlc: 8 bytes
// max htlc: 8 bytes
relayInfoSize = 26
// maxNumHopsPerPath is the maximum number of blinded path hops that can
// be included in a single encoded blinded path. This is calculated
// based on the `data_length` limit of 638 bytes for any tagged field in
@ -32,6 +23,12 @@ const (
maxNumHopsPerPath = 7
)
var (
// byteOrder defines the endian-ness we use for encoding to and from
// buffers.
byteOrder = binary.BigEndian
)
// BlindedPaymentPath holds all the information a payer needs to know about a
// blinded path to a receiver of a payment.
type BlindedPaymentPath struct {
@ -69,24 +66,30 @@ type BlindedPaymentPath struct {
// DecodeBlindedPayment attempts to parse a BlindedPaymentPath from the passed
// reader.
func DecodeBlindedPayment(r io.Reader) (*BlindedPaymentPath, error) {
var relayInfo [relayInfoSize]byte
n, err := r.Read(relayInfo[:])
var payment BlindedPaymentPath
if err := binary.Read(r, byteOrder, &payment.FeeBaseMsat); err != nil {
return nil, err
}
if err := binary.Read(r, byteOrder, &payment.FeeRate); err != nil {
return nil, err
}
err := binary.Read(r, byteOrder, &payment.CltvExpiryDelta)
if err != nil {
return nil, err
}
if n != relayInfoSize {
return nil, fmt.Errorf("unable to read %d relay info bytes "+
"off of the given stream: %w", relayInfoSize, err)
err = binary.Read(r, byteOrder, &payment.HTLCMinMsat)
if err != nil {
return nil, err
}
var payment BlindedPaymentPath
// Parse the relay info fields.
payment.FeeBaseMsat = binary.BigEndian.Uint32(relayInfo[:4])
payment.FeeRate = binary.BigEndian.Uint32(relayInfo[4:8])
payment.CltvExpiryDelta = binary.BigEndian.Uint16(relayInfo[8:10])
payment.HTLCMinMsat = binary.BigEndian.Uint64(relayInfo[10:18])
payment.HTLCMaxMsat = binary.BigEndian.Uint64(relayInfo[18:])
err = binary.Read(r, byteOrder, &payment.HTLCMaxMsat)
if err != nil {
return nil, err
}
// Parse the feature bit vector.
f := lnwire.EmptyFeatureVector()
@ -146,24 +149,31 @@ func DecodeBlindedPayment(r io.Reader) (*BlindedPaymentPath, error) {
// 5) Number of hops: 1 byte.
// 6) Encoded BlindedHops.
func (p *BlindedPaymentPath) Encode(w io.Writer) error {
var relayInfo [26]byte
binary.BigEndian.PutUint32(relayInfo[:4], p.FeeBaseMsat)
binary.BigEndian.PutUint32(relayInfo[4:8], p.FeeRate)
binary.BigEndian.PutUint16(relayInfo[8:10], p.CltvExpiryDelta)
binary.BigEndian.PutUint64(relayInfo[10:18], p.HTLCMinMsat)
binary.BigEndian.PutUint64(relayInfo[18:], p.HTLCMaxMsat)
_, err := w.Write(relayInfo[:])
if err != nil {
if err := binary.Write(w, byteOrder, p.FeeBaseMsat); err != nil {
return err
}
err = p.Features.Encode(w)
if err != nil {
if err := binary.Write(w, byteOrder, p.FeeRate); err != nil {
return err
}
_, err = w.Write(p.FirstEphemeralBlindingPoint.SerializeCompressed())
if err := binary.Write(w, byteOrder, p.CltvExpiryDelta); err != nil {
return err
}
if err := binary.Write(w, byteOrder, p.HTLCMinMsat); err != nil {
return err
}
if err := binary.Write(w, byteOrder, p.HTLCMaxMsat); err != nil {
return err
}
if err := p.Features.Encode(w); err != nil {
return err
}
_, err := w.Write(p.FirstEphemeralBlindingPoint.SerializeCompressed())
if err != nil {
return err
}
@ -174,14 +184,12 @@ func (p *BlindedPaymentPath) Encode(w io.Writer) error {
"maximum of %d", numHops, maxNumHopsPerPath)
}
_, err = w.Write([]byte{byte(numHops)})
if err != nil {
if _, err := w.Write([]byte{byte(numHops)}); err != nil {
return err
}
for _, hop := range p.Hops {
err = EncodeBlindedHop(w, hop)
if err != nil {
if err := EncodeBlindedHop(w, hop); err != nil {
return err
}
}