lnd.xprv/htlcswitch/hop/payload.go
Conner Fromknecht cba523ae30
htlcswitch/hop/payload: apply known constraints if required failure
This commit modifies the NewPayloadFromReader to apply known
presence/omission contraints in the event that the tlv parser returns an
unknown required type failure.

Now that the parser has been modified to finished parsing the stream to
obtain a proper parsed type set, we can accurately apply these higher
level validation checks. This overrides required type failures, such
that they are only returned if the sender properly abided by the
constraints on fields for which we know.

The unit tests are updated to create otherwise valid payloads that then
return unknown required type failures. In one case, a test which
previously returned an unknown required type failure is made to return
an included failure for the sid, indicating the unknown required type 0
is being overruled.
2019-10-30 21:21:10 -07:00

214 lines
6.0 KiB
Go

package hop
import (
"encoding/binary"
"fmt"
"io"
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
// 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
// for an intermediate hop.
FinalHop bool
}
// Error returns a human-readable description of the invalid payload error.
func (e ErrInvalidPayload) Error() string {
hopType := "intermediate"
if e.FinalHop {
hopType = "final"
}
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.
// A Hop can represent either a TLV or legacy payload. The primary forwarding
// instruction can be accessed via ForwardingInfo, and additional records can be
// accessed by other member functions.
type Payload struct {
// FwdInfo holds the basic parameters required for HTLC forwarding, e.g.
// amount, cltv, and next hop.
FwdInfo ForwardingInfo
}
// NewLegacyPayload builds a Payload from the amount, cltv, and next hop
// parameters provided by leegacy onion payloads.
func NewLegacyPayload(f *sphinx.HopData) *Payload {
nextHop := binary.BigEndian.Uint64(f.NextAddress[:])
return &Payload{
FwdInfo: ForwardingInfo{
Network: BitcoinNetwork,
NextHop: lnwire.NewShortChanIDFromInt(nextHop),
AmountToForward: lnwire.MilliSatoshi(f.ForwardAmount),
OutgoingCTLV: f.OutgoingCltv,
},
}
}
// 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) {
var (
cid uint64
amt uint64
cltv uint32
)
tlvStream, err := tlv.NewStream(
record.NewAmtToFwdRecord(&amt),
record.NewLockTimeRecord(&cltv),
record.NewNextHopIDRecord(&cid),
)
if err != nil {
return nil, err
}
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
}
// 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
}
return &Payload{
FwdInfo: ForwardingInfo{
Network: BitcoinNetwork,
NextHop: nextHop,
AmountToForward: lnwire.MilliSatoshi(amt),
OutgoingCTLV: cltv,
},
}, nil
}
// ForwardingInfo returns the basic parameters required for HTLC forwarding,
// e.g. amount, cltv, and next hop.
func (h *Payload) ForwardingInfo() ForwardingInfo {
return h.FwdInfo
}
// ValidateParsedPayloadTypes checks the types parsed from a hop payload to
// ensure that the proper fields are either included or omitted. The finalHop
// 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.TypeSet,
nextHop lnwire.ShortChannelID) error {
isFinalHop := nextHop == Exit
_, hasAmt := parsedTypes[record.AmtOnionType]
_, hasLockTime := parsedTypes[record.LockTimeOnionType]
_, hasNextHop := parsedTypes[record.NextHopOnionType]
switch {
// All hops must include an amount to forward.
case !hasAmt:
return ErrInvalidPayload{
Type: record.AmtOnionType,
Violation: OmittedViolation,
FinalHop: isFinalHop,
}
// All hops must include a cltv expiry.
case !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:
return ErrInvalidPayload{
Type: record.NextHopOnionType,
Violation: IncludedViolation,
FinalHop: true,
}
}
return nil
}