mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-05-12 21:00:03 +02:00
multi: add validation of blinded route encrypted data
Co-authored-by: Calvin Zachman <calvin.zachman@protonmail.com>
This commit is contained in:
parent
c48841a38b
commit
d8979d3086
@ -28,6 +28,10 @@ const (
|
|||||||
// RequiredViolation indicates that an unknown even type was found in
|
// RequiredViolation indicates that an unknown even type was found in
|
||||||
// the payload that we could not process.
|
// the payload that we could not process.
|
||||||
RequiredViolation
|
RequiredViolation
|
||||||
|
|
||||||
|
// InsufficientViolation indicates that the provided type does
|
||||||
|
// not satisfy constraints.
|
||||||
|
InsufficientViolation
|
||||||
)
|
)
|
||||||
|
|
||||||
// String returns a human-readable description of the violation as a verb.
|
// String returns a human-readable description of the violation as a verb.
|
||||||
@ -42,6 +46,9 @@ func (v PayloadViolation) String() string {
|
|||||||
case RequiredViolation:
|
case RequiredViolation:
|
||||||
return "required"
|
return "required"
|
||||||
|
|
||||||
|
case InsufficientViolation:
|
||||||
|
return "insufficient"
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return "unknown violation"
|
return "unknown violation"
|
||||||
}
|
}
|
||||||
@ -410,3 +417,70 @@ func getMinRequiredViolation(set tlv.TypeMap) *tlv.Type {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateBlindedRouteData performs the additional validation that is
|
||||||
|
// required for payments that rely on data provided in an encrypted blob to
|
||||||
|
// be forwarded. We enforce the blinded route's maximum expiry height so that
|
||||||
|
// 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 {
|
||||||
|
|
||||||
|
// Bolt 04 notes that we should enforce payment constraints _if_ they
|
||||||
|
// are present, so we do not fail if not provided.
|
||||||
|
var err error
|
||||||
|
blindedData.Constraints.WhenSome(
|
||||||
|
func(c tlv.RecordT[tlv.TlvType12, record.PaymentConstraints]) {
|
||||||
|
// MUST fail if the expiry is greater than
|
||||||
|
// max_cltv_expiry.
|
||||||
|
if incomingTimelock > c.Val.MaxCltvExpiry {
|
||||||
|
err = ErrInvalidPayload{
|
||||||
|
Type: record.LockTimeOnionType,
|
||||||
|
Violation: InsufficientViolation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MUST fail if the amount is below htlc_minimum_msat.
|
||||||
|
if incomingAmount < c.Val.HtlcMinimumMsat {
|
||||||
|
err = ErrInvalidPayload{
|
||||||
|
Type: record.AmtOnionType,
|
||||||
|
Violation: InsufficientViolation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail if we don't understand any features (even or odd), because we
|
||||||
|
// expect the features to have been set from our announcement. If the
|
||||||
|
// feature vector TLV is not included, it's interpreted as an empty
|
||||||
|
// vector (no validation required).
|
||||||
|
// expect the features to have been set from our announcement.
|
||||||
|
//
|
||||||
|
// Note that we do not yet check the features that the blinded payment
|
||||||
|
// is using against our own features, because there are currently no
|
||||||
|
// payment-related features that they utilize other than tlv-onion,
|
||||||
|
// which is implicitly supported.
|
||||||
|
blindedData.Features.WhenSome(
|
||||||
|
func(f tlv.RecordT[tlv.TlvType14, lnwire.FeatureVector]) {
|
||||||
|
if f.Val.UnknownFeatures() {
|
||||||
|
err = ErrInvalidPayload{
|
||||||
|
Type: 14,
|
||||||
|
Violation: IncludedViolation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -557,3 +557,141 @@ func testDecodeHopPayloadValidation(t *testing.T, test decodePayloadTest) {
|
|||||||
t.Fatalf("invalid custom records")
|
t.Fatalf("invalid custom records")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestValidateBlindedRouteData tests validation of the values provided in a
|
||||||
|
// blinded route.
|
||||||
|
func TestValidateBlindedRouteData(t *testing.T) {
|
||||||
|
scid := lnwire.NewShortChanIDFromInt(1)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
data *record.BlindedRouteData
|
||||||
|
incomingAmount lnwire.MilliSatoshi
|
||||||
|
incomingTimelock uint32
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "max cltv expired",
|
||||||
|
data: record.NewBlindedRouteData(
|
||||||
|
scid,
|
||||||
|
nil,
|
||||||
|
record.PaymentRelayInfo{},
|
||||||
|
&record.PaymentConstraints{
|
||||||
|
MaxCltvExpiry: 100,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
incomingTimelock: 200,
|
||||||
|
err: hop.ErrInvalidPayload{
|
||||||
|
Type: record.LockTimeOnionType,
|
||||||
|
Violation: hop.InsufficientViolation,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero max cltv",
|
||||||
|
data: record.NewBlindedRouteData(
|
||||||
|
scid,
|
||||||
|
nil,
|
||||||
|
record.PaymentRelayInfo{},
|
||||||
|
&record.PaymentConstraints{
|
||||||
|
MaxCltvExpiry: 0,
|
||||||
|
HtlcMinimumMsat: 10,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
incomingAmount: 100,
|
||||||
|
incomingTimelock: 10,
|
||||||
|
err: hop.ErrInvalidPayload{
|
||||||
|
Type: record.LockTimeOnionType,
|
||||||
|
Violation: hop.InsufficientViolation,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "amount below minimum",
|
||||||
|
data: record.NewBlindedRouteData(
|
||||||
|
scid,
|
||||||
|
nil,
|
||||||
|
record.PaymentRelayInfo{},
|
||||||
|
&record.PaymentConstraints{
|
||||||
|
HtlcMinimumMsat: 15,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
incomingAmount: 10,
|
||||||
|
err: hop.ErrInvalidPayload{
|
||||||
|
Type: record.AmtOnionType,
|
||||||
|
Violation: hop.InsufficientViolation,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid, no features",
|
||||||
|
data: record.NewBlindedRouteData(
|
||||||
|
scid,
|
||||||
|
nil,
|
||||||
|
record.PaymentRelayInfo{},
|
||||||
|
&record.PaymentConstraints{
|
||||||
|
MaxCltvExpiry: 100,
|
||||||
|
HtlcMinimumMsat: 20,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
incomingAmount: 40,
|
||||||
|
incomingTimelock: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown features",
|
||||||
|
data: record.NewBlindedRouteData(
|
||||||
|
scid,
|
||||||
|
nil,
|
||||||
|
record.PaymentRelayInfo{},
|
||||||
|
&record.PaymentConstraints{
|
||||||
|
MaxCltvExpiry: 100,
|
||||||
|
HtlcMinimumMsat: 20,
|
||||||
|
},
|
||||||
|
lnwire.NewFeatureVector(
|
||||||
|
lnwire.NewRawFeatureVector(
|
||||||
|
lnwire.FeatureBit(9999),
|
||||||
|
),
|
||||||
|
lnwire.Features,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
incomingAmount: 40,
|
||||||
|
incomingTimelock: 80,
|
||||||
|
err: hop.ErrInvalidPayload{
|
||||||
|
Type: 14,
|
||||||
|
Violation: hop.IncludedViolation,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid data",
|
||||||
|
data: record.NewBlindedRouteData(
|
||||||
|
scid,
|
||||||
|
nil,
|
||||||
|
record.PaymentRelayInfo{
|
||||||
|
CltvExpiryDelta: 10,
|
||||||
|
FeeRate: 10,
|
||||||
|
BaseFee: 100,
|
||||||
|
},
|
||||||
|
&record.PaymentConstraints{
|
||||||
|
MaxCltvExpiry: 100,
|
||||||
|
HtlcMinimumMsat: 20,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
incomingAmount: 40,
|
||||||
|
incomingTimelock: 80,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range tests {
|
||||||
|
testCase := testCase
|
||||||
|
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
err := hop.ValidateBlindedRouteData(
|
||||||
|
testCase.data, testCase.incomingAmount,
|
||||||
|
testCase.incomingTimelock,
|
||||||
|
)
|
||||||
|
require.Equal(t, testCase.err, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -759,6 +759,18 @@ func (fv *FeatureVector) UnknownRequiredFeatures() []FeatureBit {
|
|||||||
return unknown
|
return unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnknownFeatures returns a boolean if a feature vector contains *any*
|
||||||
|
// unknown features (even if they are odd).
|
||||||
|
func (fv *FeatureVector) UnknownFeatures() bool {
|
||||||
|
for feature := range fv.features {
|
||||||
|
if !fv.IsKnown(feature) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Name returns a string identifier for the feature represented by this bit. If
|
// Name returns a string identifier for the feature represented by this bit. If
|
||||||
// the bit does not represent a known feature, this returns a string indicating
|
// the bit does not represent a known feature, this returns a string indicating
|
||||||
// as such.
|
// as such.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user