Merge pull request #8142 from carlaKC/8137-validateblindedpayload

routing: Add Validation for Decoding Blinded Paths
This commit is contained in:
Olaoluwa Osuntokun authored and GitHub committed 2024-01-03 12:33:11 -08:00
commit cedbdd8999
9 files changed
+488 -117

No files matched your search

@@ -100,6 +100,10 @@
primitive type of the tlv package.
## Misc
* [Added](https://github.com/lightningnetwork/lnd/pull/8142) full validation
for blinded path payloads to allow fuzzing before LND fully supports
blinded payment relay.
### Logging
* [Add the htlc amount](https://github.com/lightningnetwork/lnd/pull/8156) to
contract court logs in case of timed-out htlcs in order to easily spot dust
+37 -12
View File
@@ -92,7 +92,21 @@ func hopFromPayload(p *Payload) (*route.Hop, uint64) {
}, p.FwdInfo.NextHop.ToUint64()
}
func FuzzPayload(f *testing.F) {
// FuzzPayloadFinal fuzzes final hop payloads, providing the additional context
// that the hop should be final (which is usually obtained by the structure
// of the sphinx packet).
func FuzzPayloadFinal(f *testing.F) {
fuzzPayload(f, true)
}
// FuzzPayloadIntermediate fuzzes intermediate hop payloads, providing the
// additional context that a hop should be intermediate (which is usually
// obtained by the structure of the sphinx packet).
func FuzzPayloadIntermediate(f *testing.F) {
fuzzPayload(f, false)
}
func fuzzPayload(f *testing.F, finalPayload bool) {
f.Fuzz(func(t *testing.T, data []byte) {
if len(data) > sphinx.MaxPayloadSize {
return
@@ -100,25 +114,36 @@ func FuzzPayload(f *testing.F) {
r := bytes.NewReader(data)
payload1, err := NewPayloadFromReader(r)
payload1, err := NewPayloadFromReader(r, finalPayload)
if err != nil {
return
}
var b bytes.Buffer
hop, nextChanID := hopFromPayload(payload1)
err = hop.PackHopPayload(&b, nextChanID)
if errors.Is(err, route.ErrAMPMissingMPP) {
// PackHopPayload refuses to encode an AMP record
// without an MPP record. However, NewPayloadFromReader
// does allow decoding an AMP record without an MPP
// record, since validation is done at a later stage. Do
// not report a bug for this case.
err = hop.PackHopPayload(&b, nextChanID, finalPayload)
switch {
// PackHopPayload refuses to encode an AMP record
// without an MPP record. However, NewPayloadFromReader
// does allow decoding an AMP record without an MPP
// record, since validation is done at a later stage. Do
// not report a bug for this case.
case errors.Is(err, route.ErrAMPMissingMPP):
return
}
require.NoError(t, err)
payload2, err := NewPayloadFromReader(&b)
// PackHopPayload will not encode regular payloads or final
// hops in blinded routes that do not have an amount or expiry
// TLV set. However, NewPayloadFromReader will allow creation
// of payloads where these TLVs are present, but they have
// zero values because validation is done at a later stage.
case errors.Is(err, route.ErrMissingField):
return
default:
require.NoError(t, err)
}
payload2, err := NewPayloadFromReader(&b, finalPayload)
require.NoError(t, err)
require.Equal(t, payload1, payload2)
+4 -3
View File
@@ -90,9 +90,10 @@ func (r *sphinxHopIterator) HopPayload() (*Payload, error) {
// Otherwise, if this is the TLV payload, then we'll make a new stream
// to decode only what we need to make routing decisions.
case sphinx.PayloadTLV:
return NewPayloadFromReader(bytes.NewReader(
r.processedPacket.Payload.Payload,
))
return NewPayloadFromReader(
bytes.NewReader(r.processedPacket.Payload.Payload),
r.processedPacket.Action == sphinx.ExitNode,
)
default:
return nil, fmt.Errorf("unknown sphinx payload type: %v",
+4 -2
View File
@@ -65,14 +65,16 @@ func TestSphinxHopIteratorForwardingInstructions(t *testing.T) {
},
expectedFwdInfo: expectedFwdInfo,
},
// A TLV payload, we can leave off the action as we'll always
// read the cid encoded.
// A TLV payload, which includes the sphinx action as
// cid may be zero for blinded routes (thus we require the
// action to signal whether we are at the final hop).
{
sphinxPacket: &sphinx.ProcessedPacket{
Payload: sphinx.HopPayload{
Type: sphinx.PayloadTLV,
Payload: b.Bytes(),
},
Action: sphinx.MoreHops,
},
expectedFwdInfo: expectedFwdInfo,
},
+49 -19
View File
@@ -127,8 +127,10 @@ func NewLegacyPayload(f *sphinx.HopData) *Payload {
}
// NewPayloadFromReader builds a new Hop from the passed io.Reader. The reader
// should correspond to the bytes encapsulated in a TLV onion payload.
func NewPayloadFromReader(r io.Reader) (*Payload, error) {
// should correspond to the bytes encapsulated in a TLV onion payload. The
// final hop bool signals that this payload was the final packet parsed by
// sphinx.
func NewPayloadFromReader(r io.Reader, finalHop bool) (*Payload, error) {
var (
cid uint64
amt uint64
@@ -165,8 +167,7 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {
// Validate whether the sender properly included or omitted tlv records
// in accordance with BOLT 04.
nextHop := lnwire.NewShortChanIDFromInt(cid)
err = ValidateParsedPayloadTypes(parsedTypes, nextHop)
err = ValidateParsedPayloadTypes(parsedTypes, finalHop)
if err != nil {
return nil, err
}
@@ -177,7 +178,7 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {
return nil, ErrInvalidPayload{
Type: *violatingType,
Violation: RequiredViolation,
FinalHop: nextHop == Exit,
FinalHop: finalHop,
}
}
@@ -210,7 +211,7 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {
return &Payload{
FwdInfo: ForwardingInfo{
NextHop: nextHop,
NextHop: lnwire.NewShortChanIDFromInt(cid),
AmountToForward: lnwire.MilliSatoshi(amt),
OutgoingCTLV: cltv,
},
@@ -248,42 +249,71 @@ func NewCustomRecords(parsedTypes tlv.TypeMap) record.CustomSet {
// boolean should be true if the payload was parsed for an exit hop. The
// requirements for this method are described in BOLT 04.
func ValidateParsedPayloadTypes(parsedTypes tlv.TypeMap,
nextHop lnwire.ShortChannelID) error {
isFinalHop := nextHop == Exit
isFinalHop bool) error {
_, hasAmt := parsedTypes[record.AmtOnionType]
_, hasLockTime := parsedTypes[record.LockTimeOnionType]
_, hasNextHop := parsedTypes[record.NextHopOnionType]
_, hasMPP := parsedTypes[record.MPPOnionType]
_, 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 {
// All hops must include an amount to forward.
case !hasAmt:
// Hops that need forwarding info must include an amount to forward.
case needFwdInfo && !hasAmt:
return ErrInvalidPayload{
Type: record.AmtOnionType,
Violation: OmittedViolation,
FinalHop: isFinalHop,
}
// All hops must include a cltv expiry.
case !hasLockTime:
// Hops that need forwarding info must include a cltv expiry.
case needFwdInfo && !hasLockTime:
return ErrInvalidPayload{
Type: record.LockTimeOnionType,
Violation: OmittedViolation,
FinalHop: isFinalHop,
}
// The exit hop should omit the next hop id. If nextHop != Exit, the
// sender must have included a record, so we don't need to test for its
// inclusion at intermediate hops directly.
case isFinalHop && hasNextHop:
// Hops that don't need forwarding info shouldn't have an amount TLV.
case !needFwdInfo && hasAmt:
return ErrInvalidPayload{
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{
Type: record.NextHopOnionType,
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.
+154 -35
View File
@@ -24,6 +24,7 @@ const testUnknownRequiredType = 0x80
type decodePayloadTest struct {
name string
payload []byte
isFinalHop bool
expErr error
expCustomRecords map[uint64][]byte
shouldHaveMPP bool
@@ -36,18 +37,21 @@ type decodePayloadTest struct {
var decodePayloadTests = []decodePayloadTest{
{
name: "final hop valid",
payload: []byte{0x02, 0x00, 0x04, 0x00},
name: "final hop valid",
isFinalHop: true,
payload: []byte{0x02, 0x00, 0x04, 0x00},
},
{
name: "intermediate hop valid",
name: "intermediate hop valid",
isFinalHop: false,
payload: []byte{0x02, 0x00, 0x04, 0x00, 0x06, 0x08, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
},
{
name: "final hop no amount",
payload: []byte{0x04, 0x00},
name: "final hop no amount",
payload: []byte{0x04, 0x00},
isFinalHop: true,
expErr: hop.ErrInvalidPayload{
Type: record.AmtOnionType,
Violation: hop.OmittedViolation,
@@ -55,7 +59,8 @@ var decodePayloadTests = []decodePayloadTest{
},
},
{
name: "intermediate hop no amount",
name: "intermediate hop no amount",
isFinalHop: false,
payload: []byte{0x04, 0x00, 0x06, 0x08, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
},
@@ -66,8 +71,9 @@ var decodePayloadTests = []decodePayloadTest{
},
},
{
name: "final hop no expiry",
payload: []byte{0x02, 0x00},
name: "final hop no expiry",
isFinalHop: true,
payload: []byte{0x02, 0x00},
expErr: hop.ErrInvalidPayload{
Type: record.LockTimeOnionType,
Violation: hop.OmittedViolation,
@@ -75,7 +81,8 @@ var decodePayloadTests = []decodePayloadTest{
},
},
{
name: "intermediate hop no expiry",
name: "intermediate hop no expiry",
isFinalHop: false,
payload: []byte{0x02, 0x00, 0x06, 0x08, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
},
@@ -86,7 +93,8 @@ var decodePayloadTests = []decodePayloadTest{
},
},
{
name: "final hop next sid present",
name: "final hop next sid present",
isFinalHop: true,
payload: []byte{0x02, 0x00, 0x04, 0x00, 0x06, 0x08, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
@@ -97,7 +105,8 @@ var decodePayloadTests = []decodePayloadTest{
},
},
{
name: "required type after omitted hop id",
name: "required type after omitted hop id",
isFinalHop: true,
payload: []byte{
0x02, 0x00, 0x04, 0x00,
testUnknownRequiredType, 0x00,
@@ -109,7 +118,8 @@ var decodePayloadTests = []decodePayloadTest{
},
},
{
name: "required type after included hop id",
name: "required type after included hop id",
isFinalHop: false,
payload: []byte{
0x02, 0x00, 0x04, 0x00, 0x06, 0x08, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
@@ -122,8 +132,9 @@ var decodePayloadTests = []decodePayloadTest{
},
},
{
name: "required type zero final hop",
payload: []byte{0x00, 0x00, 0x02, 0x00, 0x04, 0x00},
name: "required type zero final hop",
isFinalHop: true,
payload: []byte{0x00, 0x00, 0x02, 0x00, 0x04, 0x00},
expErr: hop.ErrInvalidPayload{
Type: 0,
Violation: hop.RequiredViolation,
@@ -131,7 +142,8 @@ var decodePayloadTests = []decodePayloadTest{
},
},
{
name: "required type zero final hop zero sid",
name: "required type zero final hop zero sid",
isFinalHop: true,
payload: []byte{0x00, 0x00, 0x02, 0x00, 0x04, 0x00, 0x06, 0x08,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
@@ -142,7 +154,8 @@ var decodePayloadTests = []decodePayloadTest{
},
},
{
name: "required type zero intermediate hop",
name: "required type zero intermediate hop",
isFinalHop: false,
payload: []byte{0x00, 0x00, 0x02, 0x00, 0x04, 0x00, 0x06, 0x08,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
@@ -153,8 +166,17 @@ var decodePayloadTests = []decodePayloadTest{
},
},
{
name: "required type in custom range",
payload: []byte{0x02, 0x00, 0x04, 0x00,
name: "required type in custom range",
isFinalHop: false,
payload: []byte{
// amount
0x02, 0x00,
// cltv
0x04, 0x00,
// next hop id
0x06, 0x08,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// custom
0xfe, 0x00, 0x01, 0x00, 0x00, 0x02, 0x10, 0x11,
},
expCustomRecords: map[uint64][]byte{
@@ -162,19 +184,22 @@ var decodePayloadTests = []decodePayloadTest{
},
},
{
name: "valid intermediate hop",
name: "valid intermediate hop",
isFinalHop: false,
payload: []byte{0x02, 0x00, 0x04, 0x00, 0x06, 0x08, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
expErr: nil,
},
{
name: "valid final hop",
payload: []byte{0x02, 0x00, 0x04, 0x00},
expErr: nil,
name: "valid final hop",
isFinalHop: true,
payload: []byte{0x02, 0x00, 0x04, 0x00},
expErr: nil,
},
{
name: "intermediate hop with mpp",
name: "intermediate hop with mpp",
isFinalHop: false,
payload: []byte{
// amount
0x02, 0x00,
@@ -198,7 +223,8 @@ var decodePayloadTests = []decodePayloadTest{
},
},
{
name: "intermediate hop with amp",
name: "intermediate hop with amp",
isFinalHop: false,
payload: []byte{
// amount
0x02, 0x00,
@@ -229,24 +255,35 @@ var decodePayloadTests = []decodePayloadTest{
},
},
{
name: "intermediate hop with encrypted data",
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",
isFinalHop: false,
payload: []byte{
// encrypted data
0x0a, 0x03, 0x03, 0x02, 0x01,
},
shouldHaveEncData: true,
},
{
name: "intermediate hop with blinding point",
name: "intermediate hop with blinding point",
isFinalHop: false,
payload: append([]byte{
// amount
0x02, 0x00,
// cltv
0x04, 0x00,
// encrypted data
0x0a, 0x03, 0x03, 0x02, 0x01,
// blinding point (type / length)
0x0c, 0x21,
},
@@ -254,9 +291,11 @@ var decodePayloadTests = []decodePayloadTest{
testPubKey.SerializeCompressed()...,
),
shouldHaveBlinding: true,
shouldHaveEncData: true,
},
{
name: "final hop with mpp",
name: "final hop with mpp",
isFinalHop: true,
payload: []byte{
// amount
0x02, 0x00,
@@ -274,7 +313,8 @@ var decodePayloadTests = []decodePayloadTest{
shouldHaveMPP: true,
},
{
name: "final hop with amp",
name: "final hop with amp",
isFinalHop: true,
payload: []byte{
// amount
0x02, 0x00,
@@ -298,7 +338,8 @@ var decodePayloadTests = []decodePayloadTest{
shouldHaveAMP: true,
},
{
name: "final hop with metadata",
name: "final hop with metadata",
isFinalHop: true,
payload: []byte{
// amount
0x02, 0x00,
@@ -310,7 +351,8 @@ var decodePayloadTests = []decodePayloadTest{
shouldHaveMetadata: true,
},
{
name: "final hop with total amount",
name: "final hop with total amount",
isFinalHop: true,
payload: []byte{
// amount
0x02, 0x00,
@@ -321,6 +363,81 @@ var decodePayloadTests = []decodePayloadTest{
},
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
@@ -361,7 +478,9 @@ func testDecodeHopPayloadValidation(t *testing.T, test decodePayloadTest) {
testChildIndex = uint32(9)
)
p, err := hop.NewPayloadFromReader(bytes.NewReader(test.payload))
p, err := hop.NewPayloadFromReader(
bytes.NewReader(test.payload), test.isFinalHop,
)
if !reflect.DeepEqual(test.expErr, err) {
t.Fatalf("expected error mismatch, want: %v, got: %v",
test.expErr, err)
+105 -14
View File
@@ -38,6 +38,13 @@ var (
// ErrAMPMissingMPP is returned when the caller tries to attach an AMP
// record but no MPP record is presented for the final hop.
ErrAMPMissingMPP = errors.New("cannot send AMP without MPP record")
// ErrMissingField is returned if a required TLV is missing.
ErrMissingField = errors.New("required tlv missing")
// ErrUnexpectedField is returned if a tlv field is included when it
// should not be.
ErrUnexpectedField = errors.New("unexpected tlv included")
)
// Vertex is a simple alias for the serialization of a compressed Bitcoin
@@ -172,11 +179,15 @@ func (h *Hop) Copy() *Hop {
// PackHopPayload writes to the passed io.Writer, the series of byes that can
// be placed directly into the per-hop payload (EOB) for this hop. This will
// include the required routing fields, as well as serializing any of the
// passed optional TLVRecords. nextChanID is the unique channel ID that
// references the _outgoing_ channel ID that follows this hop. This field
// follows the same semantics as the NextAddress field in the onion: it should
// be set to zero to indicate the terminal hop.
func (h *Hop) PackHopPayload(w io.Writer, nextChanID uint64) error {
// passed optional TLVRecords. nextChanID is the unique channel ID that
// references the _outgoing_ channel ID that follows this hop. The lastHop bool
// is used to signal whether this hop is the final hop in a route. Previously,
// a zero nextChanID would be used for this purpose, but with the addition of
// blinded routes which allow zero nextChanID values for intermediate hops we
// add an explicit signal.
func (h *Hop) PackHopPayload(w io.Writer, nextChanID uint64,
finalHop bool) error {
// If this is a legacy payload, then we'll exit here as this method
// shouldn't be called.
if h.LegacyPayload == true {
@@ -189,8 +200,28 @@ func (h *Hop) PackHopPayload(w io.Writer, nextChanID uint64) error {
var records []tlv.Record
// Hops that are not part of a blinded path will have an amount and
// a CLTV expiry field. Zero values indicate that the hop is inside of
// a blinded route, so the TLV should not be included.
// a CLTV expiry field. In a blinded route (where encrypted data is
// non-nil), these values may be omitted for intermediate nodes.
// Validate these fields against the structure of the payload so that
// we know they're included (or excluded) correctly.
isBlinded := h.EncryptedData != nil
if err := optionalBlindedField(
h.AmtToForward == 0, isBlinded, finalHop,
); err != nil {
return fmt.Errorf("%w: amount to forward: %v", err,
h.AmtToForward)
}
if err := optionalBlindedField(
h.OutgoingTimeLock == 0, isBlinded, finalHop,
); err != nil {
return fmt.Errorf("%w: outgoing timelock: %v", err,
h.OutgoingTimeLock)
}
// Once we've validated that these TLVs are set as we expect, we can
// go ahead and include them if non-zero.
amt := uint64(h.AmtToForward)
if amt != 0 {
records = append(
@@ -204,10 +235,13 @@ func (h *Hop) PackHopPayload(w io.Writer, nextChanID uint64) error {
)
}
// BOLT 04 says the next_hop_id should be omitted for the final hop,
// but present for all others.
//
// TODO(conner): test using hop.Exit once available
// Validate channel TLV is present as expected based on location in
// route and whether this hop is blinded.
err := validateNextChanID(nextChanID != 0, isBlinded, finalHop)
if err != nil {
return fmt.Errorf("%w: channel id: %v", err, nextChanID)
}
if nextChanID != 0 {
records = append(records,
record.NewNextHopIDRecord(&nextChanID),
@@ -218,7 +252,7 @@ func (h *Hop) PackHopPayload(w io.Writer, nextChanID uint64) error {
// attach it to the final hop. Otherwise the route was constructed
// incorrectly.
if h.MPP != nil {
if nextChanID == 0 {
if finalHop {
records = append(records, h.MPP.Record())
} else {
return ErrIntermediateMPPHop
@@ -280,6 +314,62 @@ func (h *Hop) PackHopPayload(w io.Writer, nextChanID uint64) error {
return tlvStream.Encode(w)
}
// optionalBlindedField validates fields that we expect to be non-zero for all
// hops in a regular route, but may be zero for intermediate nodes in a blinded
// route. It will validate the following cases:
// - Not blinded: require non-zero values.
// - Intermediate blinded node: require zero values.
// - Final blinded node: require non-zero values.
func optionalBlindedField(isZero, blindedHop, finalHop bool) error {
switch {
// We are not in a blinded route and the TLV is not set when it should
// be.
case !blindedHop && isZero:
return ErrMissingField
// We are not in a blinded route and the TLV is set as expected.
case !blindedHop:
return nil
// In a blinded route the final hop is expected to have TLV values set.
case finalHop && isZero:
return ErrMissingField
// In an intermediate hop in a blinded route and the field is not zero.
case !finalHop && !isZero:
return ErrUnexpectedField
}
return nil
}
// validateNextChanID validates the presence of the nextChanID TLV field in
// a payload. For regular payments, it is expected to be present for all hops
// except the final hop. For blinded paths, it is not expected to be included
// at all (as this value is provided in encrypted data).
func validateNextChanID(nextChanIDIsSet, isBlinded, finalHop bool) error {
switch {
// Hops in a blinded route should not have a next channel ID set.
case isBlinded && nextChanIDIsSet:
return ErrUnexpectedField
// Otherwise, blinded hops are allowed to have a zero value.
case isBlinded:
return nil
// The final hop in a regular route is expected to have a zero value.
case finalHop && nextChanIDIsSet:
return ErrUnexpectedField
// Intermediate hops in regular routes require non-zero value.
case !finalHop && !nextChanIDIsSet:
return ErrMissingField
default:
return nil
}
}
// Size returns the total size this hop's payload would take up in the onion
// packet.
func (h *Hop) PayloadSize(nextChanID uint64) uint64 {
@@ -559,7 +649,8 @@ func (r *Route) ToSphinxPath() (*sphinx.PaymentPath, error) {
// If we aren't on the last hop, then we set the "next address"
// field to be the channel that directly follows it.
if i != len(r.Hops)-1 {
finalHop := i == len(r.Hops)-1
if !finalHop {
nextHop = r.Hops[i+1].ChannelID
}
@@ -591,7 +682,7 @@ func (r *Route) ToSphinxPath() (*sphinx.PaymentPath, error) {
// channel should be forwarded to so we can construct a
// valid payload.
var b bytes.Buffer
err := hop.PackHopPayload(&b, nextHop)
err := hop.PackHopPayload(&b, nextHop, finalHop)
if err != nil {
return nil, err
}
+111 -16
View File
@@ -105,7 +105,7 @@ func TestMPPHop(t *testing.T) {
// Encoding an MPP record to an intermediate hop should result in a
// failure.
var b bytes.Buffer
err := hop.PackHopPayload(&b, 2)
err := hop.PackHopPayload(&b, 2, false)
if err != ErrIntermediateMPPHop {
t.Fatalf("expected err: %v, got: %v",
ErrIntermediateMPPHop, err)
@@ -113,7 +113,7 @@ func TestMPPHop(t *testing.T) {
// Encoding an MPP record to a final hop should be successful.
b.Reset()
err = hop.PackHopPayload(&b, 0)
err = hop.PackHopPayload(&b, 0, true)
if err != nil {
t.Fatalf("expected err: %v, got: %v", nil, err)
}
@@ -135,7 +135,7 @@ func TestAMPHop(t *testing.T) {
// Encoding an AMP record to an intermediate hop w/o an MPP record
// should result in a failure.
var b bytes.Buffer
err := hop.PackHopPayload(&b, 2)
err := hop.PackHopPayload(&b, 2, false)
if err != ErrAMPMissingMPP {
t.Fatalf("expected err: %v, got: %v",
ErrAMPMissingMPP, err)
@@ -144,7 +144,7 @@ func TestAMPHop(t *testing.T) {
// Encoding an AMP record to a final hop w/o an MPP record should result
// in a failure.
b.Reset()
err = hop.PackHopPayload(&b, 0)
err = hop.PackHopPayload(&b, 0, true)
if err != ErrAMPMissingMPP {
t.Fatalf("expected err: %v, got: %v",
ErrAMPMissingMPP, err)
@@ -154,24 +154,118 @@ func TestAMPHop(t *testing.T) {
// successful.
hop.MPP = record.NewMPP(testAmt, testAddr)
b.Reset()
err = hop.PackHopPayload(&b, 0)
err = hop.PackHopPayload(&b, 0, true)
if err != nil {
t.Fatalf("expected err: %v, got: %v", nil, err)
}
}
// TestNoForwardingParams tests packing of a hop payload without an amount or
// expiry height.
func TestNoForwardingParams(t *testing.T) {
// TestBlindedHops tests packing of a hop payload for various types of hops in
// a blinded route.
func TestBlindedHops(t *testing.T) {
t.Parallel()
hop := Hop{
EncryptedData: []byte{1, 2, 3},
tests := []struct {
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
err := hop.PackHopPayload(&b, 2)
require.NoError(t, err)
for _, testCase := range tests {
testCase := testCase
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
@@ -182,21 +276,22 @@ func TestPayloadSize(t *testing.T) {
hops := []*Hop{
{
PubKeyBytes: testPubKeyBytes,
AmtToForward: 1000,
AmtToForward: 2000,
OutgoingTimeLock: 600000,
ChannelID: 3432483437438,
LegacyPayload: true,
},
{
PubKeyBytes: testPubKeyBytes,
AmtToForward: 1200,
AmtToForward: 1500,
OutgoingTimeLock: 700000,
ChannelID: 63584534844,
},
{
PubKeyBytes: testPubKeyBytes,
AmtToForward: 1200,
AmtToForward: 1000,
OutgoingTimeLock: 700000,
ChannelID: 51784534844,
MPP: record.NewMPP(500, [32]byte{}),
AMP: record.NewAMP(
[32]byte{}, [32]byte{}, 8,
+20 -16
View File
@@ -3471,10 +3471,11 @@ func TestSendToRouteSkipTempErrSuccess(t *testing.T) {
// Create a simple 1-hop route.
hops := []*route.Hop{
{
ChannelID: 1,
PubKeyBytes: node.PubKeyBytes,
AmtToForward: payAmt,
MPP: record.NewMPP(payAmt, [32]byte{}),
ChannelID: 1,
PubKeyBytes: node.PubKeyBytes,
AmtToForward: payAmt,
OutgoingTimeLock: 120,
MPP: record.NewMPP(payAmt, [32]byte{}),
},
}
rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops)
@@ -3607,10 +3608,11 @@ func TestSendToRouteSkipTempErrTempFailure(t *testing.T) {
// Create a simple 1-hop route.
hops := []*route.Hop{
{
ChannelID: 1,
PubKeyBytes: node.PubKeyBytes,
AmtToForward: payAmt,
MPP: record.NewMPP(payAmt, [32]byte{}),
ChannelID: 1,
PubKeyBytes: node.PubKeyBytes,
AmtToForward: payAmt,
OutgoingTimeLock: 120,
MPP: record.NewMPP(payAmt, [32]byte{}),
},
}
rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops)
@@ -3688,10 +3690,11 @@ func TestSendToRouteSkipTempErrPermanentFailure(t *testing.T) {
// Create a simple 1-hop route.
hops := []*route.Hop{
{
ChannelID: 1,
PubKeyBytes: node.PubKeyBytes,
AmtToForward: payAmt,
MPP: record.NewMPP(payAmt, [32]byte{}),
ChannelID: 1,
PubKeyBytes: node.PubKeyBytes,
AmtToForward: payAmt,
OutgoingTimeLock: 120,
MPP: record.NewMPP(payAmt, [32]byte{}),
},
}
rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops)
@@ -3773,10 +3776,11 @@ func TestSendToRouteTempFailure(t *testing.T) {
// Create a simple 1-hop route.
hops := []*route.Hop{
{
ChannelID: 1,
PubKeyBytes: node.PubKeyBytes,
AmtToForward: payAmt,
MPP: record.NewMPP(payAmt, [32]byte{}),
ChannelID: 1,
PubKeyBytes: node.PubKeyBytes,
AmtToForward: payAmt,
OutgoingTimeLock: 120,
MPP: record.NewMPP(payAmt, [32]byte{}),
},
}
rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops)