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
}
