Skip to content
94 changes: 73 additions & 21 deletions htlcswitch/hop/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,56 @@ import (
"fmt"
"io"

"github.com/lightningnetwork/lightning-onion"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/tlv"
)

// PayloadViolation is an enum encapsulating the possible invalid payload
// violations that can occur when processing or validating a payload.
type PayloadViolation byte

const (
// OmittedViolation indicates that a type was expected to be found the
// payload but was absent.
OmittedViolation PayloadViolation = iota

// IncludedViolation indicates that a type was expected to be omitted
// from the payload but was present.
IncludedViolation

// RequiredViolation indicates that an unknown even type was found in
// the payload that we could not process.
RequiredViolation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Required and Omitted sound like the same thing. Is the real difference that one happens during tlv decode and the other one on the higher level? If that is indeed the case, maybe the names should reflect that. Or just have one of the two.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no they are different, required means that the sender required us to understand a record, but we don't. omitted means that the receiver expected a field that the sender didn't include.

)

// String returns a human-readable description of the violation as a verb.
func (v PayloadViolation) String() string {
switch v {
case OmittedViolation:
return "omitted"

case IncludedViolation:
return "included"

case RequiredViolation:
return "required"

default:
return "unknown violation"
}
}

// ErrInvalidPayload is an error returned when a parsed onion payload either
// included or omitted incorrect records for a particular hop type.
type ErrInvalidPayload struct {
// Type the record's type that cause the violation.
Type tlv.Type

// Ommitted if true, signals that the sender did not include the record.
// Otherwise, the sender included the record when it shouldn't have.
Omitted bool
// Violation is an enum indicating the type of violation detected in
// processing Type.
Violation PayloadViolation

// FinalHop if true, indicates that the violation is for the final hop
// in the route (identified by next hop id), otherwise the violation is
Expand All @@ -33,13 +68,9 @@ func (e ErrInvalidPayload) Error() string {
if e.FinalHop {
hopType = "final"
}
violation := "included"
if e.Omitted {
violation = "omitted"
}

return fmt.Sprintf("onion payload for %s hop %s record with type %d",
hopType, violation, e.Type)
return fmt.Sprintf("onion payload for %s hop %v record with type %d",
hopType, e.Violation, e.Type)
}

// Payload encapsulates all information delivered to a hop in an onion payload.
Expand Down Expand Up @@ -87,13 +118,34 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {

parsedTypes, err := tlvStream.DecodeWithParsedTypes(r)
if err != nil {
// Promote any required type failures into ErrInvalidPayload.
if e, required := err.(tlv.ErrUnknownRequiredType); required {
// If the parser returned an unknown required type
// failure, we'll first check that the payload is
// properly formed according to our known set of
// constraints. If an error is discovered, this
// overrides the required type failure.
nextHop := lnwire.NewShortChanIDFromInt(cid)
err = ValidateParsedPayloadTypes(parsedTypes, nextHop)
if err != nil {
return nil, err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this a bit too much? Always returning RequiredViolation here doesn't seem to be bad. Especially because it also requires changes to the tlv parser.

Copy link
Contributor Author

@cfromknecht cfromknecht Oct 29, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is in some sense overkill, but i think an accurate log message will help debug if things go wrong. perhaps another solution is to not log the final/intermediate info when encountering a required violation? however, many of the new records being added are specific to the hop type, which would make that information more useful.

}

// Otherwise the known constraints were applied
// successfully, report the invalid type failure
// returned by the parser.
return nil, ErrInvalidPayload{
Type: tlv.Type(e),
Violation: RequiredViolation,
FinalHop: nextHop == Exit,
}
}
return nil, err
}

nextHop := lnwire.NewShortChanIDFromInt(cid)

// Validate whether the sender properly included or omitted tlv records
// in accordance with BOLT 04.
nextHop := lnwire.NewShortChanIDFromInt(cid)
err = ValidateParsedPayloadTypes(parsedTypes, nextHop)
if err != nil {
return nil, err
Expand Down Expand Up @@ -133,27 +185,27 @@ func ValidateParsedPayloadTypes(parsedTypes tlv.TypeSet,
// All hops must include an amount to forward.
case !hasAmt:
return ErrInvalidPayload{
Type: record.AmtOnionType,
Omitted: true,
FinalHop: isFinalHop,
Type: record.AmtOnionType,
Violation: OmittedViolation,
FinalHop: isFinalHop,
}

// All hops must include a cltv expiry.
case !hasLockTime:
return ErrInvalidPayload{
Type: record.LockTimeOnionType,
Omitted: true,
FinalHop: isFinalHop,
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:
return ErrInvalidPayload{
Type: record.NextHopOnionType,
Omitted: false,
FinalHop: true,
Type: record.NextHopOnionType,
Violation: IncludedViolation,
FinalHop: true,
}
}

Expand Down
91 changes: 76 additions & 15 deletions htlcswitch/hop/payload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,23 @@ type decodePayloadTest struct {
}

var decodePayloadTests = []decodePayloadTest{
{
name: "final hop valid",
payload: []byte{0x02, 0x00, 0x04, 0x00},
},
{
name: "intermediate hop valid",
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},
expErr: hop.ErrInvalidPayload{
Type: record.AmtOnionType,
Omitted: true,
FinalHop: true,
Type: record.AmtOnionType,
Violation: hop.OmittedViolation,
FinalHop: true,
},
},
{
Expand All @@ -31,18 +41,18 @@ var decodePayloadTests = []decodePayloadTest{
0x00, 0x00, 0x00, 0x00,
},
expErr: hop.ErrInvalidPayload{
Type: record.AmtOnionType,
Omitted: true,
FinalHop: false,
Type: record.AmtOnionType,
Violation: hop.OmittedViolation,
FinalHop: false,
},
},
{
name: "final hop no expiry",
payload: []byte{0x02, 0x00},
expErr: hop.ErrInvalidPayload{
Type: record.LockTimeOnionType,
Omitted: true,
FinalHop: true,
Type: record.LockTimeOnionType,
Violation: hop.OmittedViolation,
FinalHop: true,
},
},
{
Expand All @@ -51,9 +61,9 @@ var decodePayloadTests = []decodePayloadTest{
0x00, 0x00, 0x00, 0x00,
},
expErr: hop.ErrInvalidPayload{
Type: record.LockTimeOnionType,
Omitted: true,
FinalHop: false,
Type: record.LockTimeOnionType,
Violation: hop.OmittedViolation,
FinalHop: false,
},
},
{
Expand All @@ -62,9 +72,60 @@ var decodePayloadTests = []decodePayloadTest{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
expErr: hop.ErrInvalidPayload{
Type: record.NextHopOnionType,
Omitted: false,
FinalHop: true,
Type: record.NextHopOnionType,
Violation: hop.IncludedViolation,
FinalHop: true,
},
},
{
name: "required type after omitted hop id",
payload: []byte{0x02, 0x00, 0x04, 0x00, 0x08, 0x00},
expErr: hop.ErrInvalidPayload{
Type: 8,
Violation: hop.RequiredViolation,
FinalHop: true,
},
},
{
name: "required type after included hop id",
payload: []byte{0x02, 0x00, 0x04, 0x00, 0x06, 0x08, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00,
},
expErr: hop.ErrInvalidPayload{
Type: 8,
Violation: hop.RequiredViolation,
FinalHop: false,
},
},
{
name: "required type zero final hop",
payload: []byte{0x00, 0x00, 0x02, 0x00, 0x04, 0x00},
expErr: hop.ErrInvalidPayload{
Type: 0,
Violation: hop.RequiredViolation,
FinalHop: true,
},
},
{
name: "required type zero final hop zero sid",
payload: []byte{0x00, 0x00, 0x02, 0x00, 0x04, 0x00, 0x06, 0x08,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
expErr: hop.ErrInvalidPayload{
Type: 6,
Violation: hop.IncludedViolation,
FinalHop: true,
},
},
{
name: "required type zero intermediate hop",
payload: []byte{0x00, 0x00, 0x02, 0x00, 0x04, 0x00, 0x06, 0x08,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
expErr: hop.ErrInvalidPayload{
Type: 0,
Violation: hop.RequiredViolation,
FinalHop: false,
},
},
}
Expand Down
19 changes: 15 additions & 4 deletions htlcswitch/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -2646,12 +2646,23 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
fwdInfo, err := chanIterator.ForwardingInstructions()
if err != nil {
// If we're unable to process the onion payload, or we
// we received malformed TLV stream, then we should
// send an error back to the caller so the HTLC can be
// canceled.
// received invalid onion payload failure, then we
// should send an error back to the caller so the HTLC
// can be canceled.
var failedType uint64
if e, ok := err.(hop.ErrInvalidPayload); ok {
failedType = uint64(e.Type)
}

// TODO: currently none of the test unit infrastructure
// is setup to handle TLV payloads, so testing this
// would require implementing a separate mock iterator
// for TLV payloads that also supports injecting invalid
// payloads. Deferring this non-trival effort till a
// later date
l.sendHTLCError(
pd.HtlcIndex,
lnwire.NewInvalidOnionVersion(onionBlob[:]),
lnwire.NewInvalidOnionPayload(failedType, 0),
obfuscator, pd.SourceRef,
)
needUpdate = true
Expand Down
Loading