mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-01-18 21:35:24 +01: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
|
||||
// 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
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user