lnd/htlcswitch/circuit.go
Carla Kirk-Cohen 9f038c6191
htlcswitch: introduce wrapper type error encrypter to identify blinded
Introduce two wrapper types for our existing SphinxErrorEncrypter
that are used to represent error encrypters where we're a part of a
blinded route. These encrypters are functionally the same as a sphinx
encrypter, and are just used as "markers" so that we know that we
need to handle our error differently due to our different role.

We need to persist this information to account for restart cases where
we've resovled the outgoing HTLC, then restart and need to handle the
error for the incoming link. Specifically, this is relevant for:
- On chain resolution messages received after restart
- Forwarding packages that are re-forwarded after restart

This is also generally helpful, because we can store this information
in one place (the circuit) rather than trying to reconstruct it in
various places when forwarding the failure back over the switch.
2024-04-25 09:47:10 -04:00

238 lines
6.7 KiB
Go

package htlcswitch
import (
"encoding/binary"
"io"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/htlcswitch/hop"
"github.com/lightningnetwork/lnd/lnwire"
)
// EmptyCircuitKey is a default value for an outgoing circuit key returned when
// a circuit's keystone has not been set. Note that this value is invalid for
// use as a keystone, since the outgoing channel id can never be equal to
// sourceHop.
var EmptyCircuitKey CircuitKey
// CircuitKey is a tuple of channel ID and HTLC ID, used to uniquely identify
// HTLCs in a circuit. Circuits are identified primarily by the circuit key of
// the incoming HTLC. However, a circuit may also be referenced by its outgoing
// circuit key after the HTLC has been forwarded via the outgoing link.
type CircuitKey = models.CircuitKey
// PaymentCircuit is used by the switch as placeholder between when the
// switch makes a forwarding decision and the outgoing link determines the
// proper HTLC ID for the local log. After the outgoing HTLC ID has been
// determined, the half circuit will be converted into a full PaymentCircuit.
type PaymentCircuit struct {
// AddRef is the forward reference of the Add update in the incoming
// link's forwarding package. This value is set on the htlcPacket of the
// returned settle/fail so that it can be removed from disk.
AddRef channeldb.AddRef
// Incoming is the circuit key identifying the incoming channel and htlc
// index from which this ADD originates.
Incoming CircuitKey
// Outgoing is the circuit key identifying the outgoing channel, and the
// HTLC index that was used to forward the ADD. It will be nil if this
// circuit's keystone has not been set.
Outgoing *CircuitKey
// PaymentHash used as unique identifier of payment.
PaymentHash [32]byte
// IncomingAmount is the value of the HTLC from the incoming link.
IncomingAmount lnwire.MilliSatoshi
// OutgoingAmount specifies the value of the HTLC leaving the switch,
// either as a payment or forwarded amount.
OutgoingAmount lnwire.MilliSatoshi
// ErrorEncrypter is used to re-encrypt the onion failure before
// sending it back to the originator of the payment.
ErrorEncrypter hop.ErrorEncrypter
// LoadedFromDisk is set true for any circuits loaded after the circuit
// map is reloaded from disk.
//
// NOTE: This value is determined implicitly during a restart. It is not
// persisted, and should never be set outside the circuit map.
LoadedFromDisk bool
}
// HasKeystone returns true if an outgoing link has assigned this circuit's
// outgoing circuit key.
func (c *PaymentCircuit) HasKeystone() bool {
return c.Outgoing != nil
}
// newPaymentCircuit initializes a payment circuit on the heap using the payment
// hash and an in-memory htlc packet.
func newPaymentCircuit(hash *[32]byte, pkt *htlcPacket) *PaymentCircuit {
var addRef channeldb.AddRef
if pkt.sourceRef != nil {
addRef = *pkt.sourceRef
}
return &PaymentCircuit{
AddRef: addRef,
Incoming: CircuitKey{
ChanID: pkt.incomingChanID,
HtlcID: pkt.incomingHTLCID,
},
PaymentHash: *hash,
IncomingAmount: pkt.incomingAmount,
OutgoingAmount: pkt.amount,
ErrorEncrypter: pkt.obfuscator,
}
}
// makePaymentCircuit initializes a payment circuit on the stack using the
// payment hash and an in-memory htlc packet.
func makePaymentCircuit(hash *[32]byte, pkt *htlcPacket) PaymentCircuit {
var addRef channeldb.AddRef
if pkt.sourceRef != nil {
addRef = *pkt.sourceRef
}
return PaymentCircuit{
AddRef: addRef,
Incoming: CircuitKey{
ChanID: pkt.incomingChanID,
HtlcID: pkt.incomingHTLCID,
},
PaymentHash: *hash,
IncomingAmount: pkt.incomingAmount,
OutgoingAmount: pkt.amount,
ErrorEncrypter: pkt.obfuscator,
}
}
// Encode writes a PaymentCircuit to the provided io.Writer.
func (c *PaymentCircuit) Encode(w io.Writer) error {
if err := c.AddRef.Encode(w); err != nil {
return err
}
if err := c.Incoming.Encode(w); err != nil {
return err
}
if _, err := w.Write(c.PaymentHash[:]); err != nil {
return err
}
var scratch [8]byte
binary.BigEndian.PutUint64(scratch[:], uint64(c.IncomingAmount))
if _, err := w.Write(scratch[:]); err != nil {
return err
}
binary.BigEndian.PutUint64(scratch[:], uint64(c.OutgoingAmount))
if _, err := w.Write(scratch[:]); err != nil {
return err
}
// Defaults to EncrypterTypeNone.
var encrypterType hop.EncrypterType
if c.ErrorEncrypter != nil {
encrypterType = c.ErrorEncrypter.Type()
}
err := binary.Write(w, binary.BigEndian, encrypterType)
if err != nil {
return err
}
// Skip encoding of error encrypter if this half add does not have one.
if encrypterType == hop.EncrypterTypeNone {
return nil
}
return c.ErrorEncrypter.Encode(w)
}
// Decode reads a PaymentCircuit from the provided io.Reader.
func (c *PaymentCircuit) Decode(r io.Reader) error {
if err := c.AddRef.Decode(r); err != nil {
return err
}
if err := c.Incoming.Decode(r); err != nil {
return err
}
if _, err := io.ReadFull(r, c.PaymentHash[:]); err != nil {
return err
}
var scratch [8]byte
if _, err := io.ReadFull(r, scratch[:]); err != nil {
return err
}
c.IncomingAmount = lnwire.MilliSatoshi(
binary.BigEndian.Uint64(scratch[:]))
if _, err := io.ReadFull(r, scratch[:]); err != nil {
return err
}
c.OutgoingAmount = lnwire.MilliSatoshi(
binary.BigEndian.Uint64(scratch[:]))
// Read the encrypter type used for this circuit.
var encrypterType hop.EncrypterType
err := binary.Read(r, binary.BigEndian, &encrypterType)
if err != nil {
return err
}
switch encrypterType {
case hop.EncrypterTypeNone:
// No encrypter was provided, such as when the payment is
// locally initiated.
return nil
case hop.EncrypterTypeSphinx:
// Sphinx encrypter was used as this is a forwarded HTLC.
c.ErrorEncrypter = hop.NewSphinxErrorEncrypter()
case hop.EncrypterTypeMock:
// Test encrypter.
c.ErrorEncrypter = NewMockObfuscator()
case hop.EncrypterTypeIntroduction:
c.ErrorEncrypter = hop.NewIntroductionErrorEncrypter()
case hop.EncrypterTypeRelaying:
c.ErrorEncrypter = hop.NewRelayingErrorEncrypter()
default:
return UnknownEncrypterType(encrypterType)
}
return c.ErrorEncrypter.Decode(r)
}
// InKey returns the primary identifier for the circuit corresponding to the
// incoming HTLC.
func (c *PaymentCircuit) InKey() CircuitKey {
return c.Incoming
}
// OutKey returns the keystone identifying the outgoing link and HTLC ID. If the
// circuit hasn't been completed, this method returns an EmptyKeystone, which is
// an invalid outgoing circuit key. Only call this method if HasKeystone returns
// true.
func (c *PaymentCircuit) OutKey() CircuitKey {
if c.Outgoing != nil {
return *c.Outgoing
}
return EmptyCircuitKey
}