Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
74652d6
htlcswitch: remove unused decode hop iterator
carlaKC Nov 21, 2022
ea8c9c0
lnwire: create common encoder/decoder for raw feature vectors
carlaKC Feb 1, 2023
827ed18
lnwire: add TLV encoding/decoding for blinded route data blobs
carlaKC Jun 14, 2022
671cf55
lnwire: add blinding point to update_add_htlc TLVs
carlaKC Jun 10, 2022
f20d746
multi: add blinding point to payment descriptor / use in hop decryption
carlaKC Nov 18, 2022
a935d36
htlcswitch: add incoming amount and to decode hop iterator request
carlaKC Jan 27, 2023
ef5d4d7
hop: add function for calculating forwarding amount
carlaKC Dec 14, 2022
6af0108
multi: add validation of blinded route encrypted data
carlaKC Feb 1, 2023
33972ab
htlcswitch: add blinding kit to handle encrypted data in blinded routes
carlaKC Dec 14, 2022
08ff249
Htlcswitch: add NextBlinding to ForwardingInfo and set in UpdateAddHtlc
carlaKC Dec 21, 2022
c809006
htlcswitch: pass blinding kit to payload creation
carlaKC Jan 27, 2023
0a443a2
htlcswitch: add validation for blinded route payloads
carlaKC Nov 18, 2022
18aef84
htlcswitch: set forwarding information from encrypted data
carlaKC Nov 21, 2022
aa3cbac
multi: advertise route blinding feature bit
carlaKC Jan 25, 2023
fc3573e
lntest: add setup for blinded route forwarding itest
carlaKC Dec 13, 2022
bbb5143
lntest: add helper to create blinded route
carlaKC Dec 13, 2022
695966d
lntest: add route construction to blinded forwarding test
carlaKC Dec 13, 2022
0b1adfd
lntest: dispatch payment to blinded route
carlaKC Dec 14, 2022
4510819
lntest: add interceptor to blinded forwarding test for workaround
carlaKC Dec 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions feature/default_sets.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ var defaultSetDesc = setDesc{
SetInit: {}, // I
SetNodeAnn: {}, // N
},
// Note: we set route blinding optionally in our init and announcement,
// but not yet in invoices (9) as the spec instructs because we do not
// yet support receiving payments to blinded routes, only relaying them.
lnwire.RouteBlindingOptional: {
SetInit: {}, // I
SetNodeAnn: {}, // N
},
lnwire.WumboChannelsOptional: {
SetInit: {}, // I
SetNodeAnn: {}, // N
Expand Down
6 changes: 6 additions & 0 deletions htlcswitch/hop/forwarding_info.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package hop

import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/lnwire"
)

Expand All @@ -22,4 +23,9 @@ type ForwardingInfo struct {
// OutgoingCTLV is the specified value of the CTLV timelock to be used
// in the outgoing HTLC.
OutgoingCTLV uint32

// NextBlinding is an optional blinding point to be passed to the next
// node in UpdateAddHtlc. This field is set if the htlc is part of a
// blinded route.
NextBlinding *btcec.PublicKey
}
252 changes: 196 additions & 56 deletions htlcswitch/hop/iterator.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
)

// Iterator is an interface that abstracts away the routing information
Expand Down Expand Up @@ -47,16 +48,24 @@ type sphinxHopIterator struct {
// 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
}

// makeSphinxHopIterator converts a processed packet returned from a sphinx
// router and converts it into an hop iterator for usage in the link.
// 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(ogPacket *sphinx.OnionPacket,
packet *sphinx.ProcessedPacket) *sphinxHopIterator {
packet *sphinx.ProcessedPacket,
blindingKit *BlindingKit) *sphinxHopIterator {

return &sphinxHopIterator{
ogPacket: ogPacket,
processedPacket: packet,
blindingKit: blindingKit,
}
}

Expand Down Expand Up @@ -92,7 +101,7 @@ func (r *sphinxHopIterator) HopPayload() (*Payload, error) {
case sphinx.PayloadTLV:
return NewPayloadFromReader(bytes.NewReader(
r.processedPacket.Payload.Payload,
))
), r.blindingKit)

