lnd/htlcswitch/hop/iterator.go
Elle Mouton 65aef6a69c
htlcswitch: handle blinded path dummy hops
If a blinded path payload contains a signal that the following hop on
the path is a dummy hop, then we iteratively peel the dummy hops until
the final payload is reached.
2024-07-26 09:54:18 +02:00

930 lines
30 KiB
Go

package hop
import (
"bytes"
"errors"
"fmt"
"io"
"sync"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/tlv"
)
var (
// ErrDecodeFailed is returned when we can't decode blinded data.
ErrDecodeFailed = errors.New("could not decode blinded data")
// ErrNoBlindingPoint is returned when we have not provided a blinding
// point for a validated payload with encrypted data set.
ErrNoBlindingPoint = errors.New("no blinding point set for validated " +
"blinded hop")
)
// RouteRole represents the different types of roles a node can have as a
// recipient of a HTLC.
type RouteRole uint8
const (
// RouteRoleCleartext represents a regular route hop.
RouteRoleCleartext RouteRole = iota
// RouteRoleIntroduction represents an introduction node in a blinded
// path, characterized by a blinding point in the onion payload.
RouteRoleIntroduction
// RouteRoleRelaying represents a relaying node in a blinded path,
// characterized by a blinding point in update_add_htlc.
RouteRoleRelaying
)
// String representation of a role in a route.
func (h RouteRole) String() string {
switch h {
case RouteRoleCleartext:
return "cleartext"
case RouteRoleRelaying:
return "blinded relay"
case RouteRoleIntroduction:
return "introduction node"
default:
return fmt.Sprintf("unknown route role: %d", h)
}
}
// NewRouteRole returns the role we're playing in a route depending on the
// blinding points set (or not). If we are in the situation where we received
// blinding points in both the update add message and the payload:
// - We must have had a valid update add blinding point, because we were able
// to decrypt our onion to get the payload blinding point.
// - We return a relaying node role, because an introduction node (by
// definition) does not receive a blinding point in update add.
// - We assume the sending node to be buggy (including a payload blinding
// where it shouldn't), and rely on validation elsewhere to handle this.
func NewRouteRole(updateAddBlinding, payloadBlinding bool) RouteRole {
switch {
case updateAddBlinding:
return RouteRoleRelaying
case payloadBlinding:
return RouteRoleIntroduction
default:
return RouteRoleCleartext
}
}
// Iterator is an interface that abstracts away the routing information
// included in HTLC's which includes the entirety of the payment path of an
// HTLC. This interface provides two basic method which carry out: how to
// interpret the forwarding information encoded within the HTLC packet, and hop
// to encode the forwarding information for the _next_ hop.
type Iterator interface {
// HopPayload returns the set of fields that detail exactly _how_ this
// hop should forward the HTLC to the next hop. Additionally, the
// information encoded within the returned ForwardingInfo is to be used
// by each hop to authenticate the information given to it by the prior
// hop. The payload will also contain any additional TLV fields provided
// by the sender. The role that this hop plays in the context of
// route blinding (regular, introduction or relaying) is returned
// whenever the payload is successfully parsed, even if we subsequently
// face a validation error.
HopPayload() (*Payload, RouteRole, error)
// EncodeNextHop encodes the onion packet destined for the next hop
// into the passed io.Writer.
EncodeNextHop(w io.Writer) error
// ExtractErrorEncrypter returns the ErrorEncrypter needed for this hop,
// along with a failure code to signal if the decoding was successful.
ExtractErrorEncrypter(extractor ErrorEncrypterExtracter,
introductionNode bool) (ErrorEncrypter, lnwire.FailCode)
}
// sphinxHopIterator is the Sphinx implementation of hop iterator which uses
// onion routing to encode the payment route in such a way so that node might
// see only the next hop in the route.
type sphinxHopIterator struct {
// ogPacket is the original packet from which the processed packet is
// derived.
ogPacket *sphinx.OnionPacket
// processedPacket is the outcome of processing an onion packet. It
// includes the information required to properly forward the packet to
// the next hop.
processedPacket *sphinx.ProcessedPacket
// blindingKit contains the elements required to process hops that are
// part of a blinded route.
blindingKit BlindingKit
// rHash holds the payment hash for this payment. This is needed for
// when a new hop iterator is constructed.
rHash []byte
// router holds the router which can be used to decrypt onion payloads.
// This is required for peeling of dummy hops in a blinded path where
// the same node will iteratively need to unwrap the onion.
router *sphinx.Router
}
// makeSphinxHopIterator converts a processed packet returned from a sphinx
// router and converts it into an hop iterator for usage in the link. A
// blinding kit is passed through for the link to obtain forwarding information
// for blinded routes.
func makeSphinxHopIterator(router *sphinx.Router, ogPacket *sphinx.OnionPacket,
packet *sphinx.ProcessedPacket, blindingKit BlindingKit,
rHash []byte) *sphinxHopIterator {
return &sphinxHopIterator{
router: router,
ogPacket: ogPacket,
processedPacket: packet,
blindingKit: blindingKit,
rHash: rHash,
}
}
// A compile time check to ensure sphinxHopIterator implements the HopIterator
// interface.
var _ Iterator = (*sphinxHopIterator)(nil)
// Encode encodes iterator and writes it to the writer.
//
// NOTE: Part of the HopIterator interface.
func (r *sphinxHopIterator) EncodeNextHop(w io.Writer) error {
return r.processedPacket.NextPacket.Encode(w)
}
// HopPayload returns the set of fields that detail exactly _how_ this hop
// should forward the HTLC to the next hop. Additionally, the information
// encoded within the returned ForwardingInfo is to be used by each hop to
// authenticate the information given to it by the prior hop. The role that
// this hop plays in the context of route blinding (regular, introduction or
// relaying) is returned whenever the payload is successfully parsed, even if
// we subsequently face a validation error. The payload will also contain any
// additional TLV fields provided by the sender.
//
// NOTE: Part of the HopIterator interface.
func (r *sphinxHopIterator) HopPayload() (*Payload, RouteRole, error) {
switch r.processedPacket.Payload.Type {
// If this is the legacy payload, then we'll extract the information
// directly from the pre-populated ForwardingInstructions field.
case sphinx.PayloadLegacy:
fwdInst := r.processedPacket.ForwardingInstructions
return NewLegacyPayload(fwdInst), RouteRoleCleartext, nil
// Otherwise, if this is the TLV payload, then we'll make a new stream
// to decode only what we need to make routing decisions.
case sphinx.PayloadTLV:
return extractTLVPayload(r)
default:
return nil, RouteRoleCleartext,
fmt.Errorf("unknown sphinx payload type: %v",
r.processedPacket.Payload.Type)
}
}
// extractTLVPayload parses the hop payload and assumes that it uses the TLV
// format. It returns the parsed payload along with the RouteRole that this hop
// plays given the contents of the payload.
func extractTLVPayload(r *sphinxHopIterator) (*Payload, RouteRole, error) {
isFinal := r.processedPacket.Action == sphinx.ExitNode
// Initial payload parsing and validation
payload, routeRole, recipientData, err := parseAndValidateSenderPayload(
r.processedPacket.Payload.Payload, isFinal,
r.blindingKit.UpdateAddBlinding.IsSome(),
)
if err != nil {
return nil, routeRole, err
}
// If the payload contained no recipient data, then we can exit now.
if !recipientData {
return payload, routeRole, nil
}
return parseAndValidateRecipientData(r, payload, isFinal, routeRole)
}
// parseAndValidateRecipientData decrypts the payload from the recipient and
// then continues handling and validation based on if we are a forwarding node
// in this blinded path or the final destination node.
func parseAndValidateRecipientData(r *sphinxHopIterator, payload *Payload,
isFinal bool, routeRole RouteRole) (*Payload, RouteRole, error) {
// Decrypt and validate the blinded route data
routeData, blindingPoint, err := decryptAndValidateBlindedRouteData(
r, payload,
)
if err != nil {
return nil, routeRole, err
}
// This is the final node in the blinded route.
if isFinal {
return deriveBlindedRouteFinalHopForwardingInfo(
routeData, payload, routeRole,
)
}
// Else, we are a forwarding node in this blinded path.
return deriveBlindedRouteForwardingInfo(
r, routeData, payload, routeRole, blindingPoint,
)
}
// deriveBlindedRouteFinalHopForwardingInfo extracts the PathID from the
// routeData and constructs the ForwardingInfo accordingly.
func deriveBlindedRouteFinalHopForwardingInfo(
routeData *record.BlindedRouteData, payload *Payload,
routeRole RouteRole) (*Payload, RouteRole, error) {
var pathID *chainhash.Hash
routeData.PathID.WhenSome(func(r tlv.RecordT[tlv.TlvType6, []byte]) {
var id chainhash.Hash
copy(id[:], r.Val)
pathID = &id
})
if pathID == nil {
return nil, routeRole, ErrInvalidPayload{
Type: tlv.Type(6),
Violation: InsufficientViolation,
}
}
payload.FwdInfo = ForwardingInfo{
PathID: pathID,
}
return payload, routeRole, nil
}
// deriveBlindedRouteForwardingInfo uses the parsed BlindedRouteData from the
// recipient to derive the ForwardingInfo for the payment.
func deriveBlindedRouteForwardingInfo(r *sphinxHopIterator,
routeData *record.BlindedRouteData, payload *Payload,
routeRole RouteRole, blindingPoint *btcec.PublicKey) (*Payload,
RouteRole, error) {
relayInfo, err := routeData.RelayInfo.UnwrapOrErr(
fmt.Errorf("relay info not set for non-final blinded hop"),
)
if err != nil {
return nil, routeRole, err
}
fwdAmt, err := calculateForwardingAmount(
r.blindingKit.IncomingAmount, relayInfo.Val.BaseFee,
relayInfo.Val.FeeRate,
)
if err != nil {
return nil, routeRole, err
}
nextEph, err := routeData.NextBlindingOverride.UnwrapOrFuncErr(
func() (tlv.RecordT[tlv.TlvType8, *btcec.PublicKey], error) {
next, err := r.blindingKit.Processor.NextEphemeral(
blindingPoint,
)
if err != nil {
return routeData.NextBlindingOverride.Zero(),
err
}
return tlv.NewPrimitiveRecord[tlv.TlvType8](next), nil
})
if err != nil {
return nil, routeRole, err
}
// If the payload signals that the following hop is a dummy hop, then
// we will iteratively peel the dummy hop until we reach the final
// payload.
if checkForDummyHop(routeData, r.router.OnionPublicKey()) {
return peelBlindedPathDummyHop(
r, uint32(relayInfo.Val.CltvExpiryDelta), fwdAmt,
routeRole, nextEph,
)
}
nextSCID, err := routeData.ShortChannelID.UnwrapOrErr(
fmt.Errorf("next SCID not set for non-final blinded hop"),
)
if err != nil {
return nil, routeRole, err
}
payload.FwdInfo = ForwardingInfo{
NextHop: nextSCID.Val,
AmountToForward: fwdAmt,
OutgoingCTLV: r.blindingKit.IncomingCltv - uint32(
relayInfo.Val.CltvExpiryDelta,
),
// Remap from blinding override type to blinding point type.
NextBlinding: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
nextEph.Val,
),
),
}
return payload, routeRole, nil
}
// checkForDummyHop returns whether the given BlindedRouteData packet indicates
// the presence of a dummy hop.
func checkForDummyHop(routeData *record.BlindedRouteData,
routerPubKey *btcec.PublicKey) bool {
var isDummy bool
routeData.NextNodeID.WhenSome(
func(r tlv.RecordT[tlv.TlvType4, *btcec.PublicKey]) {
isDummy = r.Val.IsEqual(routerPubKey)
},
)
return isDummy
}
// peelBlindedPathDummyHop packages the next onion packet and then constructs
// a new hop iterator using our router and then proceeds to process the next
// packet. This can only be done for blinded route dummy hops since we expect
// to be the final hop on the path.
func peelBlindedPathDummyHop(r *sphinxHopIterator, cltvExpiryDelta uint32,
fwdAmt lnwire.MilliSatoshi, routeRole RouteRole,
nextEph tlv.RecordT[tlv.TlvType8, *btcec.PublicKey]) (*Payload,
RouteRole, error) {
onionPkt := r.processedPacket.NextPacket
sphinxPacket, err := r.router.ReconstructOnionPacket(
onionPkt, r.rHash, sphinx.WithBlindingPoint(nextEph.Val),
)
if err != nil {
return nil, routeRole, err
}
iterator := makeSphinxHopIterator(
r.router, onionPkt, sphinxPacket, BlindingKit{
Processor: r.router,
UpdateAddBlinding: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType]( //nolint:lll
nextEph.Val,
),
),
IncomingAmount: fwdAmt,
IncomingCltv: r.blindingKit.IncomingCltv -
cltvExpiryDelta,
}, r.rHash,
)
return extractTLVPayload(iterator)
}
// decryptAndValidateBlindedRouteData decrypts the encrypted payload from the
// payment recipient using a blinding key. The incoming HTLC amount and CLTV
// values are then verified against the policy values from the recipient.
func decryptAndValidateBlindedRouteData(r *sphinxHopIterator,
payload *Payload) (*record.BlindedRouteData, *btcec.PublicKey, error) {
blindingPoint, err := r.blindingKit.getBlindingPoint(
payload.blindingPoint,
)
if err != nil {
return nil, nil, err
}
decrypted, err := r.blindingKit.Processor.DecryptBlindedHopData(
blindingPoint, payload.encryptedData,
)
if err != nil {
return nil, nil, fmt.Errorf("decrypt blinded data: %w", err)
}
buf := bytes.NewBuffer(decrypted)
routeData, err := record.DecodeBlindedRouteData(buf)
if err != nil {
return nil, nil, fmt.Errorf("%w: %w", ErrDecodeFailed, err)
}
err = ValidateBlindedRouteData(
routeData, r.blindingKit.IncomingAmount,
r.blindingKit.IncomingCltv,
)
if err != nil {
return nil, nil, err
}
return routeData, blindingPoint, nil
}
// parseAndValidateSenderPayload parses the payload bytes received from the
// onion constructor (the sender) and validates that various fields have been
// set. It also uses the presence of a blinding key in either the
// update_add_htlc message or in the payload to determine the RouteRole.
// The RouteRole is returned even if an error is returned. The boolean return
// value indicates that the sender payload includes encrypted data from the
// recipient that should be parsed.
func parseAndValidateSenderPayload(payloadBytes []byte, isFinalHop,
updateAddBlindingSet bool) (*Payload, RouteRole, bool, error) {
// Extract TLVs from the packet constructor (the sender).
payload, parsed, err := ParseTLVPayload(bytes.NewReader(payloadBytes))
if err != nil {
// If we couldn't even parse our payload then we do a
// best-effort of determining our role in a blinded route,
// accepting that we can't know whether we were the introduction
// node (as the payload is not parseable).
routeRole := RouteRoleCleartext
if updateAddBlindingSet {
routeRole = RouteRoleRelaying
}
return nil, routeRole, false, err
}
// Now that we've parsed our payload we can determine which role we're
// playing in the route.
_, payloadBlinding := parsed[record.BlindingPointOnionType]
routeRole := NewRouteRole(updateAddBlindingSet, payloadBlinding)
// Validate the presence of the various payload fields we received from
// the sender.
err = ValidateTLVPayload(parsed, isFinalHop, updateAddBlindingSet)
if err != nil {
return nil, routeRole, false, err
}
// If there is no encrypted data from the receiver then return the
// payload as is since the forwarding info would have been received
// from the sender.
if payload.encryptedData == nil {
return payload, routeRole, false, nil
}
// Validate the presence of various fields in the sender payload given
// that we now know that this is a hop with instructions from the
// recipient.
err = ValidatePayloadWithBlinded(isFinalHop, parsed)
if err != nil {
return payload, routeRole, true, err
}
return payload, routeRole, true, nil
}
// ExtractErrorEncrypter decodes and returns the ErrorEncrypter for this hop,
// along with a failure code to signal if the decoding was successful. The
// ErrorEncrypter is used to encrypt errors back to the sender in the event that
// a payment fails.
//
// NOTE: Part of the HopIterator interface.
func (r *sphinxHopIterator) ExtractErrorEncrypter(
extracter ErrorEncrypterExtracter, introductionNode bool) (
ErrorEncrypter, lnwire.FailCode) {
encrypter, errCode := extracter(r.ogPacket.EphemeralKey)
if errCode != lnwire.CodeNone {
return nil, errCode
}
// If we're in a blinded path, wrap the error encrypter that we just
// derived in a "marker" type which we'll use to know what type of
// error we're handling.
switch {
case introductionNode:
return &IntroductionErrorEncrypter{
ErrorEncrypter: encrypter,
}, errCode
case r.blindingKit.UpdateAddBlinding.IsSome():
return &RelayingErrorEncrypter{
ErrorEncrypter: encrypter,
}, errCode
default:
return encrypter, errCode
}
}
// BlindingProcessor is an interface that provides the cryptographic operations
// required for processing blinded hops.
//
// This interface is extracted to allow more granular testing of blinded
// forwarding calculations.
type BlindingProcessor interface {
// DecryptBlindedHopData decrypts a blinded blob of data using the
// ephemeral key provided.
DecryptBlindedHopData(ephemPub *btcec.PublicKey,
encryptedData []byte) ([]byte, error)
// NextEphemeral returns the next hop's ephemeral key, calculated
// from the current ephemeral key provided.
NextEphemeral(*btcec.PublicKey) (*btcec.PublicKey, error)
}
// BlindingKit contains the components required to extract forwarding
// information for hops in a blinded route.
type BlindingKit struct {
// Processor provides the low-level cryptographic operations to
// handle an encrypted blob of data in a blinded forward.
Processor BlindingProcessor
// UpdateAddBlinding holds a blinding point that was passed to the
// node via update_add_htlc's TLVs.
UpdateAddBlinding lnwire.BlindingPointRecord
// IncomingCltv is the expiry of the incoming HTLC.
IncomingCltv uint32
// IncomingAmount is the amount of the incoming HTLC.
IncomingAmount lnwire.MilliSatoshi
}
// getBlindingPoint returns either the payload or updateAddHtlc blinding point,
// assuming that validation that these values are appropriately set has already
// been handled elsewhere.
func (b *BlindingKit) getBlindingPoint(payloadBlinding *btcec.PublicKey) (
*btcec.PublicKey, error) {
payloadBlindingSet := payloadBlinding != nil
updateBlindingSet := b.UpdateAddBlinding.IsSome()
switch {
case payloadBlindingSet:
return payloadBlinding, nil
case updateBlindingSet:
pk, err := b.UpdateAddBlinding.UnwrapOrErr(
fmt.Errorf("expected update add blinding"),
)
if err != nil {
return nil, err
}
return pk.Val, nil
default:
return nil, ErrNoBlindingPoint
}
}
// calculateForwardingAmount calculates the amount to forward for a blinded
// hop based on the incoming amount and forwarding parameters.
//
// When forwarding a payment, the fee we take is calculated, not on the
// incoming amount, but rather on the amount we forward. We charge fees based
// on our own liquidity we are forwarding downstream.
//
// With route blinding, we are NOT given the amount to forward. This
// unintuitive looking formula comes from the fact that without the amount to
// forward, we cannot compute the fees taken directly.
//
// The amount to be forwarded can be computed as follows:
//
// amt_to_forward = incoming_amount - total_fees
// total_fees = base_fee + amt_to_forward*(fee_rate/1000000)
//
// Solving for amount_to_forward:
// amt_to_forward = incoming_amount - base_fee - (amount_to_forward * fee_rate)/1e6
// amt_to_forward + (amount_to_forward * fee_rate) / 1e6 = incoming_amount - base_fee
// amt_to_forward * 1e6 + (amount_to_forward * fee_rate) = (incoming_amount - base_fee) * 1e6
// amt_to_forward * (1e6 + fee_rate) = (incoming_amount - base_fee) * 1e6
// amt_to_forward = ((incoming_amount - base_fee) * 1e6) / (1e6 + fee_rate)
//
// From there we use a ceiling formula for integer division so that we always
// round up, otherwise the sender may receive slightly less than intended:
//
// ceil(a/b) = (a + b - 1)/(b).
//
//nolint:lll,dupword
func calculateForwardingAmount(incomingAmount, baseFee lnwire.MilliSatoshi,
proportionalFee uint32) (lnwire.MilliSatoshi, error) {
// Sanity check to prevent overflow.
if incomingAmount < baseFee {
return 0, fmt.Errorf("incoming amount: %v < base fee: %v",
incomingAmount, baseFee)
}
numerator := (uint64(incomingAmount) - uint64(baseFee)) * 1e6
denominator := 1e6 + uint64(proportionalFee)
ceiling := (numerator + denominator - 1) / denominator
return lnwire.MilliSatoshi(ceiling), nil
}
// OnionProcessor is responsible for keeping all sphinx dependent parts inside
// and expose only decoding function. With such approach we give freedom for
// subsystems which wants to decode sphinx path to not be dependable from
// sphinx at all.
//
// NOTE: The reason for keeping decoder separated from hop iterator is too
// maintain the hop iterator abstraction. Without it the structures which using
// the hop iterator should contain sphinx router which makes their creations in
// tests dependent from the sphinx internal parts.
type OnionProcessor struct {
router *sphinx.Router
}
// NewOnionProcessor creates new instance of decoder.
func NewOnionProcessor(router *sphinx.Router) *OnionProcessor {
return &OnionProcessor{router}
}
// Start spins up the onion processor's sphinx router.
func (p *OnionProcessor) Start() error {
log.Info("Onion processor starting")
return p.router.Start()
}
// Stop shutsdown the onion processor's sphinx router.
func (p *OnionProcessor) Stop() error {
log.Info("Onion processor shutting down...")
defer log.Debug("Onion processor shutdown complete")
p.router.Stop()
return nil
}
// ReconstructBlindingInfo contains the information required to reconstruct a
// blinded onion.
type ReconstructBlindingInfo struct {
// BlindingKey is the blinding point set in UpdateAddHTLC.
BlindingKey lnwire.BlindingPointRecord
// IncomingAmt is the amount for the incoming HTLC.
IncomingAmt lnwire.MilliSatoshi
// IncomingExpiry is the expiry height of the incoming HTLC.
IncomingExpiry uint32
}
// ReconstructHopIterator attempts to decode a valid sphinx packet from the
// passed io.Reader instance using the rHash as the associated data when
// checking the relevant MACs during the decoding process.
func (p *OnionProcessor) ReconstructHopIterator(r io.Reader, rHash []byte,
blindingInfo ReconstructBlindingInfo) (Iterator, error) {
onionPkt := &sphinx.OnionPacket{}
if err := onionPkt.Decode(r); err != nil {
return nil, err
}
var opts []sphinx.ProcessOnionOpt
blindingInfo.BlindingKey.WhenSome(func(
r tlv.RecordT[lnwire.BlindingPointTlvType, *btcec.PublicKey]) {
opts = append(opts, sphinx.WithBlindingPoint(r.Val))
})
// Attempt to process the Sphinx packet. We include the payment hash of
// the HTLC as it's authenticated within the Sphinx packet itself as
// associated data in order to thwart attempts a replay attacks. In the
// case of a replay, an attacker is *forced* to use the same payment
// hash twice, thereby losing their money entirely.
sphinxPacket, err := p.router.ReconstructOnionPacket(
onionPkt, rHash, opts...,
)
if err != nil {
return nil, err
}
return makeSphinxHopIterator(p.router, onionPkt, sphinxPacket,
BlindingKit{
Processor: p.router,
UpdateAddBlinding: blindingInfo.BlindingKey,
IncomingAmount: blindingInfo.IncomingAmt,
IncomingCltv: blindingInfo.IncomingExpiry,
}, rHash,
), nil
}
// DecodeHopIteratorRequest encapsulates all date necessary to process an onion
// packet, perform sphinx replay detection, and schedule the entry for garbage
// collection.
type DecodeHopIteratorRequest struct {
OnionReader io.Reader
RHash []byte
IncomingCltv uint32
IncomingAmount lnwire.MilliSatoshi
BlindingPoint lnwire.BlindingPointRecord
}
// DecodeHopIteratorResponse encapsulates the outcome of a batched sphinx onion
// processing.
type DecodeHopIteratorResponse struct {
HopIterator Iterator
FailCode lnwire.FailCode
}
// Result returns the (HopIterator, lnwire.FailCode) tuple, which should
// correspond to the index of a particular DecodeHopIteratorRequest.
//
// NOTE: The HopIterator should be considered invalid if the fail code is
// anything but lnwire.CodeNone.
func (r *DecodeHopIteratorResponse) Result() (Iterator, lnwire.FailCode) {
return r.HopIterator, r.FailCode
}
// DecodeHopIterators performs batched decoding and validation of incoming
// sphinx packets. For the same `id`, this method will return the same iterators
// and failcodes upon subsequent invocations.
//
// NOTE: In order for the responses to be valid, the caller must guarantee that
// the presented readers and rhashes *NEVER* deviate across invocations for the
// same id.
func (p *OnionProcessor) DecodeHopIterators(id []byte,
reqs []DecodeHopIteratorRequest) ([]DecodeHopIteratorResponse, error) {
var (
batchSize = len(reqs)
onionPkts = make([]sphinx.OnionPacket, batchSize)
resps = make([]DecodeHopIteratorResponse, batchSize)
)
tx := p.router.BeginTxn(id, batchSize)
decode := func(seqNum uint16, onionPkt *sphinx.OnionPacket,
req DecodeHopIteratorRequest) lnwire.FailCode {
err := onionPkt.Decode(req.OnionReader)
switch err {
case nil:
// success
case sphinx.ErrInvalidOnionVersion:
return lnwire.CodeInvalidOnionVersion
case sphinx.ErrInvalidOnionKey:
return lnwire.CodeInvalidOnionKey
default:
log.Errorf("unable to decode onion packet: %v", err)
return lnwire.CodeInvalidOnionKey
}
var opts []sphinx.ProcessOnionOpt
req.BlindingPoint.WhenSome(func(
b tlv.RecordT[lnwire.BlindingPointTlvType,
*btcec.PublicKey]) {
opts = append(opts, sphinx.WithBlindingPoint(
b.Val,
))
})
err = tx.ProcessOnionPacket(
seqNum, onionPkt, req.RHash, req.IncomingCltv, opts...,
)
switch err {
case nil:
// success
return lnwire.CodeNone
case sphinx.ErrInvalidOnionVersion:
return lnwire.CodeInvalidOnionVersion
case sphinx.ErrInvalidOnionHMAC:
return lnwire.CodeInvalidOnionHmac
case sphinx.ErrInvalidOnionKey:
return lnwire.CodeInvalidOnionKey
default:
log.Errorf("unable to process onion packet: %v", err)
return lnwire.CodeInvalidOnionKey
}
}
// Execute cpu-heavy onion decoding in parallel.
var wg sync.WaitGroup
for i := range reqs {
wg.Add(1)
go func(seqNum uint16) {
defer wg.Done()
onionPkt := &onionPkts[seqNum]
resps[seqNum].FailCode = decode(
seqNum, onionPkt, reqs[seqNum],
)
}(uint16(i))
}
wg.Wait()
// With that batch created, we will now attempt to write the shared
// secrets to disk. This operation will returns the set of indices that
// were detected as replays, and the computed sphinx packets for all
// indices that did not fail the above loop. Only indices that are not
// in the replay set should be considered valid, as they are
// opportunistically computed.
packets, replays, err := tx.Commit()
if err != nil {
log.Errorf("unable to process onion packet batch %x: %v",
id, err)
// If we failed to commit the batch to the secret share log, we
// will mark all not-yet-failed channels with a temporary
// channel failure and exit since we cannot proceed.
for i := range resps {
resp := &resps[i]
// Skip any indexes that already failed onion decoding.
if resp.FailCode != lnwire.CodeNone {
continue
}
log.Errorf("unable to process onion packet %x-%v",
id, i)
resp.FailCode = lnwire.CodeTemporaryChannelFailure
}
// TODO(conner): return real errors to caller so link can fail?
return resps, err
}
// Otherwise, the commit was successful. Now we will post process any
// remaining packets, additionally failing any that were included in the
// replay set.
for i := range resps {
resp := &resps[i]
// Skip any indexes that already failed onion decoding.
if resp.FailCode != lnwire.CodeNone {
continue
}
// If this index is contained in the replay set, mark it with a
// temporary channel failure error code. We infer that the
// offending error was due to a replayed packet because this
// index was found in the replay set.
if replays.Contains(uint16(i)) {
log.Errorf("unable to process onion packet: %v",
sphinx.ErrReplayedPacket)
// We set FailCode to CodeInvalidOnionVersion even
// though the ephemeral key isn't the problem. We need
// to set the BADONION bit since we're sending back a
// malformed packet, but as there isn't a specific
// failure code for replays, we reuse one of the
// failure codes that has BADONION.
resp.FailCode = lnwire.CodeInvalidOnionVersion
continue
}
// Finally, construct a hop iterator from our processed sphinx
// packet, simultaneously caching the original onion packet.
resp.HopIterator = makeSphinxHopIterator(
p.router, &onionPkts[i], &packets[i], BlindingKit{
Processor: p.router,
UpdateAddBlinding: reqs[i].BlindingPoint,
IncomingAmount: reqs[i].IncomingAmount,
IncomingCltv: reqs[i].IncomingCltv,
}, reqs[i].RHash,
)
}
return resps, nil
}
// ExtractErrorEncrypter takes an io.Reader which should contain the onion
// packet as original received by a forwarding node and creates an
// ErrorEncrypter instance using the derived shared secret. In the case that en
// error occurs, a lnwire failure code detailing the parsing failure will be
// returned.
func (p *OnionProcessor) ExtractErrorEncrypter(ephemeralKey *btcec.PublicKey) (
ErrorEncrypter, lnwire.FailCode) {
onionObfuscator, err := sphinx.NewOnionErrorEncrypter(
p.router, ephemeralKey,
)
if err != nil {
switch err {
case sphinx.ErrInvalidOnionVersion:
return nil, lnwire.CodeInvalidOnionVersion
case sphinx.ErrInvalidOnionHMAC:
return nil, lnwire.CodeInvalidOnionHmac
case sphinx.ErrInvalidOnionKey:
return nil, lnwire.CodeInvalidOnionKey
default:
log.Errorf("unable to process onion packet: %v", err)
return nil, lnwire.CodeInvalidOnionKey
}
}
return &SphinxErrorEncrypter{
OnionErrorEncrypter: onionObfuscator,
EphemeralKey: ephemeralKey,
}, lnwire.CodeNone
}