mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
65aef6a69c
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.
930 lines
30 KiB
Go
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
|
|
}
|