default:
return nil, fmt.Errorf("unknown sphinx payload type: %v",
Expand All @@ -112,6 +121,166 @@ func (r *sphinxHopIterator) ExtractErrorEncrypter(
return extracter(r.ogPacket.EphemeralKey)
}

// BlindingProcessor is an interface that provides the cryptographic operations
// required for processing blinded hops.
type BlindingProcessor interface {
// UnBlindData decrypts a blinded blob of data using the ephemeral key
// provided.
UnBlindData(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 {
// BlindingPoint holds a blinding point that was passed to the node via
// update_add_htlc's TLVs.
BlindingPoint *btcec.PublicKey

// lastHop indicates whether we're in the last hop in the onion route.
lastHop bool

// forwardingInfo uses the ephemeral blinding key provided to decrypt
// a blob of encrypted data provided in the onion and obtain the
// forwarding information for the blinded hop.
forwardingInfo func(*btcec.PublicKey, []byte) (*ForwardingInfo,
error)
}

// MakeBlindingKit produces a kit that is used to decrypte and decode
// forwarding information for hops in blinded routes.
func MakeBlindingKit(processor BlindingProcessor,
blindingPoint *btcec.PublicKey, lastHop bool,
incomingAmount lnwire.MilliSatoshi, incomingCltv uint32) *BlindingKit {

return &BlindingKit{
BlindingPoint: blindingPoint,
lastHop: lastHop,
forwardingInfo: deriveForwardingInfo(
processor, incomingAmount, incomingCltv,
),
}
}

// deriveForwardingInfo produces a function that will decrypt and deserialize
// an encrypted blob of data for a hop in a blinded route and reconstruct the
// forwarding information for the hop from the information provided.
func deriveForwardingInfo(processor BlindingProcessor,
incomingAmount lnwire.MilliSatoshi, incomingCltv uint32) func(
*btcec.PublicKey, []byte) (*ForwardingInfo, error) {

return func(blinding *btcec.PublicKey, data []byte) (*ForwardingInfo,
error) {

decrypted, err := processor.UnBlindData(blinding, data)
if err != nil {
return nil, fmt.Errorf("decrypt blinded data: %w", err)
}

b := bytes.NewBuffer(decrypted)
routeData, err := record.DecodeBlindedRouteData(b)
if err != nil {
return nil, fmt.Errorf("decode route data: %w", err)
}

if err := validateBlindedRouteData(
routeData, incomingAmount, incomingCltv,
); err != nil {
return nil, err
}
// If we have our short channel ID or expiry present, set
// values in our forwarding information. We start with the
// incoming values as defaults so that they will have the
// correct values for the final hop in the blinded route
// (which does not have relay info set).
var (
nextHop = Exit
expiry = incomingCltv
fwdAmt = incomingAmount
)

if routeData.ShortChannelID != nil {
nextHop = *routeData.ShortChannelID
}

if routeData.RelayInfo != nil {
fwdAmt, err = calculateForwardingAmount(
incomingAmount, routeData.RelayInfo.BaseFee,
routeData.RelayInfo.FeeRate,
)
if err != nil {
return nil, err
}

expiry = incomingCltv - uint32(
routeData.RelayInfo.CltvExpiryDelta,
)
}

nextEph, err := processor.NextEphemeral(blinding)
if err != nil {
return nil, err
}

return &ForwardingInfo{
Network: BitcoinNetwork,
NextHop: nextHop,
AmountToForward: fwdAmt,
OutgoingCTLV: expiry,
NextBlinding: nextEph,
}, nil
}
}

// 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 //nolint:dupword
// total_fees = base_fee + amt_to_forward*(fee_rate/1000000)
//
// After substitution and some massaging you will get:
//
// amt_to_forward = (incoming_amount - base_fee) /
// ( 1 + fee_rate / 1000000 )
//
// 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)
func calculateForwardingAmount(incomingAmount lnwire.MilliSatoshi, baseFee,
proportionalFee uint32) (lnwire.MilliSatoshi, error) {

// proportionalParts is the number of parts that our proportional fee
// is expressed per.
var proportionalParts uint64 = 1_000_000

// Sanity check to prevent overflow.
if incomingAmount < lnwire.MilliSatoshi(baseFee) {
return 0, fmt.Errorf("incoming amount: %v < base fee: %v",
incomingAmount, baseFee)
}

ceiling := ((uint64(incomingAmount) - uint64(baseFee)) +
(1 + uint64(proportionalFee)/proportionalParts) - 1) /
(1 + uint64(proportionalFee)/proportionalParts)

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
Expand Down Expand Up @@ -146,53 +315,9 @@ func (p *OnionProcessor) Stop() error {
return nil
}

// DecodeHopIterator 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) DecodeHopIterator(r io.Reader, rHash []byte,
incomingCltv uint32) (Iterator, lnwire.FailCode) {

onionPkt := &sphinx.OnionPacket{}
if err := onionPkt.Decode(r); err != nil {
switch err {
case sphinx.ErrInvalidOnionVersion:
return nil, lnwire.CodeInvalidOnionVersion
case sphinx.ErrInvalidOnionKey:
return nil, lnwire.CodeInvalidOnionKey
default:
log.Errorf("unable to decode onion packet: %v", err)
return nil, lnwire.CodeInvalidOnionKey
}
}

// 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.ProcessOnionPacket(
onionPkt, rHash, incomingCltv,
)
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 makeSphinxHopIterator(onionPkt, sphinxPacket), lnwire.CodeNone
}

// 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.
// 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) (
Iterator, error) {

Expand All @@ -206,21 +331,28 @@ func (p *OnionProcessor) ReconstructHopIterator(r io.Reader, rHash []byte) (
// 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)
//
// TODO(carla): contract court will need to be able to pass the
// blinding point back in here (requires interface update).
sphinxPacket, err := p.router.ReconstructOnionPacket(
onionPkt, rHash, // Blinding Opts
)
if err != nil {
return nil, err
}

return makeSphinxHopIterator(onionPkt, sphinxPacket), nil
return makeSphinxHopIterator(onionPkt, sphinxPacket, nil), 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
OnionReader io.Reader
RHash []byte
IncomingCltv uint32
IncomingAmount lnwire.MilliSatoshi
BlindingPoint *btcec.PublicKey
}

// DecodeHopIteratorResponse encapsulates the outcome of a batched sphinx onion
Expand Down Expand Up @@ -370,7 +502,15 @@ func (p *OnionProcessor) DecodeHopIterators(id []byte,

// Finally, construct a hop iterator from our processed sphinx
// packet, simultaneously caching the original onion packet.
resp.HopIterator = makeSphinxHopIterator(&onionPkts[i], &packets[i])
resp.HopIterator = makeSphinxHopIterator(
&onionPkts[i], &packets[i], MakeBlindingKit(
p.router, reqs[i].BlindingPoint,
// We are the last hop if the next hop if the
// processed packet's action is to exit.
packets[i].Action == sphinx.ExitNode,
reqs[i].IncomingAmount, reqs[i].IncomingCltv,
),
)
}

return resps, nil
Expand Down
45 changes: 45 additions & 0 deletions htlcswitch/hop/iterator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,48 @@ func TestSphinxHopIteratorForwardingInstructions(t *testing.T) {
}
}
}

// TestForwardingAmountCalc tests calculation of forwarding amounts from the
// hop's forwarding parameters.
func TestForwardingAmountCalc(t *testing.T) {
t.Parallel()

tests := []struct {
name string
incomingAmount lnwire.MilliSatoshi
baseFee uint32
proportional uint32
forwardAmount lnwire.MilliSatoshi
expectErr bool
}{
{
name: "overflow",
incomingAmount: 10,
baseFee: 100,
expectErr: true,
},
{
name: "ok",
incomingAmount: 100_000,
baseFee: 1000,
proportional: 10,
forwardAmount: 99000,
},
}

for _, testCase := range tests {
testCase := testCase

t.Run(testCase.name, func(t *testing.T) {
t.Parallel()

actual, err := calculateForwardingAmount(
testCase.incomingAmount, testCase.baseFee,
testCase.proportional,
)

require.Equal(t, testCase.expectErr, err != nil)
require.Equal(t, testCase.forwardAmount, actual)
})
}
}
Loading