diff --git a/routing/blinding.go b/routing/blinding.go new file mode 100644 index 000000000..0a226276d --- /dev/null +++ b/routing/blinding.go @@ -0,0 +1,79 @@ +package routing + +import ( + "errors" + "fmt" + + sphinx "github.com/lightningnetwork/lightning-onion" + "github.com/lightningnetwork/lnd/lnwire" +) + +var ( + // ErrNoBlindedPath is returned when the blinded path in a blinded + // payment is missing. + ErrNoBlindedPath = errors.New("blinded path required") + + // ErrInsufficientBlindedHops is returned when a blinded path does + // not have enough blinded hops. + ErrInsufficientBlindedHops = errors.New("blinded path requires " + + "at least one hop") + + // ErrHTLCRestrictions is returned when a blinded path has invalid + // HTLC maximum and minimum values. + ErrHTLCRestrictions = errors.New("invalid htlc minimum and maximum") +) + +// BlindedPayment provides the path and payment parameters required to send a +// payment along a blinded path. +type BlindedPayment struct { + // BlindedPath contains the unblinded introduction point and blinded + // hops for the blinded section of the payment. + BlindedPath *sphinx.BlindedPath + + // BaseFee is the total base fee to be paid for payments made over the + // blinded path. + BaseFee uint32 + + // ProportionalFee is the aggregated proportional fee for payments + // made over the blinded path. + ProportionalFee uint32 + + // CltvExpiryDelta is the total expiry delta for the blinded path. Note + // this does not include the final cltv delta for the receiving node + // (which should be provided in an invoice). + CltvExpiryDelta uint16 + + // HtlcMinimum is the highest HLTC minimum supported along the blinded + // path (while some hops may have lower values, we're effectively + // bounded by the highest minimum). + HtlcMinimum uint64 + + // HtlcMaximum is the lowest HTLC maximum supported along the blinded + // path (while some hops may have higher values, we're effectively + // bounded by the lowest maximum). + HtlcMaximum uint64 + + // Features is the set of relay features available for the payment. + Features *lnwire.FeatureVector +} + +// Validate performs validation on a blinded payment. +func (b *BlindedPayment) Validate() error { + if b.BlindedPath == nil { + return ErrNoBlindedPath + } + + // The sphinx library inserts the introduction node as the first hop, + // so we expect at least one hop. + if len(b.BlindedPath.BlindedHops) < 1 { + return fmt.Errorf("%w got: %v", ErrInsufficientBlindedHops, + len(b.BlindedPath.BlindedHops)) + } + + if b.HtlcMaximum < b.HtlcMinimum { + return fmt.Errorf("%w: %v < %v", ErrHTLCRestrictions, + b.HtlcMaximum, b.HtlcMinimum) + } + + return nil +} diff --git a/routing/blinding_test.go b/routing/blinding_test.go new file mode 100644 index 000000000..75ea36d9e --- /dev/null +++ b/routing/blinding_test.go @@ -0,0 +1,70 @@ +package routing + +import ( + "testing" + + sphinx "github.com/lightningnetwork/lightning-onion" + "github.com/stretchr/testify/require" +) + +// TestBlindedPathValidation tests validation of blinded paths. +func TestBlindedPathValidation(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + payment *BlindedPayment + err error + }{ + { + name: "no path", + payment: &BlindedPayment{}, + err: ErrNoBlindedPath, + }, + { + name: "insufficient hops", + payment: &BlindedPayment{ + BlindedPath: &sphinx.BlindedPath{ + BlindedHops: []*sphinx.BlindedHopInfo{}, + }, + }, + err: ErrInsufficientBlindedHops, + }, + { + name: "maximum < minimum", + payment: &BlindedPayment{ + BlindedPath: &sphinx.BlindedPath{ + BlindedHops: []*sphinx.BlindedHopInfo{ + {}, + }, + }, + HtlcMaximum: 10, + HtlcMinimum: 20, + }, + err: ErrHTLCRestrictions, + }, + { + name: "valid", + payment: &BlindedPayment{ + BlindedPath: &sphinx.BlindedPath{ + BlindedHops: []*sphinx.BlindedHopInfo{ + {}, + }, + }, + HtlcMaximum: 15, + HtlcMinimum: 5, + }, + }, + } + + for _, testCase := range tests { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + err := testCase.payment.Validate() + require.ErrorIs(t, err, testCase.err) + }) + } +}