mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-20 02:27:21 +01:00
292 lines
8.0 KiB
Go
292 lines
8.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
|
|
|
|
// MPP holds the info provided in an option_mpp record when parsed from
|
|
// a TLV onion payload.
|
|
MPP *record.MPP
|
|
|
|
// customRecords are user-defined records in the custom type range that
|
|
// were included in the payload.
|
|
customRecords record.CustomSet
|
|
}
|
|
|
|
// 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,
|
|
},
|
|
customRecords: make(record.CustomSet),
|
|
}
|
|
}
|
|
|
|
// 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
|
|
mpp = &record.MPP{}
|
|
)
|
|
|
|
tlvStream, err := tlv.NewStream(
|
|
record.NewAmtToFwdRecord(&amt),
|
|
record.NewLockTimeRecord(&cltv),
|
|
record.NewNextHopIDRecord(&cid),
|
|
mpp.Record(),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
parsedTypes, err := tlvStream.DecodeWithParsedTypes(r)
|
|
if err != nil {
|
|
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
|
|
}
|
|
|
|
// Check for violation of the rules for mandatory fields.
|
|
violatingType := getMinRequiredViolation(parsedTypes)
|
|
if violatingType != nil {
|
|
return nil, ErrInvalidPayload{
|
|
Type: *violatingType,
|
|
Violation: RequiredViolation,
|
|
FinalHop: nextHop == Exit,
|
|
}
|
|
}
|
|
|
|
// If no MPP field was parsed, set the MPP field on the resulting
|
|
// payload to nil.
|
|
if _, ok := parsedTypes[record.MPPOnionType]; !ok {
|
|
mpp = nil
|
|
}
|
|
|
|
// Filter out the custom records.
|
|
customRecords := NewCustomRecords(parsedTypes)
|
|
|
|
return &Payload{
|
|
FwdInfo: ForwardingInfo{
|
|
Network: BitcoinNetwork,
|
|
NextHop: nextHop,
|
|
AmountToForward: lnwire.MilliSatoshi(amt),
|
|
OutgoingCTLV: cltv,
|
|
},
|
|
MPP: mpp,
|
|
customRecords: customRecords,
|
|
}, 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
|
|
}
|
|
|
|
// NewCustomRecords filters the types parsed from the tlv stream for custom
|
|
// records.
|
|
func NewCustomRecords(parsedTypes tlv.TypeMap) record.CustomSet {
|
|
customRecords := make(record.CustomSet)
|
|
for t, parseResult := range parsedTypes {
|
|
if parseResult == nil || t < record.CustomTypeStart {
|
|
continue
|
|
}
|
|
customRecords[uint64(t)] = parseResult
|
|
}
|
|
return customRecords
|
|
}
|
|
|
|
// 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.TypeMap,
|
|
nextHop lnwire.ShortChannelID) error {
|
|
|
|
isFinalHop := nextHop == Exit
|
|
|
|
_, hasAmt := parsedTypes[record.AmtOnionType]
|
|
_, hasLockTime := parsedTypes[record.LockTimeOnionType]
|
|
_, hasNextHop := parsedTypes[record.NextHopOnionType]
|
|
_, hasMPP := parsedTypes[record.MPPOnionType]
|
|
|
|
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,
|
|
}
|
|
|
|
// Intermediate nodes should never receive MPP fields.
|
|
case !isFinalHop && hasMPP:
|
|
return ErrInvalidPayload{
|
|
Type: record.MPPOnionType,
|
|
Violation: IncludedViolation,
|
|
FinalHop: isFinalHop,
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MultiPath returns the record corresponding the option_mpp parsed from the
|
|
// onion payload.
|
|
func (h *Payload) MultiPath() *record.MPP {
|
|
return h.MPP
|
|
}
|
|
|
|
// CustomRecords returns the custom tlv type records that were parsed from the
|
|
// payload.
|
|
func (h *Payload) CustomRecords() record.CustomSet {
|
|
return h.customRecords
|
|
}
|
|
|
|
// getMinRequiredViolation checks for unrecognized required (even) fields in the
|
|
// standard range and returns the lowest required type. Always returning the
|
|
// lowest required type allows a failure message to be deterministic.
|
|
func getMinRequiredViolation(set tlv.TypeMap) *tlv.Type {
|
|
var (
|
|
requiredViolation bool
|
|
minRequiredViolationType tlv.Type
|
|
)
|
|
for t, parseResult := range set {
|
|
// If a type is even but not known to us, we cannot process the
|
|
// payload. We are required to understand a field that we don't
|
|
// support.
|
|
//
|
|
// We always accept custom fields, because a higher level
|
|
// application may understand them.
|
|
if parseResult == nil || t%2 != 0 ||
|
|
t >= record.CustomTypeStart {
|
|
|
|
continue
|
|
}
|
|
|
|
if !requiredViolation || t < minRequiredViolationType {
|
|
minRequiredViolationType = t
|
|
}
|
|
requiredViolation = true
|
|
}
|
|
|
|
if requiredViolation {
|
|
return &minRequiredViolationType
|
|
}
|
|
|
|
return nil
|
|
}
|