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. primitive type of the tlv package.
## Misc ## 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 ### Logging
* [Add the htlc amount](https://github.com/lightningnetwork/lnd/pull/8156) to * [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 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() }, 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) { f.Fuzz(func(t *testing.T, data []byte) {
if len(data) > sphinx.MaxPayloadSize { if len(data) > sphinx.MaxPayloadSize {
return return
@@ -100,25 +114,36 @@ func FuzzPayload(f *testing.F) {
r := bytes.NewReader(data) r := bytes.NewReader(data)
payload1, err := NewPayloadFromReader(r) payload1, err := NewPayloadFromReader(r, finalPayload)
if err != nil { if err != nil {
return return
} }
var b bytes.Buffer var b bytes.Buffer
hop, nextChanID := hopFromPayload(payload1) hop, nextChanID := hopFromPayload(payload1)
err = hop.PackHopPayload(&b, nextChanID) err = hop.PackHopPayload(&b, nextChanID, finalPayload)
if errors.Is(err, route.ErrAMPMissingMPP) { switch {
// PackHopPayload refuses to encode an AMP record // PackHopPayload refuses to encode an AMP record
// without an MPP record. However, NewPayloadFromReader // without an MPP record. However, NewPayloadFromReader
// does allow decoding an AMP record without an MPP // does allow decoding an AMP record without an MPP
// record, since validation is done at a later stage. Do // record, since validation is done at a later stage. Do
// not report a bug for this case. // not report a bug for this case.
case errors.Is(err, route.ErrAMPMissingMPP):
return 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.NoError(t, err)
require.Equal(t, payload1, payload2) 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 // Otherwise, if this is the TLV payload, then we'll make a new stream
// to decode only what we need to make routing decisions. // to decode only what we need to make routing decisions.
case sphinx.PayloadTLV: case sphinx.PayloadTLV:
return NewPayloadFromReader(bytes.NewReader( return NewPayloadFromReader(
r.processedPacket.Payload.Payload, bytes.NewReader(r.processedPacket.Payload.Payload),
)) r.processedPacket.Action == sphinx.ExitNode,
)
default: default:
return nil, fmt.Errorf("unknown sphinx payload type: %v", return nil, fmt.Errorf("unknown sphinx payload type: %v",
+4 -2
View File
@@ -65,14 +65,16 @@ func TestSphinxHopIteratorForwardingInstructions(t *testing.T) {
}, },
expectedFwdInfo: expectedFwdInfo, expectedFwdInfo: expectedFwdInfo,
}, },
// A TLV payload, we can leave off the action as we'll always // A TLV payload, which includes the sphinx action as
// read the cid encoded. // cid may be zero for blinded routes (thus we require the
// action to signal whether we are at the final hop).
{ {
sphinxPacket: &sphinx.ProcessedPacket{ sphinxPacket: &sphinx.ProcessedPacket{
Payload: sphinx.HopPayload{ Payload: sphinx.HopPayload{
Type: sphinx.PayloadTLV, Type: sphinx.PayloadTLV,
Payload: b.Bytes(), Payload: b.Bytes(),
}, },
Action: sphinx.MoreHops,
}, },
expectedFwdInfo: expectedFwdInfo, 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 // NewPayloadFromReader builds a new Hop from the passed io.Reader. The reader
// should correspond to the bytes encapsulated in a TLV onion payload. // should correspond to the bytes encapsulated in a TLV onion payload. The
func NewPayloadFromReader(r io.Reader) (*Payload, error) { // final hop bool signals that this payload was the final packet parsed by
// sphinx.
func NewPayloadFromReader(r io.Reader, finalHop bool) (*Payload, error) {
var ( var (
cid uint64 cid uint64
amt uint64 amt uint64
@@ -165,8 +167,7 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {
// Validate whether the sender properly included or omitted tlv records // Validate whether the sender properly included or omitted tlv records
// in accordance with BOLT 04. // in accordance with BOLT 04.
nextHop := lnwire.NewShortChanIDFromInt(cid) err = ValidateParsedPayloadTypes(parsedTypes, finalHop)
err = ValidateParsedPayloadTypes(parsedTypes, nextHop)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -177,7 +178,7 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {
return nil, ErrInvalidPayload{ return nil, ErrInvalidPayload{
Type: *violatingType, Type: *violatingType,
Violation: RequiredViolation, Violation: RequiredViolation,
FinalHop: nextHop == Exit, FinalHop: finalHop,
} }
} }
@@ -210,7 +211,7 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {
return &Payload{ return &Payload{
FwdInfo: ForwardingInfo{ FwdInfo: ForwardingInfo{
NextHop: nextHop, NextHop: lnwire.NewShortChanIDFromInt(cid),
AmountToForward: lnwire.MilliSatoshi(amt), AmountToForward: lnwire.MilliSatoshi(amt),
OutgoingCTLV: cltv, 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 // boolean should be true if the payload was parsed for an exit hop. The
// requirements for this method are described in BOLT 04. // requirements for this method are described in BOLT 04.
func ValidateParsedPayloadTypes(parsedTypes tlv.TypeMap, func ValidateParsedPayloadTypes(parsedTypes tlv.TypeMap,
nextHop lnwire.ShortChannelID) error { isFinalHop bool) error {
isFinalHop := nextHop == Exit
_, hasAmt := parsedTypes[record.AmtOnionType] _, hasAmt := parsedTypes[record.AmtOnionType]
_, hasLockTime := parsedTypes[record.LockTimeOnionType] _, hasLockTime := parsedTypes[record.LockTimeOnionType]
_, 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. If nextHop != Exit, the // Hops that don't need forwarding info shouldn't have an amount TLV.
// sender must 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.
+154 -35
View File
@@ -24,6 +24,7 @@ const testUnknownRequiredType = 0x80
type decodePayloadTest struct { type decodePayloadTest struct {
name string name string
payload []byte payload []byte
isFinalHop bool
expErr error expErr error
expCustomRecords map[uint64][]byte expCustomRecords map[uint64][]byte
shouldHaveMPP bool shouldHaveMPP bool
@@ -36,18 +37,21 @@ type decodePayloadTest struct {
var decodePayloadTests = []decodePayloadTest{ var decodePayloadTests = []decodePayloadTest{
{ {
name: "final hop valid", name: "final hop valid",
payload: []byte{0x02, 0x00, 0x04, 0x00}, 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, payload: []byte{0x02, 0x00, 0x04, 0x00, 0x06, 0x08, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}, },
}, },
{ {
name: "final hop no amount", name: "final hop no amount",
payload: []byte{0x04, 0x00}, payload: []byte{0x04, 0x00},
isFinalHop: true,
expErr: hop.ErrInvalidPayload{ expErr: hop.ErrInvalidPayload{
Type: record.AmtOnionType, Type: record.AmtOnionType,
Violation: hop.OmittedViolation, 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, payload: []byte{0x04, 0x00, 0x06, 0x08, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}, },
@@ -66,8 +71,9 @@ var decodePayloadTests = []decodePayloadTest{
}, },
}, },
{ {
name: "final hop no expiry", name: "final hop no expiry",
payload: []byte{0x02, 0x00}, isFinalHop: true,
payload: []byte{0x02, 0x00},
expErr: hop.ErrInvalidPayload{ expErr: hop.ErrInvalidPayload{
Type: record.LockTimeOnionType, Type: record.LockTimeOnionType,
Violation: hop.OmittedViolation, 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, payload: []byte{0x02, 0x00, 0x06, 0x08, 0x01, 0x00, 0x00, 0x00,
0x00, 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, payload: []byte{0x02, 0x00, 0x04, 0x00, 0x06, 0x08, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 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{ payload: []byte{
0x02, 0x00, 0x04, 0x00, 0x02, 0x00, 0x04, 0x00,
testUnknownRequiredType, 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{ payload: []byte{
0x02, 0x00, 0x04, 0x00, 0x06, 0x08, 0x01, 0x00, 0x00, 0x02, 0x00, 0x04, 0x00, 0x06, 0x08, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@@ -122,8 +132,9 @@ var decodePayloadTests = []decodePayloadTest{
}, },
}, },
{ {
name: "required type zero final hop", name: "required type zero final hop",
payload: []byte{0x00, 0x00, 0x02, 0x00, 0x04, 0x00}, isFinalHop: true,
payload: []byte{0x00, 0x00, 0x02, 0x00, 0x04, 0x00},
expErr: hop.ErrInvalidPayload{ expErr: hop.ErrInvalidPayload{
Type: 0, Type: 0,
Violation: hop.RequiredViolation, 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, payload: []byte{0x00, 0x00, 0x02, 0x00, 0x04, 0x00, 0x06, 0x08,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 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, payload: []byte{0x00, 0x00, 0x02, 0x00, 0x04, 0x00, 0x06, 0x08,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}, },
@@ -153,8 +166,17 @@ var decodePayloadTests = []decodePayloadTest{
}, },
}, },
{ {
name: "required type in custom range", name: "required type in custom range",
payload: []byte{0x02, 0x00, 0x04, 0x00, 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, 0xfe, 0x00, 0x01, 0x00, 0x00, 0x02, 0x10, 0x11,
}, },
expCustomRecords: map[uint64][]byte{ 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, payload: []byte{0x02, 0x00, 0x04, 0x00, 0x06, 0x08, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}, },
expErr: nil, expErr: nil,
}, },
{ {
name: "valid final hop", name: "valid final hop",
payload: []byte{0x02, 0x00, 0x04, 0x00}, isFinalHop: true,
expErr: nil, payload: []byte{0x02, 0x00, 0x04, 0x00},
expErr: nil,
}, },
{ {
name: "intermediate hop with mpp", name: "intermediate hop with mpp",
isFinalHop: false,
payload: []byte{ payload: []byte{
// amount // amount
0x02, 0x00, 0x02, 0x00,
@@ -198,7 +223,8 @@ var decodePayloadTests = []decodePayloadTest{
}, },
}, },
{ {
name: "intermediate hop with amp", name: "intermediate hop with amp",
isFinalHop: false,
payload: []byte{ payload: []byte{
// amount // amount
0x02, 0x00, 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{ payload: []byte{
// amount // amount
0x02, 0x00, 0x02, 0x00,
// cltv // cltv
0x04, 0x00, 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 // encrypted data
0x0a, 0x03, 0x03, 0x02, 0x01, 0x0a, 0x03, 0x03, 0x02, 0x01,
}, },
shouldHaveEncData: true, shouldHaveEncData: true,
}, },
{ {
name: "intermediate hop with blinding point", name: "intermediate hop with blinding point",
isFinalHop: false,
payload: append([]byte{ payload: append([]byte{
// amount // encrypted data
0x02, 0x00, 0x0a, 0x03, 0x03, 0x02, 0x01,
// cltv
0x04, 0x00,
// blinding point (type / length) // blinding point (type / length)
0x0c, 0x21, 0x0c, 0x21,
}, },
@@ -254,9 +291,11 @@ var decodePayloadTests = []decodePayloadTest{
testPubKey.SerializeCompressed()..., testPubKey.SerializeCompressed()...,
), ),
shouldHaveBlinding: true, shouldHaveBlinding: true,
shouldHaveEncData: true,
}, },
{ {
name: "final hop with mpp", name: "final hop with mpp",
isFinalHop: true,
payload: []byte{ payload: []byte{
// amount // amount
0x02, 0x00, 0x02, 0x00,
@@ -274,7 +313,8 @@ var decodePayloadTests = []decodePayloadTest{
shouldHaveMPP: true, shouldHaveMPP: true,
}, },
{ {
name: "final hop with amp", name: "final hop with amp",
isFinalHop: true,
payload: []byte{ payload: []byte{
// amount // amount
0x02, 0x00, 0x02, 0x00,
@@ -298,7 +338,8 @@ var decodePayloadTests = []decodePayloadTest{
shouldHaveAMP: true, shouldHaveAMP: true,
}, },
{ {
name: "final hop with metadata", name: "final hop with metadata",
isFinalHop: true,
payload: []byte{ payload: []byte{
// amount // amount
0x02, 0x00, 0x02, 0x00,
@@ -310,7 +351,8 @@ var decodePayloadTests = []decodePayloadTest{
shouldHaveMetadata: true, shouldHaveMetadata: true,
}, },
{ {
name: "final hop with total amount", name: "final hop with total amount",
isFinalHop: true,
payload: []byte{ payload: []byte{
// amount // amount
0x02, 0x00, 0x02, 0x00,
@@ -321,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
@@ -361,7 +478,9 @@ func testDecodeHopPayloadValidation(t *testing.T, test decodePayloadTest) {
testChildIndex = uint32(9) 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) { if !reflect.DeepEqual(test.expErr, err) {
t.Fatalf("expected error mismatch, want: %v, got: %v", t.Fatalf("expected error mismatch, want: %v, got: %v",
test.expErr, err) test.expErr, err)
+105 -14
View File
@@ -38,6 +38,13 @@ var (
// ErrAMPMissingMPP is returned when the caller tries to attach an AMP // ErrAMPMissingMPP is returned when the caller tries to attach an AMP
// record but no MPP record is presented for the final hop. // record but no MPP record is presented for the final hop.
ErrAMPMissingMPP = errors.New("cannot send AMP without MPP record") 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 // 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 // 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 // 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 // include the required routing fields, as well as serializing any of the
// passed optional TLVRecords. nextChanID is the unique channel ID that // passed optional TLVRecords. nextChanID is the unique channel ID that
// references the _outgoing_ channel ID that follows this hop. This field // references the _outgoing_ channel ID that follows this hop. The lastHop bool
// follows the same semantics as the NextAddress field in the onion: it should // is used to signal whether this hop is the final hop in a route. Previously,
// be set to zero to indicate the terminal hop. // a zero nextChanID would be used for this purpose, but with the addition of
func (h *Hop) PackHopPayload(w io.Writer, nextChanID uint64) error { // 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 // If this is a legacy payload, then we'll exit here as this method
// shouldn't be called. // shouldn't be called.
if h.LegacyPayload == true { if h.LegacyPayload == true {
@@ -189,8 +200,28 @@ func (h *Hop) PackHopPayload(w io.Writer, nextChanID uint64) error {
var records []tlv.Record var records []tlv.Record
// Hops that are not part of a blinded path will have an amount and // 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 CLTV expiry field. In a blinded route (where encrypted data is
// a blinded route, so the TLV should not be included. // 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) amt := uint64(h.AmtToForward)
if amt != 0 { if amt != 0 {
records = append( 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, // Validate channel TLV is present as expected based on location in
// but present for all others. // route and whether this hop is blinded.
// err := validateNextChanID(nextChanID != 0, isBlinded, finalHop)
// TODO(conner): test using hop.Exit once available if err != nil {
return fmt.Errorf("%w: channel id: %v", err, nextChanID)
}
if nextChanID != 0 { if nextChanID != 0 {
records = append(records, records = append(records,
record.NewNextHopIDRecord(&nextChanID), 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 // attach it to the final hop. Otherwise the route was constructed
// incorrectly. // incorrectly.
if h.MPP != nil { if h.MPP != nil {
if nextChanID == 0 { if finalHop {
records = append(records, h.MPP.Record()) records = append(records, h.MPP.Record())
} else { } else {
return ErrIntermediateMPPHop return ErrIntermediateMPPHop
@@ -280,6 +314,62 @@ func (h *Hop) PackHopPayload(w io.Writer, nextChanID uint64) error {
return tlvStream.Encode(w) 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 // Size returns the total size this hop's payload would take up in the onion
// packet. // packet.
func (h *Hop) PayloadSize(nextChanID uint64) uint64 { 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" // If we aren't on the last hop, then we set the "next address"
// field to be the channel that directly follows it. // 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 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 // channel should be forwarded to so we can construct a
// valid payload. // valid payload.
var b bytes.Buffer var b bytes.Buffer
err := hop.PackHopPayload(&b, nextHop) err := hop.PackHopPayload(&b, nextHop, finalHop)
if err != nil { if err != nil {
return nil, err 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 // Encoding an MPP record to an intermediate hop should result in a
// failure. // failure.
var b bytes.Buffer var b bytes.Buffer
err := hop.PackHopPayload(&b, 2) err := hop.PackHopPayload(&b, 2, false)
if err != ErrIntermediateMPPHop { if err != ErrIntermediateMPPHop {
t.Fatalf("expected err: %v, got: %v", t.Fatalf("expected err: %v, got: %v",
ErrIntermediateMPPHop, err) ErrIntermediateMPPHop, err)
@@ -113,7 +113,7 @@ func TestMPPHop(t *testing.T) {
// Encoding an MPP record to a final hop should be successful. // Encoding an MPP record to a final hop should be successful.
b.Reset() b.Reset()
err = hop.PackHopPayload(&b, 0) err = hop.PackHopPayload(&b, 0, true)
if err != nil { if err != nil {
t.Fatalf("expected err: %v, got: %v", nil, err) 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 // Encoding an AMP record to an intermediate hop w/o an MPP record
// should result in a failure. // should result in a failure.
var b bytes.Buffer var b bytes.Buffer
err := hop.PackHopPayload(&b, 2) err := hop.PackHopPayload(&b, 2, false)
if err != ErrAMPMissingMPP { if err != ErrAMPMissingMPP {
t.Fatalf("expected err: %v, got: %v", t.Fatalf("expected err: %v, got: %v",
ErrAMPMissingMPP, err) 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 // Encoding an AMP record to a final hop w/o an MPP record should result
// in a failure. // in a failure.
b.Reset() b.Reset()
err = hop.PackHopPayload(&b, 0) err = hop.PackHopPayload(&b, 0, true)
if err != ErrAMPMissingMPP { if err != ErrAMPMissingMPP {
t.Fatalf("expected err: %v, got: %v", t.Fatalf("expected err: %v, got: %v",
ErrAMPMissingMPP, err) ErrAMPMissingMPP, err)
@@ -154,24 +154,118 @@ func TestAMPHop(t *testing.T) {
// successful. // successful.
hop.MPP = record.NewMPP(testAmt, testAddr) hop.MPP = record.NewMPP(testAmt, testAddr)
b.Reset() b.Reset()
err = hop.PackHopPayload(&b, 0) err = hop.PackHopPayload(&b, 0, true)
if err != nil { if err != nil {
t.Fatalf("expected err: %v, got: %v", nil, err) t.Fatalf("expected err: %v, got: %v", nil, err)
} }
} }
// 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, 2) 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
@@ -182,21 +276,22 @@ func TestPayloadSize(t *testing.T) {
hops := []*Hop{ hops := []*Hop{
{ {
PubKeyBytes: testPubKeyBytes, PubKeyBytes: testPubKeyBytes,
AmtToForward: 1000, AmtToForward: 2000,
OutgoingTimeLock: 600000, OutgoingTimeLock: 600000,
ChannelID: 3432483437438, ChannelID: 3432483437438,
LegacyPayload: true, LegacyPayload: true,
}, },
{ {
PubKeyBytes: testPubKeyBytes, PubKeyBytes: testPubKeyBytes,
AmtToForward: 1200, AmtToForward: 1500,
OutgoingTimeLock: 700000, OutgoingTimeLock: 700000,
ChannelID: 63584534844, ChannelID: 63584534844,
}, },
{ {
PubKeyBytes: testPubKeyBytes, PubKeyBytes: testPubKeyBytes,
AmtToForward: 1200, AmtToForward: 1000,
OutgoingTimeLock: 700000, OutgoingTimeLock: 700000,
ChannelID: 51784534844,
MPP: record.NewMPP(500, [32]byte{}), MPP: record.NewMPP(500, [32]byte{}),
AMP: record.NewAMP( AMP: record.NewAMP(
[32]byte{}, [32]byte{}, 8, [32]byte{}, [32]byte{}, 8,
+20 -16
View File
@@ -3471,10 +3471,11 @@ func TestSendToRouteSkipTempErrSuccess(t *testing.T) {
// Create a simple 1-hop route. // Create a simple 1-hop route.
hops := []*route.Hop{ hops := []*route.Hop{
{ {
ChannelID: 1, ChannelID: 1,
PubKeyBytes: node.PubKeyBytes, PubKeyBytes: node.PubKeyBytes,
AmtToForward: payAmt, AmtToForward: payAmt,
MPP: record.NewMPP(payAmt, [32]byte{}), OutgoingTimeLock: 120,
MPP: record.NewMPP(payAmt, [32]byte{}),
}, },
} }
rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops) rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops)
@@ -3607,10 +3608,11 @@ func TestSendToRouteSkipTempErrTempFailure(t *testing.T) {
// Create a simple 1-hop route. // Create a simple 1-hop route.
hops := []*route.Hop{ hops := []*route.Hop{
{ {
ChannelID: 1, ChannelID: 1,
PubKeyBytes: node.PubKeyBytes, PubKeyBytes: node.PubKeyBytes,
AmtToForward: payAmt, AmtToForward: payAmt,
MPP: record.NewMPP(payAmt, [32]byte{}), OutgoingTimeLock: 120,
MPP: record.NewMPP(payAmt, [32]byte{}),
}, },
} }
rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops) rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops)
@@ -3688,10 +3690,11 @@ func TestSendToRouteSkipTempErrPermanentFailure(t *testing.T) {
// Create a simple 1-hop route. // Create a simple 1-hop route.
hops := []*route.Hop{ hops := []*route.Hop{
{ {
ChannelID: 1, ChannelID: 1,
PubKeyBytes: node.PubKeyBytes, PubKeyBytes: node.PubKeyBytes,
AmtToForward: payAmt, AmtToForward: payAmt,
MPP: record.NewMPP(payAmt, [32]byte{}), OutgoingTimeLock: 120,
MPP: record.NewMPP(payAmt, [32]byte{}),
}, },
} }
rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops) rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops)
@@ -3773,10 +3776,11 @@ func TestSendToRouteTempFailure(t *testing.T) {
// Create a simple 1-hop route. // Create a simple 1-hop route.
hops := []*route.Hop{ hops := []*route.Hop{
{ {
ChannelID: 1, ChannelID: 1,
PubKeyBytes: node.PubKeyBytes, PubKeyBytes: node.PubKeyBytes,
AmtToForward: payAmt, AmtToForward: payAmt,
MPP: record.NewMPP(payAmt, [32]byte{}), OutgoingTimeLock: 120,
MPP: record.NewMPP(payAmt, [32]byte{}),
}, },
} }
rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops) rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops)