mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-09-02 03:54:26 +02:00
multi: update payload validation to account for blinded routes
This commit is contained in:
@@ -256,33 +256,64 @@ func ValidateParsedPayloadTypes(parsedTypes tlv.TypeMap,
|
|||||||
_, hasNextHop := parsedTypes[record.NextHopOnionType]
|
_, hasNextHop := parsedTypes[record.NextHopOnionType]
|
||||||
_, hasMPP := parsedTypes[record.MPPOnionType]
|
_, hasMPP := parsedTypes[record.MPPOnionType]
|
||||||
_, hasAMP := parsedTypes[record.AMPOnionType]
|
_, hasAMP := parsedTypes[record.AMPOnionType]
|
||||||
|
_, hasEncryptedData := parsedTypes[record.EncryptedDataOnionType]
|
||||||
|
|
||||||
|
// All cleartext hops (including final hop) and the final hop in a
|
||||||
|
// blinded path require the forwading amount and expiry TLVs to be set.
|
||||||
|
needFwdInfo := isFinalHop || !hasEncryptedData
|
||||||
|
|
||||||
|
// No blinded hops should have a next hop specified, and only the final
|
||||||
|
// hop in a cleartext route should exclude it.
|
||||||
|
needNextHop := !(hasEncryptedData || isFinalHop)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
// Hops that need forwarding info must include an amount to forward.
|
||||||
// All hops must include an amount to forward.
|
case needFwdInfo && !hasAmt:
|
||||||
case !hasAmt:
|
|
||||||
return ErrInvalidPayload{
|
return ErrInvalidPayload{
|
||||||
Type: record.AmtOnionType,
|
Type: record.AmtOnionType,
|
||||||
Violation: OmittedViolation,
|
Violation: OmittedViolation,
|
||||||
FinalHop: isFinalHop,
|
FinalHop: isFinalHop,
|
||||||
}
|
}
|
||||||
|
|
||||||
// All hops must include a cltv expiry.
|
// Hops that need forwarding info must include a cltv expiry.
|
||||||
case !hasLockTime:
|
case needFwdInfo && !hasLockTime:
|
||||||
return ErrInvalidPayload{
|
return ErrInvalidPayload{
|
||||||
Type: record.LockTimeOnionType,
|
Type: record.LockTimeOnionType,
|
||||||
Violation: OmittedViolation,
|
Violation: OmittedViolation,
|
||||||
FinalHop: isFinalHop,
|
FinalHop: isFinalHop,
|
||||||
}
|
}
|
||||||
|
|
||||||
// The exit hop should omit the next hop id, otherwise the sender must
|
// Hops that don't need forwarding info shouldn't have an amount TLV.
|
||||||
// have included a record, so we don't need to test for its
|
case !needFwdInfo && hasAmt:
|
||||||
// inclusion at intermediate hops directly.
|
return ErrInvalidPayload{
|
||||||
case isFinalHop && hasNextHop:
|
Type: record.AmtOnionType,
|
||||||
|
Violation: IncludedViolation,
|
||||||
|
FinalHop: isFinalHop,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hops that don't need forwarding info shouldn't have a cltv TLV.
|
||||||
|
case !needFwdInfo && hasLockTime:
|
||||||
|
return ErrInvalidPayload{
|
||||||
|
Type: record.LockTimeOnionType,
|
||||||
|
Violation: IncludedViolation,
|
||||||
|
FinalHop: isFinalHop,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The exit hop and all blinded hops should omit the next hop id.
|
||||||
|
case !needNextHop && hasNextHop:
|
||||||
return ErrInvalidPayload{
|
return ErrInvalidPayload{
|
||||||
Type: record.NextHopOnionType,
|
Type: record.NextHopOnionType,
|
||||||
Violation: IncludedViolation,
|
Violation: IncludedViolation,
|
||||||
FinalHop: true,
|
FinalHop: isFinalHop,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require that the next hop is set for intermediate hops in regular
|
||||||
|
// routes.
|
||||||
|
case needNextHop && !hasNextHop:
|
||||||
|
return ErrInvalidPayload{
|
||||||
|
Type: record.NextHopOnionType,
|
||||||
|
Violation: OmittedViolation,
|
||||||
|
FinalHop: isFinalHop,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intermediate nodes should never receive MPP fields.
|
// Intermediate nodes should never receive MPP fields.
|
||||||
|
@@ -254,6 +254,21 @@ var decodePayloadTests = []decodePayloadTest{
|
|||||||
FinalHop: false,
|
FinalHop: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "intermediate hop no next channel",
|
||||||
|
isFinalHop: false,
|
||||||
|
payload: []byte{
|
||||||
|
// amount
|
||||||
|
0x02, 0x00,
|
||||||
|
// cltv
|
||||||
|
0x04, 0x00,
|
||||||
|
},
|
||||||
|
expErr: hop.ErrInvalidPayload{
|
||||||
|
Type: record.NextHopOnionType,
|
||||||
|
Violation: hop.OmittedViolation,
|
||||||
|
FinalHop: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "intermediate hop with encrypted data",
|
name: "intermediate hop with encrypted data",
|
||||||
isFinalHop: false,
|
isFinalHop: false,
|
||||||
@@ -348,6 +363,81 @@ var decodePayloadTests = []decodePayloadTest{
|
|||||||
},
|
},
|
||||||
shouldHaveTotalAmt: true,
|
shouldHaveTotalAmt: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "final blinded hop with total amount",
|
||||||
|
isFinalHop: true,
|
||||||
|
payload: []byte{
|
||||||
|
// amount
|
||||||
|
0x02, 0x00,
|
||||||
|
// cltv
|
||||||
|
0x04, 0x00,
|
||||||
|
// encrypted data
|
||||||
|
0x0a, 0x03, 0x03, 0x02, 0x01,
|
||||||
|
},
|
||||||
|
shouldHaveEncData: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "final blinded missing amt",
|
||||||
|
isFinalHop: true,
|
||||||
|
payload: []byte{
|
||||||
|
// cltv
|
||||||
|
0x04, 0x00,
|
||||||
|
// encrypted data
|
||||||
|
0x0a, 0x03, 0x03, 0x02, 0x01,
|
||||||
|
},
|
||||||
|
shouldHaveEncData: true,
|
||||||
|
expErr: hop.ErrInvalidPayload{
|
||||||
|
Type: record.AmtOnionType,
|
||||||
|
Violation: hop.OmittedViolation,
|
||||||
|
FinalHop: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "final blinded missing cltv",
|
||||||
|
isFinalHop: true,
|
||||||
|
payload: []byte{
|
||||||
|
// amount
|
||||||
|
0x02, 0x00,
|
||||||
|
// encrypted data
|
||||||
|
0x0a, 0x03, 0x03, 0x02, 0x01,
|
||||||
|
},
|
||||||
|
shouldHaveEncData: true,
|
||||||
|
expErr: hop.ErrInvalidPayload{
|
||||||
|
Type: record.LockTimeOnionType,
|
||||||
|
Violation: hop.OmittedViolation,
|
||||||
|
FinalHop: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "intermediate blinded has amount",
|
||||||
|
isFinalHop: false,
|
||||||
|
payload: []byte{
|
||||||
|
// amount
|
||||||
|
0x02, 0x00,
|
||||||
|
// encrypted data
|
||||||
|
0x0a, 0x03, 0x03, 0x02, 0x01,
|
||||||
|
},
|
||||||
|
expErr: hop.ErrInvalidPayload{
|
||||||
|
Type: record.AmtOnionType,
|
||||||
|
Violation: hop.IncludedViolation,
|
||||||
|
FinalHop: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "intermediate blinded has expiry",
|
||||||
|
isFinalHop: false,
|
||||||
|
payload: []byte{
|
||||||
|
// cltv
|
||||||
|
0x04, 0x00,
|
||||||
|
// encrypted data
|
||||||
|
0x0a, 0x03, 0x03, 0x02, 0x01,
|
||||||
|
},
|
||||||
|
expErr: hop.ErrInvalidPayload{
|
||||||
|
Type: record.LockTimeOnionType,
|
||||||
|
Violation: hop.IncludedViolation,
|
||||||
|
FinalHop: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDecodeHopPayloadRecordValidation asserts that parsing the payloads in the
|
// TestDecodeHopPayloadRecordValidation asserts that parsing the payloads in the
|
||||||
|
@@ -42,9 +42,9 @@ var (
|
|||||||
// ErrMissingField is returned if a required TLV is missing.
|
// ErrMissingField is returned if a required TLV is missing.
|
||||||
ErrMissingField = errors.New("required tlv missing")
|
ErrMissingField = errors.New("required tlv missing")
|
||||||
|
|
||||||
// ErrIncorrectField is returned if a tlv field is included when it
|
// ErrUnexpectedField is returned if a tlv field is included when it
|
||||||
// should not be.
|
// should not be.
|
||||||
ErrIncorrectField = errors.New("incorrect tlv included")
|
ErrUnexpectedField = errors.New("unexpected tlv included")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Vertex is a simple alias for the serialization of a compressed Bitcoin
|
// Vertex is a simple alias for the serialization of a compressed Bitcoin
|
||||||
@@ -337,7 +337,7 @@ func optionalBlindedField(isZero, blindedHop, finalHop bool) error {
|
|||||||
|
|
||||||
// In an intermediate hop in a blinded route and the field is not zero.
|
// In an intermediate hop in a blinded route and the field is not zero.
|
||||||
case !finalHop && !isZero:
|
case !finalHop && !isZero:
|
||||||
return ErrIncorrectField
|
return ErrUnexpectedField
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -351,7 +351,7 @@ func validateNextChanID(nextChanIDIsSet, isBlinded, finalHop bool) error {
|
|||||||
switch {
|
switch {
|
||||||
// Hops in a blinded route should not have a next channel ID set.
|
// Hops in a blinded route should not have a next channel ID set.
|
||||||
case isBlinded && nextChanIDIsSet:
|
case isBlinded && nextChanIDIsSet:
|
||||||
return ErrIncorrectField
|
return ErrUnexpectedField
|
||||||
|
|
||||||
// Otherwise, blinded hops are allowed to have a zero value.
|
// Otherwise, blinded hops are allowed to have a zero value.
|
||||||
case isBlinded:
|
case isBlinded:
|
||||||
@@ -359,7 +359,7 @@ func validateNextChanID(nextChanIDIsSet, isBlinded, finalHop bool) error {
|
|||||||
|
|
||||||
// The final hop in a regular route is expected to have a zero value.
|
// The final hop in a regular route is expected to have a zero value.
|
||||||
case finalHop && nextChanIDIsSet:
|
case finalHop && nextChanIDIsSet:
|
||||||
return ErrIncorrectField
|
return ErrUnexpectedField
|
||||||
|
|
||||||
// Intermediate hops in regular routes require non-zero value.
|
// Intermediate hops in regular routes require non-zero value.
|
||||||
case !finalHop && !nextChanIDIsSet:
|
case !finalHop && !nextChanIDIsSet:
|
||||||
|
@@ -160,18 +160,112 @@ func TestAMPHop(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestNoForwardingParams tests packing of a hop payload without an amount or
|
// TestBlindedHops tests packing of a hop payload for various types of hops in
|
||||||
// expiry height.
|
// a blinded route.
|
||||||
func TestNoForwardingParams(t *testing.T) {
|
func TestBlindedHops(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
hop := Hop{
|
tests := []struct {
|
||||||
EncryptedData: []byte{1, 2, 3},
|
name string
|
||||||
|
hop Hop
|
||||||
|
nextChannel uint64
|
||||||
|
isFinal bool
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "introduction point with next channel",
|
||||||
|
hop: Hop{
|
||||||
|
EncryptedData: []byte{1, 2, 3},
|
||||||
|
BlindingPoint: testPubKey,
|
||||||
|
},
|
||||||
|
nextChannel: 1,
|
||||||
|
isFinal: false,
|
||||||
|
err: ErrUnexpectedField,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "final node with next channel",
|
||||||
|
hop: Hop{
|
||||||
|
EncryptedData: []byte{1, 2, 3},
|
||||||
|
AmtToForward: 150,
|
||||||
|
OutgoingTimeLock: 26,
|
||||||
|
},
|
||||||
|
nextChannel: 1,
|
||||||
|
isFinal: true,
|
||||||
|
err: ErrUnexpectedField,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid introduction point",
|
||||||
|
hop: Hop{
|
||||||
|
EncryptedData: []byte{1, 2, 3},
|
||||||
|
BlindingPoint: testPubKey,
|
||||||
|
},
|
||||||
|
nextChannel: 0,
|
||||||
|
isFinal: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid intermediate blinding",
|
||||||
|
hop: Hop{
|
||||||
|
EncryptedData: []byte{1, 2, 3},
|
||||||
|
},
|
||||||
|
nextChannel: 0,
|
||||||
|
isFinal: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "final blinded missing amount",
|
||||||
|
hop: Hop{
|
||||||
|
EncryptedData: []byte{1, 2, 3},
|
||||||
|
},
|
||||||
|
nextChannel: 0,
|
||||||
|
isFinal: true,
|
||||||
|
err: ErrMissingField,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "final blinded expiry missing",
|
||||||
|
hop: Hop{
|
||||||
|
EncryptedData: []byte{1, 2, 3},
|
||||||
|
AmtToForward: 100,
|
||||||
|
},
|
||||||
|
nextChannel: 0,
|
||||||
|
isFinal: true,
|
||||||
|
err: ErrMissingField,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid final blinded",
|
||||||
|
hop: Hop{
|
||||||
|
EncryptedData: []byte{1, 2, 3},
|
||||||
|
AmtToForward: 100,
|
||||||
|
OutgoingTimeLock: 52,
|
||||||
|
},
|
||||||
|
nextChannel: 0,
|
||||||
|
isFinal: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// The introduction node can also be the final hop.
|
||||||
|
name: "valid final intro blinded",
|
||||||
|
hop: Hop{
|
||||||
|
EncryptedData: []byte{1, 2, 3},
|
||||||
|
BlindingPoint: testPubKey,
|
||||||
|
AmtToForward: 100,
|
||||||
|
OutgoingTimeLock: 52,
|
||||||
|
},
|
||||||
|
nextChannel: 0,
|
||||||
|
isFinal: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var b bytes.Buffer
|
for _, testCase := range tests {
|
||||||
err := hop.PackHopPayload(&b, 0, false)
|
testCase := testCase
|
||||||
require.NoError(t, err)
|
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
err := testCase.hop.PackHopPayload(
|
||||||
|
&b, testCase.nextChannel, testCase.isFinal,
|
||||||
|
)
|
||||||
|
require.ErrorIs(t, err, testCase.err)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestPayloadSize tests the payload size calculation that is provided by Hop
|
// TestPayloadSize tests the payload size calculation that is provided by Hop
|
||||||
|
Reference in New Issue
Block a user