Merge pull request #3470 from cfromknecht/invalid-onion-payload
htlcswitch+lnwire: invalid onion payload
This commit is contained in:
commit
acd8a6e302
@ -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
|
||||
)
|
||||
|
||||
// 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
|
||||
@ -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.
|
||||
@ -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
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -133,17 +185,17 @@ 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
|
||||
@ -151,9 +203,9 @@ func ValidateParsedPayloadTypes(parsedTypes tlv.TypeSet,
|
||||
// inclusion at intermediate hops directly.
|
||||
case isFinalHop && hasNextHop:
|
||||
return ErrInvalidPayload{
|
||||
Type: record.NextHopOnionType,
|
||||
Omitted: false,
|
||||
FinalHop: true,
|
||||
Type: record.NextHopOnionType,
|
||||
Violation: IncludedViolation,
|
||||
FinalHop: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -2645,12 +2645,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
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
)
|
||||
|
||||
// FailureMessage represents the onion failure object identified by its unique
|
||||
@ -78,6 +79,7 @@ const (
|
||||
CodeFinalIncorrectCltvExpiry FailCode = 18
|
||||
CodeFinalIncorrectHtlcAmount FailCode = 19
|
||||
CodeExpiryTooFar FailCode = 21
|
||||
CodeInvalidOnionPayload = FlagPerm | 22
|
||||
)
|
||||
|
||||
// String returns the string representation of the failure code.
|
||||
@ -149,6 +151,9 @@ func (c FailCode) String() string {
|
||||
case CodeExpiryTooFar:
|
||||
return "ExpiryTooFar"
|
||||
|
||||
case CodeInvalidOnionPayload:
|
||||
return "InvalidOnionPayload"
|
||||
|
||||
default:
|
||||
return "<unknown>"
|
||||
}
|
||||
@ -1117,6 +1122,66 @@ func (f *FailExpiryTooFar) Error() string {
|
||||
return f.Code().String()
|
||||
}
|
||||
|
||||
// InvalidOnionPayload is returned if the hop could not process the TLV payload
|
||||
// enclosed in the onion.
|
||||
type InvalidOnionPayload struct {
|
||||
// Type is the TLV type that caused the specific failure.
|
||||
Type uint64
|
||||
|
||||
// Offset is the byte offset within the payload where the failure
|
||||
// occurred.
|
||||
Offset uint16
|
||||
}
|
||||
|
||||
// NewInvalidOnionPayload initializes a new InvalidOnionPayload failure.
|
||||
func NewInvalidOnionPayload(typ uint64, offset uint16) *InvalidOnionPayload {
|
||||
return &InvalidOnionPayload{
|
||||
Type: typ,
|
||||
Offset: offset,
|
||||
}
|
||||
}
|
||||
|
||||
// Code returns the failure unique code.
|
||||
//
|
||||
// NOTE: Part of the FailureMessage interface.
|
||||
func (f *InvalidOnionPayload) Code() FailCode {
|
||||
return CodeInvalidOnionPayload
|
||||
}
|
||||
|
||||
// Returns a human readable string describing the target FailureMessage.
|
||||
//
|
||||
// NOTE: Implements the error interface.
|
||||
func (f *InvalidOnionPayload) Error() string {
|
||||
return fmt.Sprintf("%v(type=%v, offset=%d)",
|
||||
f.Code(), f.Type, f.Offset)
|
||||
}
|
||||
|
||||
// Decode decodes the failure from bytes stream.
|
||||
//
|
||||
// NOTE: Part of the Serializable interface.
|
||||
func (f *InvalidOnionPayload) Decode(r io.Reader, pver uint32) error {
|
||||
var buf [8]byte
|
||||
typ, err := tlv.ReadVarInt(r, &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.Type = typ
|
||||
|
||||
return ReadElements(r, &f.Offset)
|
||||
}
|
||||
|
||||
// Encode writes the failure in bytes stream.
|
||||
//
|
||||
// NOTE: Part of the Serializable interface.
|
||||
func (f *InvalidOnionPayload) Encode(w io.Writer, pver uint32) error {
|
||||
var buf [8]byte
|
||||
if err := tlv.WriteVarInt(w, f.Type, &buf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return WriteElements(w, f.Offset)
|
||||
}
|
||||
|
||||
// DecodeFailure decodes, validates, and parses the lnwire onion failure, for
|
||||
// the provided protocol version.
|
||||
func DecodeFailure(r io.Reader, pver uint32) (FailureMessage, error) {
|
||||
@ -1298,6 +1363,9 @@ func makeEmptyOnionError(code FailCode) (FailureMessage, error) {
|
||||
case CodeExpiryTooFar:
|
||||
return &FailExpiryTooFar{}, nil
|
||||
|
||||
case CodeInvalidOnionPayload:
|
||||
return &InvalidOnionPayload{}, nil
|
||||
|
||||
default:
|
||||
return nil, errors.Errorf("unknown error code: %v", code)
|
||||
}
|
||||
|
@ -16,6 +16,8 @@ var (
|
||||
testAmount = MilliSatoshi(1)
|
||||
testCtlvExpiry = uint32(2)
|
||||
testFlags = uint16(2)
|
||||
testType = uint64(3)
|
||||
testOffset = uint16(24)
|
||||
sig, _ = NewSigFromSignature(testSig)
|
||||
testChannelUpdate = ChannelUpdate{
|
||||
Signature: sig,
|
||||
@ -50,6 +52,7 @@ var onionFailures = []FailureMessage{
|
||||
NewChannelDisabled(testFlags, testChannelUpdate),
|
||||
NewFinalIncorrectCltvExpiry(testCtlvExpiry),
|
||||
NewFinalIncorrectHtlcAmount(testAmount),
|
||||
NewInvalidOnionPayload(testType, testOffset),
|
||||
}
|
||||
|
||||
// TestEncodeDecodeCode tests the ability of onion errors to be properly encoded
|
||||
|
@ -265,7 +265,17 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate(
|
||||
|
||||
// All nodes up to the failing pair must have forwarded
|
||||
// successfully.
|
||||
if errorSourceIdx > 2 {
|
||||
if errorSourceIdx > 1 {
|
||||
i.successPairRange(route, 0, errorSourceIdx-2)
|
||||
}
|
||||
}
|
||||
|
||||
reportNode := func() {
|
||||
// Fail only the node that reported the failure.
|
||||
i.failNode(route, errorSourceIdx)
|
||||
|
||||
// Other preceding channels in the route forwarded correctly.
|
||||
if errorSourceIdx > 1 {
|
||||
i.successPairRange(route, 0, errorSourceIdx-2)
|
||||
}
|
||||
}
|
||||
@ -302,6 +312,14 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate(
|
||||
|
||||
reportOutgoing()
|
||||
|
||||
// If InvalidOnionPayload is received, we penalize only the reporting
|
||||
// node. We know the preceding hop didn't corrupt the onion, since the
|
||||
// reporting node is able to send the failure. We assume that we
|
||||
// constructed a valid onion payload and that the failure is most likely
|
||||
// an unknown required type or a bug in their implementation.
|
||||
case *lnwire.InvalidOnionPayload:
|
||||
reportNode()
|
||||
|
||||
// If the next hop in the route wasn't known or offline, we'll only
|
||||
// penalize the channel set which we attempted to route over. This is
|
||||
// conservative, and it can handle faulty channels between nodes
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
@ -47,6 +48,11 @@ func getTestPair(from, to int) DirectedNodePair {
|
||||
return NewDirectedNodePair(hops[from], hops[to])
|
||||
}
|
||||
|
||||
func getPolicyFailure(from, to int) *DirectedNodePair {
|
||||
pair := getTestPair(from, to)
|
||||
return &pair
|
||||
}
|
||||
|
||||
type resultTestCase struct {
|
||||
name string
|
||||
route *route.Route
|
||||
@ -169,6 +175,97 @@ var resultTestCases = []resultTestCase{
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Tests that a fee insufficient failure to an intermediate hop with
|
||||
// index 2 results in the first hop marked as success, and then a
|
||||
// bidirectional failure for the incoming channel. It should also result
|
||||
// in a policy failure for the outgoing hop.
|
||||
{
|
||||
name: "fail fee insufficient intermediate",
|
||||
route: &routeFourHop,
|
||||
failureSrcIdx: 2,
|
||||
failure: lnwire.NewFeeInsufficient(0, lnwire.ChannelUpdate{}),
|
||||
|
||||
expectedResult: &interpretedResult{
|
||||
pairResults: map[DirectedNodePair]pairResult{
|
||||
getTestPair(0, 1): {
|
||||
success: true,
|
||||
},
|
||||
getTestPair(1, 2): {},
|
||||
getTestPair(2, 1): {},
|
||||
},
|
||||
policyFailure: getPolicyFailure(2, 3),
|
||||
},
|
||||
},
|
||||
|
||||
// Tests an invalid onion payload from a final hop. The final hop should
|
||||
// be failed while the proceeding hops are reproed as successes. The
|
||||
// failure is terminal since the receiver can't process our onion.
|
||||
{
|
||||
name: "fail invalid onion payload final hop",
|
||||
route: &routeFourHop,
|
||||
failureSrcIdx: 4,
|
||||
failure: lnwire.NewInvalidOnionPayload(0, 0),
|
||||
|
||||
expectedResult: &interpretedResult{
|
||||
pairResults: map[DirectedNodePair]pairResult{
|
||||
getTestPair(0, 1): {
|
||||
success: true,
|
||||
},
|
||||
getTestPair(1, 2): {
|
||||
success: true,
|
||||
},
|
||||
getTestPair(2, 3): {
|
||||
success: true,
|
||||
},
|
||||
getTestPair(4, 3): {},
|
||||
},
|
||||
finalFailureReason: &reasonError,
|
||||
nodeFailure: &hops[4],
|
||||
},
|
||||
},
|
||||
|
||||
// Tests an invalid onion payload from an intermediate hop. Only the
|
||||
// reporting node should be failed. The failure is non-terminal since we
|
||||
// can still try other paths.
|
||||
{
|
||||
name: "fail invalid onion payload intermediate",
|
||||
route: &routeFourHop,
|
||||
failureSrcIdx: 3,
|
||||
failure: lnwire.NewInvalidOnionPayload(0, 0),
|
||||
|
||||
expectedResult: &interpretedResult{
|
||||
pairResults: map[DirectedNodePair]pairResult{
|
||||
getTestPair(0, 1): {
|
||||
success: true,
|
||||
},
|
||||
getTestPair(1, 2): {
|
||||
success: true,
|
||||
},
|
||||
getTestPair(3, 2): {},
|
||||
getTestPair(3, 4): {},
|
||||
},
|
||||
nodeFailure: &hops[3],
|
||||
},
|
||||
},
|
||||
|
||||
// Tests an invalid onion payload in a direct peer that is also the
|
||||
// final hop. The final node should be failed and the error is terminal
|
||||
// since the remote node can't process our onion.
|
||||
{
|
||||
name: "fail invalid onion payload direct",
|
||||
route: &routeOneHop,
|
||||
failureSrcIdx: 1,
|
||||
failure: lnwire.NewInvalidOnionPayload(0, 0),
|
||||
|
||||
expectedResult: &interpretedResult{
|
||||
pairResults: map[DirectedNodePair]pairResult{
|
||||
getTestPair(1, 0): {},
|
||||
},
|
||||
finalFailureReason: &reasonError,
|
||||
nodeFailure: &hops[1],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// TestResultInterpretation executes a list of test cases that test the result
|
||||
@ -192,7 +289,8 @@ func TestResultInterpretation(t *testing.T) {
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(i, expected) {
|
||||
t.Fatal("unexpected result")
|
||||
t.Fatalf("unexpected result\nwant: %v\ngot: %v",
|
||||
spew.Sdump(expected), spew.Sdump(i))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -162,6 +162,7 @@ func (s *Stream) decode(r io.Reader, parsedTypes TypeSet) (TypeSet, error) {
|
||||
var (
|
||||
typ Type
|
||||
min Type
|
||||
firstFail *Type
|
||||
recordIdx int
|
||||
overflow bool
|
||||
)
|
||||
@ -176,7 +177,10 @@ func (s *Stream) decode(r io.Reader, parsedTypes TypeSet) (TypeSet, error) {
|
||||
// We'll silence an EOF when zero bytes remain, meaning the
|
||||
// stream was cleanly encoded.
|
||||
case err == io.EOF:
|
||||
return parsedTypes, nil
|
||||
if firstFail == nil {
|
||||
return parsedTypes, nil
|
||||
}
|
||||
return parsedTypes, ErrUnknownRequiredType(*firstFail)
|
||||
|
||||
// Other unexpected errors.
|
||||
case err != nil:
|
||||
@ -243,7 +247,27 @@ func (s *Stream) decode(r io.Reader, parsedTypes TypeSet) (TypeSet, error) {
|
||||
// This record type is unknown to the stream, fail if the type
|
||||
// is even meaning that we are required to understand it.
|
||||
case typ%2 == 0:
|
||||
return nil, ErrUnknownRequiredType(typ)
|
||||
// We'll fail immediately in the case that we aren't
|
||||
// tracking the set of parsed types.
|
||||
if parsedTypes == nil {
|
||||
return nil, ErrUnknownRequiredType(typ)
|
||||
}
|
||||
|
||||
// Otherwise, we'll track the first such failure and
|
||||
// allow parsing to continue. If no other types of
|
||||
// errors are encountered, the first failure will be
|
||||
// returned as an ErrUnknownRequiredType so that the
|
||||
// full set of included types can be returned.
|
||||
if firstFail == nil {
|
||||
failTyp := typ
|
||||
firstFail = &failTyp
|
||||
}
|
||||
|
||||
// With the failure type recorded, we'll simply discard
|
||||
// the remainder of the record as if it were optional.
|
||||
// The first failure will be returned after reaching the
|
||||
// stopping condition.
|
||||
fallthrough
|
||||
|
||||
// Otherwise, the record type is unknown and is odd, discard the
|
||||
// number of bytes specified by length.
|
||||
|
@ -2,50 +2,106 @@ package tlv_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
)
|
||||
|
||||
type parsedTypeTest struct {
|
||||
name string
|
||||
encode []tlv.Type
|
||||
decode []tlv.Type
|
||||
expErr error
|
||||
}
|
||||
|
||||
// TestParsedTypes asserts that a Stream will properly return the set of types
|
||||
// that it encounters when the type is known-and-decoded or unknown-and-ignored.
|
||||
func TestParsedTypes(t *testing.T) {
|
||||
const (
|
||||
knownType = 1
|
||||
unknownType = 3
|
||||
firstReqType = 0
|
||||
knownType = 1
|
||||
unknownType = 3
|
||||
secondReqType = 4
|
||||
)
|
||||
|
||||
// Construct a stream that will encode two types, one that will be known
|
||||
// to the decoder and another that will be unknown.
|
||||
encStream := tlv.MustNewStream(
|
||||
tlv.MakePrimitiveRecord(knownType, new(uint64)),
|
||||
tlv.MakePrimitiveRecord(unknownType, new(uint64)),
|
||||
)
|
||||
tests := []parsedTypeTest{
|
||||
{
|
||||
name: "known optional and unknown optional",
|
||||
encode: []tlv.Type{knownType, unknownType},
|
||||
decode: []tlv.Type{knownType},
|
||||
},
|
||||
{
|
||||
name: "unknown required and known optional",
|
||||
encode: []tlv.Type{firstReqType, knownType},
|
||||
decode: []tlv.Type{knownType},
|
||||
expErr: tlv.ErrUnknownRequiredType(firstReqType),
|
||||
},
|
||||
{
|
||||
name: "unknown required and unknown optional",
|
||||
encode: []tlv.Type{unknownType, secondReqType},
|
||||
expErr: tlv.ErrUnknownRequiredType(secondReqType),
|
||||
},
|
||||
{
|
||||
name: "unknown required and known required",
|
||||
encode: []tlv.Type{firstReqType, secondReqType},
|
||||
decode: []tlv.Type{secondReqType},
|
||||
expErr: tlv.ErrUnknownRequiredType(firstReqType),
|
||||
},
|
||||
{
|
||||
name: "two unknown required",
|
||||
encode: []tlv.Type{firstReqType, secondReqType},
|
||||
expErr: tlv.ErrUnknownRequiredType(firstReqType),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
testParsedTypes(t, test)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testParsedTypes(t *testing.T, test parsedTypeTest) {
|
||||
encRecords := make([]tlv.Record, 0, len(test.encode))
|
||||
for _, typ := range test.encode {
|
||||
encRecords = append(
|
||||
encRecords, tlv.MakePrimitiveRecord(typ, new(uint64)),
|
||||
)
|
||||
}
|
||||
|
||||
decRecords := make([]tlv.Record, 0, len(test.decode))
|
||||
for _, typ := range test.decode {
|
||||
decRecords = append(
|
||||
decRecords, tlv.MakePrimitiveRecord(typ, new(uint64)),
|
||||
)
|
||||
}
|
||||
|
||||
// Construct a stream that will encode the test's set of types.
|
||||
encStream := tlv.MustNewStream(encRecords...)
|
||||
|
||||
var b bytes.Buffer
|
||||
if err := encStream.Encode(&b); err != nil {
|
||||
t.Fatalf("unable to encode stream: %v", err)
|
||||
}
|
||||
|
||||
// Create a stream that will parse only the known type.
|
||||
decStream := tlv.MustNewStream(
|
||||
tlv.MakePrimitiveRecord(knownType, new(uint64)),
|
||||
)
|
||||
// Create a stream that will parse a subset of the test's types.
|
||||
decStream := tlv.MustNewStream(decRecords...)
|
||||
|
||||
parsedTypes, err := decStream.DecodeWithParsedTypes(
|
||||
bytes.NewReader(b.Bytes()),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to decode stream: %v", err)
|
||||
if !reflect.DeepEqual(err, test.expErr) {
|
||||
t.Fatalf("error mismatch, want: %v got: %v", err, test.expErr)
|
||||
}
|
||||
|
||||
// Assert that both the known and unknown types are included in the set
|
||||
// of parsed types.
|
||||
if _, ok := parsedTypes[knownType]; !ok {
|
||||
t.Fatalf("known type %d should be in parsed types", knownType)
|
||||
}
|
||||
if _, ok := parsedTypes[unknownType]; !ok {
|
||||
t.Fatalf("unknown type %d should be in parsed types",
|
||||
unknownType)
|
||||
// Assert that all encoded types are included in the set of parsed
|
||||
// types.
|
||||
for _, typ := range test.encode {
|
||||
if _, ok := parsedTypes[typ]; !ok {
|
||||
t.Fatalf("encoded type %d should be in parsed types",
|
||||
typ)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user