multi: add validation of blinded route encrypted data

Co-authored-by: Calvin Zachman <calvin.zachman@protonmail.com>
This commit is contained in:
Carla Kirk-Cohen 2023-02-01 11:21:07 -05:00
parent c48841a38b
commit d8979d3086
No known key found for this signature in database
GPG Key ID: 4CA7FE54A6213C91
3 changed files with 224 additions and 0 deletions

View File

@ -28,6 +28,10 @@ const (
// RequiredViolation indicates that an unknown even type was found in
// the payload that we could not process.
RequiredViolation
// InsufficientViolation indicates that the provided type does
// not satisfy constraints.
InsufficientViolation
)
// String returns a human-readable description of the violation as a verb.
@ -42,6 +46,9 @@ func (v PayloadViolation) String() string {
case RequiredViolation:
return "required"
case InsufficientViolation:
return "insufficient"
default:
return "unknown violation"
}
@ -410,3 +417,70 @@ func getMinRequiredViolation(set tlv.TypeMap) *tlv.Type {
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
}

View File

@ -557,3 +557,141 @@ func testDecodeHopPayloadValidation(t *testing.T, test decodePayloadTest) {
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)
})
}
}

View File

@ -759,6 +759,18 @@ func (fv *FeatureVector) UnknownRequiredFeatures() []FeatureBit {
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
// the bit does not represent a known feature, this returns a string indicating
// as such.