mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-05 18:10:27 +01:00
Merge pull request #9316 from ziggie1984/fix-blindedpath-mc
routing: fix mc blinded path behaviour.
This commit is contained in:
commit
7a3401555c
9 changed files with 576 additions and 117 deletions
|
@ -26,6 +26,9 @@
|
||||||
* [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/9324) to prevent
|
* [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/9324) to prevent
|
||||||
potential deadlocks when LND depends on external components (e.g. aux
|
potential deadlocks when LND depends on external components (e.g. aux
|
||||||
components, hooks).
|
components, hooks).
|
||||||
|
|
||||||
|
* [Make sure blinded payment failures are handled correctly in the mission
|
||||||
|
controller](https://github.com/lightningnetwork/lnd/pull/9316).
|
||||||
|
|
||||||
# New Features
|
# New Features
|
||||||
|
|
||||||
|
@ -121,4 +124,5 @@ types in a series of changes:
|
||||||
* George Tsagkarelis
|
* George Tsagkarelis
|
||||||
* Olaoluwa Osuntokun
|
* Olaoluwa Osuntokun
|
||||||
* Oliver Gugger
|
* Oliver Gugger
|
||||||
|
* Ziggie
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
* [Make the contract resolutions for the channel arbitrator optional](
|
* [Make the contract resolutions for the channel arbitrator optional](
|
||||||
https://github.com/lightningnetwork/lnd/pull/9253)
|
https://github.com/lightningnetwork/lnd/pull/9253)
|
||||||
|
|
||||||
* [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/9322) that caused
|
* [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/9322) that caused
|
||||||
estimateroutefee to ignore the default payment timeout.
|
estimateroutefee to ignore the default payment timeout.
|
||||||
|
|
||||||
# New Features
|
# New Features
|
||||||
|
@ -80,7 +80,7 @@
|
||||||
## RPC Additions
|
## RPC Additions
|
||||||
|
|
||||||
* [Add a new rpc endpoint](https://github.com/lightningnetwork/lnd/pull/8843)
|
* [Add a new rpc endpoint](https://github.com/lightningnetwork/lnd/pull/8843)
|
||||||
`BumpForceCloseFee` which moves the functionality soley available in the
|
`BumpForceCloseFee` which moves the functionality solely available in the
|
||||||
`lncli` to LND hence making it more universal.
|
`lncli` to LND hence making it more universal.
|
||||||
|
|
||||||
* [The `walletrpc.FundPsbt` RPC method now has an option to specify the fee as
|
* [The `walletrpc.FundPsbt` RPC method now has an option to specify the fee as
|
||||||
|
|
|
@ -29,9 +29,9 @@ var (
|
||||||
SequenceLockTimeSeconds = uint32(1 << 22)
|
SequenceLockTimeSeconds = uint32(1 << 22)
|
||||||
)
|
)
|
||||||
|
|
||||||
// mustParsePubKey parses a hex encoded public key string into a public key and
|
// MustParsePubKey parses a hex encoded public key string into a public key and
|
||||||
// panic if parsing fails.
|
// panic if parsing fails.
|
||||||
func mustParsePubKey(pubStr string) btcec.PublicKey {
|
func MustParsePubKey(pubStr string) btcec.PublicKey {
|
||||||
pubBytes, err := hex.DecodeString(pubStr)
|
pubBytes, err := hex.DecodeString(pubStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -55,7 +55,7 @@ var (
|
||||||
// https://github.com/lightninglabs/lightning-node-connect/tree/
|
// https://github.com/lightninglabs/lightning-node-connect/tree/
|
||||||
// master/mailbox/numsgen, with the seed phrase "Lightning Simple
|
// master/mailbox/numsgen, with the seed phrase "Lightning Simple
|
||||||
// Taproot".
|
// Taproot".
|
||||||
TaprootNUMSKey = mustParsePubKey(TaprootNUMSHex)
|
TaprootNUMSKey = MustParsePubKey(TaprootNUMSHex)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Signature is an interface for objects that can populate signatures during
|
// Signature is an interface for objects that can populate signatures during
|
||||||
|
|
|
@ -1,17 +1,27 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec/v2"
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
sphinx "github.com/lightningnetwork/lightning-onion"
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||||
"github.com/lightningnetwork/lnd/fn/v2"
|
|
||||||
"github.com/lightningnetwork/lnd/graph/db/models"
|
"github.com/lightningnetwork/lnd/graph/db/models"
|
||||||
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/routing/route"
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// BlindedPathNUMSHex is the hex encoded version of the blinded path target
|
||||||
|
// NUMs key (in compressed format) which has no known private key.
|
||||||
|
// This was generated using the following script:
|
||||||
|
// https://github.com/lightninglabs/lightning-node-connect/tree/master/
|
||||||
|
// mailbox/numsgen, with the seed phrase "Lightning Blinded Path".
|
||||||
|
const BlindedPathNUMSHex = "02667a98ef82ecb522f803b17a74f14508a48b25258f9831" +
|
||||||
|
"dd6e95f5e299dfd54e"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrNoBlindedPath is returned when the blinded path in a blinded
|
// ErrNoBlindedPath is returned when the blinded path in a blinded
|
||||||
// payment is missing.
|
// payment is missing.
|
||||||
|
@ -25,6 +35,14 @@ var (
|
||||||
// ErrHTLCRestrictions is returned when a blinded path has invalid
|
// ErrHTLCRestrictions is returned when a blinded path has invalid
|
||||||
// HTLC maximum and minimum values.
|
// HTLC maximum and minimum values.
|
||||||
ErrHTLCRestrictions = errors.New("invalid htlc minimum and maximum")
|
ErrHTLCRestrictions = errors.New("invalid htlc minimum and maximum")
|
||||||
|
|
||||||
|
// BlindedPathNUMSKey is a NUMS key (nothing up my sleeves number) that
|
||||||
|
// has no known private key.
|
||||||
|
BlindedPathNUMSKey = input.MustParsePubKey(BlindedPathNUMSHex)
|
||||||
|
|
||||||
|
// CompressedBlindedPathNUMSKey is the compressed version of the
|
||||||
|
// BlindedPathNUMSKey.
|
||||||
|
CompressedBlindedPathNUMSKey = BlindedPathNUMSKey.SerializeCompressed()
|
||||||
)
|
)
|
||||||
|
|
||||||
// BlindedPaymentPathSet groups the data we need to handle sending to a set of
|
// BlindedPaymentPathSet groups the data we need to handle sending to a set of
|
||||||
|
@ -70,7 +88,9 @@ type BlindedPaymentPathSet struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBlindedPaymentPathSet constructs a new BlindedPaymentPathSet from a set of
|
// NewBlindedPaymentPathSet constructs a new BlindedPaymentPathSet from a set of
|
||||||
// BlindedPayments.
|
// BlindedPayments. For blinded paths which have more than one single hop a
|
||||||
|
// dummy hop via a NUMS key is appeneded to allow for MPP path finding via
|
||||||
|
// multiple blinded paths.
|
||||||
func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet,
|
func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet,
|
||||||
error) {
|
error) {
|
||||||
|
|
||||||
|
@ -103,36 +123,53 @@ func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derive an ephemeral target priv key that will be injected into each
|
// Deep copy the paths to avoid mutating the original paths.
|
||||||
// blinded path final hop.
|
pathSet := make([]*BlindedPayment, len(paths))
|
||||||
targetPriv, err := btcec.NewPrivateKey()
|
for i, path := range paths {
|
||||||
if err != nil {
|
pathSet[i] = path.deepCopy()
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
targetPub := targetPriv.PubKey()
|
|
||||||
|
|
||||||
var (
|
// For blinded paths we use the NUMS key as a target if the blinded
|
||||||
pathSet = paths
|
// path has more hops than just the introduction node.
|
||||||
finalCLTVDelta uint16
|
targetPub := &BlindedPathNUMSKey
|
||||||
)
|
|
||||||
// If any provided blinded path only has a single hop (ie, the
|
var finalCLTVDelta uint16
|
||||||
// destination node is also the introduction node), then we discard all
|
|
||||||
// other paths since we know the real pub key of the destination node.
|
// In case the paths do NOT include a single hop route we append a
|
||||||
// We also then set the final CLTV delta to the path's delta since
|
// dummy hop via a NUMS key to allow for MPP path finding via multiple
|
||||||
// there are no other edge hints that will account for it. For a single
|
// blinded paths. A unified target is needed to use all blinded paths
|
||||||
// hop path, there is also no need for the pseudo target pub key
|
// during the payment lifecycle. A dummy hop is solely added for the
|
||||||
// replacement, so our target pub key in this case just remains the
|
// path finding process and is removed after the path is found. This
|
||||||
// real introduction node ID.
|
// ensures that we still populate the mission control with the correct
|
||||||
for _, path := range paths {
|
// data and also respect these mc entries when looking for a path.
|
||||||
if len(path.BlindedPath.BlindedHops) != 1 {
|
for _, path := range pathSet {
|
||||||
continue
|
pathLength := len(path.BlindedPath.BlindedHops)
|
||||||
|
|
||||||
|
// If any provided blinded path only has a single hop (ie, the
|
||||||
|
// destination node is also the introduction node), then we
|
||||||
|
// discard all other paths since we know the real pub key of the
|
||||||
|
// destination node. We also then set the final CLTV delta to
|
||||||
|
// the path's delta since there are no other edge hints that
|
||||||
|
// will account for it.
|
||||||
|
if pathLength == 1 {
|
||||||
|
pathSet = []*BlindedPayment{path}
|
||||||
|
finalCLTVDelta = path.CltvExpiryDelta
|
||||||
|
targetPub = path.BlindedPath.IntroductionPoint
|
||||||
|
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
pathSet = []*BlindedPayment{path}
|
lastHop := path.BlindedPath.BlindedHops[pathLength-1]
|
||||||
finalCLTVDelta = path.CltvExpiryDelta
|
path.BlindedPath.BlindedHops = append(
|
||||||
targetPub = path.BlindedPath.IntroductionPoint
|
path.BlindedPath.BlindedHops,
|
||||||
|
&sphinx.BlindedHopInfo{
|
||||||
break
|
BlindedNodePub: &BlindedPathNUMSKey,
|
||||||
|
// We add the last hop's cipher text so that
|
||||||
|
// the payload size of the final hop is equal
|
||||||
|
// to the real last hop.
|
||||||
|
CipherText: lastHop.CipherText,
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &BlindedPaymentPathSet{
|
return &BlindedPaymentPathSet{
|
||||||
|
@ -198,21 +235,33 @@ func (s *BlindedPaymentPathSet) FinalCLTVDelta() uint16 {
|
||||||
// LargestLastHopPayloadPath returns the BlindedPayment in the set that has the
|
// LargestLastHopPayloadPath returns the BlindedPayment in the set that has the
|
||||||
// largest last-hop payload. This is to be used for onion size estimation in
|
// largest last-hop payload. This is to be used for onion size estimation in
|
||||||
// path finding.
|
// path finding.
|
||||||
func (s *BlindedPaymentPathSet) LargestLastHopPayloadPath() *BlindedPayment {
|
func (s *BlindedPaymentPathSet) LargestLastHopPayloadPath() (*BlindedPayment,
|
||||||
|
error) {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
largestPath *BlindedPayment
|
largestPath *BlindedPayment
|
||||||
currentMax int
|
currentMax int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if len(s.paths) == 0 {
|
||||||
|
return nil, fmt.Errorf("no blinded paths in the set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We set the largest path to make sure we always return a path even
|
||||||
|
// if the cipher text is empty.
|
||||||
|
largestPath = s.paths[0]
|
||||||
|
|
||||||
for _, path := range s.paths {
|
for _, path := range s.paths {
|
||||||
numHops := len(path.BlindedPath.BlindedHops)
|
numHops := len(path.BlindedPath.BlindedHops)
|
||||||
lastHop := path.BlindedPath.BlindedHops[numHops-1]
|
lastHop := path.BlindedPath.BlindedHops[numHops-1]
|
||||||
|
|
||||||
if len(lastHop.CipherText) > currentMax {
|
if len(lastHop.CipherText) > currentMax {
|
||||||
largestPath = path
|
largestPath = path
|
||||||
|
currentMax = len(lastHop.CipherText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return largestPath
|
return largestPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToRouteHints converts the blinded path payment set into a RouteHints map so
|
// ToRouteHints converts the blinded path payment set into a RouteHints map so
|
||||||
|
@ -222,7 +271,7 @@ func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
|
||||||
hints := make(RouteHints)
|
hints := make(RouteHints)
|
||||||
|
|
||||||
for _, path := range s.paths {
|
for _, path := range s.paths {
|
||||||
pathHints, err := path.toRouteHints(fn.Some(s.targetPubKey))
|
pathHints, err := path.toRouteHints()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -239,6 +288,12 @@ func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
|
||||||
return hints, nil
|
return hints, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsBlindedRouteNUMSTargetKey returns true if the given public key is the
|
||||||
|
// NUMS key used as a target for blinded path final hops.
|
||||||
|
func IsBlindedRouteNUMSTargetKey(pk []byte) bool {
|
||||||
|
return bytes.Equal(pk, CompressedBlindedPathNUMSKey)
|
||||||
|
}
|
||||||
|
|
||||||
// BlindedPayment provides the path and payment parameters required to send a
|
// BlindedPayment provides the path and payment parameters required to send a
|
||||||
// payment along a blinded path.
|
// payment along a blinded path.
|
||||||
type BlindedPayment struct {
|
type BlindedPayment struct {
|
||||||
|
@ -291,6 +346,22 @@ func (b *BlindedPayment) Validate() error {
|
||||||
b.HtlcMaximum, b.HtlcMinimum)
|
b.HtlcMaximum, b.HtlcMinimum)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, hop := range b.BlindedPath.BlindedHops {
|
||||||
|
// The first hop of the blinded path does not necessarily have
|
||||||
|
// blinded node pub key because it is the introduction point.
|
||||||
|
if hop.BlindedNodePub == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if IsBlindedRouteNUMSTargetKey(
|
||||||
|
hop.BlindedNodePub.SerializeCompressed(),
|
||||||
|
) {
|
||||||
|
|
||||||
|
return fmt.Errorf("blinded path cannot include NUMS "+
|
||||||
|
"key: %s", BlindedPathNUMSHex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,11 +372,8 @@ func (b *BlindedPayment) Validate() error {
|
||||||
// effectively the final_cltv_delta for the receiving introduction node). In
|
// effectively the final_cltv_delta for the receiving introduction node). In
|
||||||
// the case of multiple blinded hops, CLTV delta is fully accounted for in the
|
// the case of multiple blinded hops, CLTV delta is fully accounted for in the
|
||||||
// hints (both for intermediate hops and the final_cltv_delta for the receiving
|
// hints (both for intermediate hops and the final_cltv_delta for the receiving
|
||||||
// node). The pseudoTarget, if provided, will be used to override the pub key
|
// node).
|
||||||
// of the destination node in the path.
|
func (b *BlindedPayment) toRouteHints() (RouteHints, error) {
|
||||||
func (b *BlindedPayment) toRouteHints(
|
|
||||||
pseudoTarget fn.Option[*btcec.PublicKey]) (RouteHints, error) {
|
|
||||||
|
|
||||||
// If we just have a single hop in our blinded route, it just contains
|
// If we just have a single hop in our blinded route, it just contains
|
||||||
// an introduction node (this is a valid path according to the spec).
|
// an introduction node (this is a valid path according to the spec).
|
||||||
// Since we have the un-blinded node ID for the introduction node, we
|
// Since we have the un-blinded node ID for the introduction node, we
|
||||||
|
@ -393,16 +461,77 @@ func (b *BlindedPayment) toRouteHints(
|
||||||
hints[fromNode] = []AdditionalEdge{lastEdge}
|
hints[fromNode] = []AdditionalEdge{lastEdge}
|
||||||
}
|
}
|
||||||
|
|
||||||
pseudoTarget.WhenSome(func(key *btcec.PublicKey) {
|
|
||||||
// For the very last hop on the path, switch out the ToNodePub
|
|
||||||
// for the pseudo target pub key.
|
|
||||||
lastEdge.policy.ToNodePubKey = func() route.Vertex {
|
|
||||||
return route.NewVertex(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then override the final hint with this updated edge.
|
|
||||||
hints[fromNode] = []AdditionalEdge{lastEdge}
|
|
||||||
})
|
|
||||||
|
|
||||||
return hints, nil
|
return hints, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deepCopy returns a deep copy of the BlindedPayment.
|
||||||
|
func (b *BlindedPayment) deepCopy() *BlindedPayment {
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cpyPayment := &BlindedPayment{
|
||||||
|
BaseFee: b.BaseFee,
|
||||||
|
ProportionalFeeRate: b.ProportionalFeeRate,
|
||||||
|
CltvExpiryDelta: b.CltvExpiryDelta,
|
||||||
|
HtlcMinimum: b.HtlcMinimum,
|
||||||
|
HtlcMaximum: b.HtlcMaximum,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deep copy the BlindedPath if it exists
|
||||||
|
if b.BlindedPath != nil {
|
||||||
|
cpyPayment.BlindedPath = &sphinx.BlindedPath{
|
||||||
|
BlindedHops: make([]*sphinx.BlindedHopInfo,
|
||||||
|
len(b.BlindedPath.BlindedHops)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.BlindedPath.IntroductionPoint != nil {
|
||||||
|
cpyPayment.BlindedPath.IntroductionPoint =
|
||||||
|
copyPublicKey(b.BlindedPath.IntroductionPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.BlindedPath.BlindingPoint != nil {
|
||||||
|
cpyPayment.BlindedPath.BlindingPoint =
|
||||||
|
copyPublicKey(b.BlindedPath.BlindingPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy each blinded hop info.
|
||||||
|
for i, hop := range b.BlindedPath.BlindedHops {
|
||||||
|
if hop == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cpyHop := &sphinx.BlindedHopInfo{
|
||||||
|
CipherText: hop.CipherText,
|
||||||
|
}
|
||||||
|
|
||||||
|
if hop.BlindedNodePub != nil {
|
||||||
|
cpyHop.BlindedNodePub =
|
||||||
|
copyPublicKey(hop.BlindedNodePub)
|
||||||
|
}
|
||||||
|
|
||||||
|
cpyHop.CipherText = make([]byte, len(hop.CipherText))
|
||||||
|
copy(cpyHop.CipherText, hop.CipherText)
|
||||||
|
|
||||||
|
cpyPayment.BlindedPath.BlindedHops[i] = cpyHop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deep copy the Features if they exist
|
||||||
|
if b.Features != nil {
|
||||||
|
cpyPayment.Features = b.Features.Clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
return cpyPayment
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyPublicKey makes a deep copy of a public key.
|
||||||
|
//
|
||||||
|
// TODO(ziggie): Remove this function if this is available in the btcec library.
|
||||||
|
func copyPublicKey(pk *btcec.PublicKey) *btcec.PublicKey {
|
||||||
|
var result secp256k1.JacobianPoint
|
||||||
|
pk.AsJacobian(&result)
|
||||||
|
result.ToAffine()
|
||||||
|
|
||||||
|
return btcec.NewPublicKey(&result.X, &result.Y)
|
||||||
|
}
|
||||||
|
|
|
@ -2,11 +2,11 @@ package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec/v2"
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
sphinx "github.com/lightningnetwork/lightning-onion"
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||||
"github.com/lightningnetwork/lnd/fn/v2"
|
|
||||||
"github.com/lightningnetwork/lnd/graph/db/models"
|
"github.com/lightningnetwork/lnd/graph/db/models"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/routing/route"
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
|
@ -129,7 +129,7 @@ func TestBlindedPaymentToHints(t *testing.T) {
|
||||||
HtlcMaximum: htlcMax,
|
HtlcMaximum: htlcMax,
|
||||||
Features: features,
|
Features: features,
|
||||||
}
|
}
|
||||||
hints, err := blindedPayment.toRouteHints(fn.None[*btcec.PublicKey]())
|
hints, err := blindedPayment.toRouteHints()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Nil(t, hints)
|
require.Nil(t, hints)
|
||||||
|
|
||||||
|
@ -184,7 +184,7 @@ func TestBlindedPaymentToHints(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
actual, err := blindedPayment.toRouteHints(fn.None[*btcec.PublicKey]())
|
actual, err := blindedPayment.toRouteHints()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, len(expected), len(actual))
|
require.Equal(t, len(expected), len(actual))
|
||||||
|
@ -218,3 +218,63 @@ func TestBlindedPaymentToHints(t *testing.T) {
|
||||||
require.Equal(t, expectedHint[0], actualHint[0])
|
require.Equal(t, expectedHint[0], actualHint[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBlindedPaymentDeepCopy tests the deep copy method of the BLindedPayment
|
||||||
|
// struct.
|
||||||
|
//
|
||||||
|
// TODO(ziggie): Make this a property test instead.
|
||||||
|
func TestBlindedPaymentDeepCopy(t *testing.T) {
|
||||||
|
_, pkBlind1 := btcec.PrivKeyFromBytes([]byte{1})
|
||||||
|
_, blindingPoint := btcec.PrivKeyFromBytes([]byte{2})
|
||||||
|
_, pkBlind2 := btcec.PrivKeyFromBytes([]byte{3})
|
||||||
|
|
||||||
|
// Create a test BlindedPayment with non-nil fields
|
||||||
|
original := &BlindedPayment{
|
||||||
|
BaseFee: 1000,
|
||||||
|
ProportionalFeeRate: 2000,
|
||||||
|
CltvExpiryDelta: 144,
|
||||||
|
HtlcMinimum: 1000,
|
||||||
|
HtlcMaximum: 1000000,
|
||||||
|
Features: lnwire.NewFeatureVector(nil, nil),
|
||||||
|
BlindedPath: &sphinx.BlindedPath{
|
||||||
|
IntroductionPoint: pkBlind1,
|
||||||
|
BlindingPoint: blindingPoint,
|
||||||
|
BlindedHops: []*sphinx.BlindedHopInfo{
|
||||||
|
{
|
||||||
|
BlindedNodePub: pkBlind2,
|
||||||
|
CipherText: []byte("test cipher"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a deep copy
|
||||||
|
cpyPayment := original.deepCopy()
|
||||||
|
|
||||||
|
// Test 1: Verify the copy is not the same pointer
|
||||||
|
if cpyPayment == original {
|
||||||
|
t.Fatal("deepCopy returned same pointer")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all fields are equal
|
||||||
|
if !reflect.DeepEqual(original, cpyPayment) {
|
||||||
|
t.Fatal("copy is not equal to original")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify the copy and verify it doesn't affect the original
|
||||||
|
cpyPayment.BaseFee = 2000
|
||||||
|
cpyPayment.BlindedPath.BlindedHops[0].CipherText = []byte("modified")
|
||||||
|
|
||||||
|
require.NotEqual(t, original.BaseFee, cpyPayment.BaseFee)
|
||||||
|
|
||||||
|
require.NotEqual(
|
||||||
|
t,
|
||||||
|
original.BlindedPath.BlindedHops[0].CipherText,
|
||||||
|
cpyPayment.BlindedPath.BlindedHops[0].CipherText,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify nil handling.
|
||||||
|
var nilPayment *BlindedPayment
|
||||||
|
nilCopy := nilPayment.deepCopy()
|
||||||
|
require.Nil(t, nilCopy)
|
||||||
|
}
|
||||||
|
|
|
@ -158,6 +158,32 @@ func newRoute(sourceVertex route.Vertex,
|
||||||
)
|
)
|
||||||
|
|
||||||
pathLength := len(pathEdges)
|
pathLength := len(pathEdges)
|
||||||
|
|
||||||
|
// When paying to a blinded route we might have appended a dummy hop at
|
||||||
|
// the end to make MPP payments possible via all paths of the blinded
|
||||||
|
// route set. We always append a dummy hop when the internal pathfiner
|
||||||
|
// looks for a route to a blinded path which is at least one hop long
|
||||||
|
// (excluding the introduction point). We add this dummy hop so that
|
||||||
|
// we search for a universal target but also respect potential mc
|
||||||
|
// entries which might already be present for a particular blinded path.
|
||||||
|
// However when constructing the Sphinx packet we need to remove this
|
||||||
|
// dummy hop again which we do here.
|
||||||
|
//
|
||||||
|
// NOTE: The path length is always at least 1 because there must be one
|
||||||
|
// edge from the source to the destination. However we check for > 0
|
||||||
|
// just for robustness here.
|
||||||
|
if blindedPathSet != nil && pathLength > 0 {
|
||||||
|
finalBlindedPubKey := pathEdges[pathLength-1].policy.
|
||||||
|
ToNodePubKey()
|
||||||
|
|
||||||
|
if IsBlindedRouteNUMSTargetKey(finalBlindedPubKey[:]) {
|
||||||
|
// If the last hop is the NUMS key for blinded paths, we
|
||||||
|
// remove the dummy hop from the route.
|
||||||
|
pathEdges = pathEdges[:pathLength-1]
|
||||||
|
pathLength--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for i := pathLength - 1; i >= 0; i-- {
|
for i := pathLength - 1; i >= 0; i-- {
|
||||||
// Now we'll start to calculate the items within the per-hop
|
// Now we'll start to calculate the items within the per-hop
|
||||||
// payload for the hop this edge is leading to.
|
// payload for the hop this edge is leading to.
|
||||||
|
@ -319,10 +345,6 @@ func newRoute(sourceVertex route.Vertex,
|
||||||
dataIndex = 0
|
dataIndex = 0
|
||||||
|
|
||||||
blindedPath = blindedPayment.BlindedPath
|
blindedPath = blindedPayment.BlindedPath
|
||||||
numHops = len(blindedPath.BlindedHops)
|
|
||||||
realFinal = blindedPath.BlindedHops[numHops-1].
|
|
||||||
BlindedNodePub
|
|
||||||
|
|
||||||
introVertex = route.NewVertex(
|
introVertex = route.NewVertex(
|
||||||
blindedPath.IntroductionPoint,
|
blindedPath.IntroductionPoint,
|
||||||
)
|
)
|
||||||
|
@ -350,11 +372,6 @@ func newRoute(sourceVertex route.Vertex,
|
||||||
if i != len(hops)-1 {
|
if i != len(hops)-1 {
|
||||||
hop.AmtToForward = 0
|
hop.AmtToForward = 0
|
||||||
hop.OutgoingTimeLock = 0
|
hop.OutgoingTimeLock = 0
|
||||||
} else {
|
|
||||||
// For the final hop, we swap out the pub key
|
|
||||||
// bytes to the original destination node pub
|
|
||||||
// key for that payment path.
|
|
||||||
hop.PubKeyBytes = route.NewVertex(realFinal)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dataIndex++
|
dataIndex++
|
||||||
|
@ -683,7 +700,10 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
||||||
|
|
||||||
// The payload size of the final hop differ from intermediate hops
|
// The payload size of the final hop differ from intermediate hops
|
||||||
// and depends on whether the destination is blinded or not.
|
// and depends on whether the destination is blinded or not.
|
||||||
lastHopPayloadSize := lastHopPayloadSize(r, finalHtlcExpiry, amt)
|
lastHopPayloadSize, err := lastHopPayloadSize(r, finalHtlcExpiry, amt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
// We can't always assume that the end destination is publicly
|
// We can't always assume that the end destination is publicly
|
||||||
// advertised to the network so we'll manually include the target node.
|
// advertised to the network so we'll manually include the target node.
|
||||||
|
@ -901,6 +921,13 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
||||||
// included. If we are coming from the source hop, the payload
|
// included. If we are coming from the source hop, the payload
|
||||||
// size is zero, because the original htlc isn't in the onion
|
// size is zero, because the original htlc isn't in the onion
|
||||||
// blob.
|
// blob.
|
||||||
|
//
|
||||||
|
// NOTE: For blinded paths with the NUMS key as the last hop,
|
||||||
|
// the payload size accounts for this dummy hop which is of
|
||||||
|
// the same size as the real last hop. So we account for a
|
||||||
|
// bigger size than the route is however we accept this
|
||||||
|
// little inaccuracy here because we are over estimating by
|
||||||
|
// 1 hop.
|
||||||
var payloadSize uint64
|
var payloadSize uint64
|
||||||
if fromVertex != source {
|
if fromVertex != source {
|
||||||
// In case the unifiedEdge does not have a payload size
|
// In case the unifiedEdge does not have a payload size
|
||||||
|
@ -1409,11 +1436,15 @@ func getProbabilityBasedDist(weight int64, probability float64,
|
||||||
// It depends on the tlv types which are present and also whether the hop is
|
// It depends on the tlv types which are present and also whether the hop is
|
||||||
// part of a blinded route or not.
|
// part of a blinded route or not.
|
||||||
func lastHopPayloadSize(r *RestrictParams, finalHtlcExpiry int32,
|
func lastHopPayloadSize(r *RestrictParams, finalHtlcExpiry int32,
|
||||||
amount lnwire.MilliSatoshi) uint64 {
|
amount lnwire.MilliSatoshi) (uint64, error) {
|
||||||
|
|
||||||
if r.BlindedPaymentPathSet != nil {
|
if r.BlindedPaymentPathSet != nil {
|
||||||
paymentPath := r.BlindedPaymentPathSet.
|
paymentPath, err := r.BlindedPaymentPathSet.
|
||||||
LargestLastHopPayloadPath()
|
LargestLastHopPayloadPath()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
blindedPath := paymentPath.BlindedPath.BlindedHops
|
blindedPath := paymentPath.BlindedPath.BlindedHops
|
||||||
blindedPoint := paymentPath.BlindedPath.BlindingPoint
|
blindedPoint := paymentPath.BlindedPath.BlindingPoint
|
||||||
|
|
||||||
|
@ -1428,7 +1459,7 @@ func lastHopPayloadSize(r *RestrictParams, finalHtlcExpiry int32,
|
||||||
}
|
}
|
||||||
|
|
||||||
// The final hop does not have a short chanID set.
|
// The final hop does not have a short chanID set.
|
||||||
return finalHop.PayloadSize(0)
|
return finalHop.PayloadSize(0), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var mpp *record.MPP
|
var mpp *record.MPP
|
||||||
|
@ -1454,7 +1485,7 @@ func lastHopPayloadSize(r *RestrictParams, finalHtlcExpiry int32,
|
||||||
}
|
}
|
||||||
|
|
||||||
// The final hop does not have a short chanID set.
|
// The final hop does not have a short chanID set.
|
||||||
return finalHop.PayloadSize(0)
|
return finalHop.PayloadSize(0), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// overflowSafeAdd adds two MilliSatoshi values and returns the result. If an
|
// overflowSafeAdd adds two MilliSatoshi values and returns the result. If an
|
||||||
|
|
|
@ -765,6 +765,9 @@ func TestPathFinding(t *testing.T) {
|
||||||
}, {
|
}, {
|
||||||
name: "path finding with additional edges",
|
name: "path finding with additional edges",
|
||||||
fn: runPathFindingWithAdditionalEdges,
|
fn: runPathFindingWithAdditionalEdges,
|
||||||
|
}, {
|
||||||
|
name: "path finding with duplicate blinded hop",
|
||||||
|
fn: runPathFindingWithBlindedPathDuplicateHop,
|
||||||
}, {
|
}, {
|
||||||
name: "path finding with redundant additional edges",
|
name: "path finding with redundant additional edges",
|
||||||
fn: runPathFindingWithRedundantAdditionalEdges,
|
fn: runPathFindingWithRedundantAdditionalEdges,
|
||||||
|
@ -1265,6 +1268,107 @@ func runPathFindingWithAdditionalEdges(t *testing.T, useCache bool) {
|
||||||
assertExpectedPath(t, graph.aliasMap, path, "songoku", "doge")
|
assertExpectedPath(t, graph.aliasMap, path, "songoku", "doge")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runPathFindingWithBlindedPathDuplicateHop tests that in case a blinded path
|
||||||
|
// has duplicate hops that the path finding algorithm does not fail or behave
|
||||||
|
// incorrectly. This can happen because the creator of the blinded path can
|
||||||
|
// specify the same hop multiple times and this will only be detected at the
|
||||||
|
// forwarding nodes, so it is important that we can handle this case.
|
||||||
|
func runPathFindingWithBlindedPathDuplicateHop(t *testing.T, useCache bool) {
|
||||||
|
graph, err := parseTestGraph(t, useCache, basicGraphFilePath)
|
||||||
|
require.NoError(t, err, "unable to create graph")
|
||||||
|
|
||||||
|
sourceNode, err := graph.graph.SourceNode()
|
||||||
|
require.NoError(t, err, "unable to fetch source node")
|
||||||
|
|
||||||
|
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
||||||
|
|
||||||
|
songokuPubKeyBytes := graph.aliasMap["songoku"]
|
||||||
|
songokuPubKey, err := btcec.ParsePubKey(songokuPubKeyBytes[:])
|
||||||
|
require.NoError(t, err, "unable to parse public key from bytes")
|
||||||
|
|
||||||
|
_, pkb1 := btcec.PrivKeyFromBytes([]byte{2})
|
||||||
|
_, pkb2 := btcec.PrivKeyFromBytes([]byte{3})
|
||||||
|
_, blindedPoint := btcec.PrivKeyFromBytes([]byte{5})
|
||||||
|
|
||||||
|
sizeEncryptedData := 100
|
||||||
|
cipherText := bytes.Repeat(
|
||||||
|
[]byte{1}, sizeEncryptedData,
|
||||||
|
)
|
||||||
|
|
||||||
|
vb1 := route.NewVertex(pkb1)
|
||||||
|
vb2 := route.NewVertex(pkb2)
|
||||||
|
|
||||||
|
// Payments to blinded paths always pay to the NUMS target key.
|
||||||
|
dummyTarget := route.NewVertex(&BlindedPathNUMSKey)
|
||||||
|
|
||||||
|
graph.aliasMap["pkb1"] = vb1
|
||||||
|
graph.aliasMap["pkb2"] = vb2
|
||||||
|
graph.aliasMap["dummyTarget"] = dummyTarget
|
||||||
|
|
||||||
|
// Create a blinded payment with duplicate hops and make sure the
|
||||||
|
// path finding algorithm can cope with that. We add blinded hop 2
|
||||||
|
// 3 times. The path finding algorithm should create a path with a
|
||||||
|
// single hop to pkb2 (the first entry).
|
||||||
|
blindedPayment := &BlindedPayment{
|
||||||
|
BlindedPath: &sphinx.BlindedPath{
|
||||||
|
IntroductionPoint: songokuPubKey,
|
||||||
|
BlindingPoint: blindedPoint,
|
||||||
|
BlindedHops: []*sphinx.BlindedHopInfo{
|
||||||
|
{
|
||||||
|
CipherText: cipherText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BlindedNodePub: pkb2,
|
||||||
|
CipherText: cipherText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BlindedNodePub: pkb1,
|
||||||
|
CipherText: cipherText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BlindedNodePub: pkb2,
|
||||||
|
CipherText: cipherText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BlindedNodePub: &BlindedPathNUMSKey,
|
||||||
|
CipherText: cipherText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
BlindedNodePub: pkb2,
|
||||||
|
CipherText: cipherText,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
HtlcMinimum: 1,
|
||||||
|
HtlcMaximum: 100_000_000,
|
||||||
|
CltvExpiryDelta: 140,
|
||||||
|
}
|
||||||
|
|
||||||
|
blindedPath, err := blindedPayment.toRouteHints()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
find := func(r *RestrictParams) (
|
||||||
|
[]*unifiedEdge, error) {
|
||||||
|
|
||||||
|
return dbFindPath(
|
||||||
|
graph.graph, blindedPath, &mockBandwidthHints{},
|
||||||
|
r, testPathFindingConfig,
|
||||||
|
sourceNode.PubKeyBytes, dummyTarget, paymentAmt,
|
||||||
|
0, 0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should now be able to find a path however not the chained path
|
||||||
|
// of the blinded hops.
|
||||||
|
path, err := find(noRestrictions)
|
||||||
|
require.NoError(t, err, "unable to create route to blinded path")
|
||||||
|
|
||||||
|
// The path should represent the following hops:
|
||||||
|
// source node -> songoku -> pkb2 -> dummyTarget
|
||||||
|
assertExpectedPath(t, graph.aliasMap, path, "songoku", "pkb2",
|
||||||
|
"dummyTarget")
|
||||||
|
}
|
||||||
|
|
||||||
// runPathFindingWithRedundantAdditionalEdges asserts that we are able to find
|
// runPathFindingWithRedundantAdditionalEdges asserts that we are able to find
|
||||||
// paths to nodes ignoring additional edges that are already known by self node.
|
// paths to nodes ignoring additional edges that are already known by self node.
|
||||||
func runPathFindingWithRedundantAdditionalEdges(t *testing.T, useCache bool) {
|
func runPathFindingWithRedundantAdditionalEdges(t *testing.T, useCache bool) {
|
||||||
|
@ -3284,9 +3388,7 @@ func TestBlindedRouteConstruction(t *testing.T) {
|
||||||
// that make up the graph we'll give to route construction. The hints
|
// that make up the graph we'll give to route construction. The hints
|
||||||
// map is keyed by source node, so we can retrieve our blinded edges
|
// map is keyed by source node, so we can retrieve our blinded edges
|
||||||
// accordingly.
|
// accordingly.
|
||||||
blindedEdges, err := blindedPayment.toRouteHints(
|
blindedEdges, err := blindedPayment.toRouteHints()
|
||||||
fn.None[*btcec.PublicKey](),
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
carolDaveEdge := blindedEdges[carolVertex][0]
|
carolDaveEdge := blindedEdges[carolVertex][0]
|
||||||
|
@ -3415,32 +3517,48 @@ func TestLastHopPayloadSize(t *testing.T) {
|
||||||
customRecords = map[uint64][]byte{
|
customRecords = map[uint64][]byte{
|
||||||
record.CustomTypeStart: {1, 2, 3},
|
record.CustomTypeStart: {1, 2, 3},
|
||||||
}
|
}
|
||||||
sizeEncryptedData = 100
|
|
||||||
encrypedData = bytes.Repeat(
|
encrypedDataSmall = bytes.Repeat(
|
||||||
[]byte{1}, sizeEncryptedData,
|
[]byte{1}, 5,
|
||||||
)
|
)
|
||||||
_, blindedPoint = btcec.PrivKeyFromBytes([]byte{5})
|
encrypedDataLarge = bytes.Repeat(
|
||||||
paymentAddr = &[32]byte{1}
|
[]byte{1}, 100,
|
||||||
ampOptions = &Options{}
|
)
|
||||||
amtToForward = lnwire.MilliSatoshi(10000)
|
_, blindedPoint = btcec.PrivKeyFromBytes([]byte{5})
|
||||||
finalHopExpiry int32 = 144
|
paymentAddr = &[32]byte{1}
|
||||||
|
ampOptions = &Options{}
|
||||||
|
amtToForward = lnwire.MilliSatoshi(10000)
|
||||||
|
emptyEncryptedData = []byte{}
|
||||||
|
finalHopExpiry int32 = 144
|
||||||
|
|
||||||
oneHopPath = &sphinx.BlindedPath{
|
oneHopPath = &sphinx.BlindedPath{
|
||||||
BlindedHops: []*sphinx.BlindedHopInfo{
|
BlindedHops: []*sphinx.BlindedHopInfo{
|
||||||
{
|
{
|
||||||
CipherText: encrypedData,
|
CipherText: emptyEncryptedData,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
BlindingPoint: blindedPoint,
|
BlindingPoint: blindedPoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
twoHopPath = &sphinx.BlindedPath{
|
twoHopPathSmallHopSize = &sphinx.BlindedPath{
|
||||||
BlindedHops: []*sphinx.BlindedHopInfo{
|
BlindedHops: []*sphinx.BlindedHopInfo{
|
||||||
{
|
{
|
||||||
CipherText: encrypedData,
|
CipherText: encrypedDataLarge,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
CipherText: encrypedData,
|
CipherText: encrypedDataLarge,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
BlindingPoint: blindedPoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
twoHopPathLargeHopSize = &sphinx.BlindedPath{
|
||||||
|
BlindedHops: []*sphinx.BlindedHopInfo{
|
||||||
|
{
|
||||||
|
CipherText: encrypedDataSmall,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CipherText: encrypedDataSmall,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
BlindingPoint: blindedPoint,
|
BlindingPoint: blindedPoint,
|
||||||
|
@ -3453,15 +3571,19 @@ func TestLastHopPayloadSize(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
twoHopBlindedPayment, err := NewBlindedPaymentPathSet(
|
twoHopBlindedPayment, err := NewBlindedPaymentPathSet(
|
||||||
[]*BlindedPayment{{BlindedPath: twoHopPath}},
|
[]*BlindedPayment{
|
||||||
|
{BlindedPath: twoHopPathLargeHopSize},
|
||||||
|
{BlindedPath: twoHopPathSmallHopSize},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
restrictions *RestrictParams
|
restrictions *RestrictParams
|
||||||
finalHopExpiry int32
|
finalHopExpiry int32
|
||||||
amount lnwire.MilliSatoshi
|
amount lnwire.MilliSatoshi
|
||||||
|
expectedEncryptedData []byte
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Non blinded final hop",
|
name: "Non blinded final hop",
|
||||||
|
@ -3479,16 +3601,18 @@ func TestLastHopPayloadSize(t *testing.T) {
|
||||||
restrictions: &RestrictParams{
|
restrictions: &RestrictParams{
|
||||||
BlindedPaymentPathSet: oneHopBlindedPayment,
|
BlindedPaymentPathSet: oneHopBlindedPayment,
|
||||||
},
|
},
|
||||||
amount: amtToForward,
|
amount: amtToForward,
|
||||||
finalHopExpiry: finalHopExpiry,
|
finalHopExpiry: finalHopExpiry,
|
||||||
|
expectedEncryptedData: emptyEncryptedData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Blinded final hop of a two hop payment",
|
name: "Blinded final hop of a two hop payment",
|
||||||
restrictions: &RestrictParams{
|
restrictions: &RestrictParams{
|
||||||
BlindedPaymentPathSet: twoHopBlindedPayment,
|
BlindedPaymentPathSet: twoHopBlindedPayment,
|
||||||
},
|
},
|
||||||
amount: amtToForward,
|
amount: amtToForward,
|
||||||
finalHopExpiry: finalHopExpiry,
|
finalHopExpiry: finalHopExpiry,
|
||||||
|
expectedEncryptedData: encrypedDataLarge,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3512,16 +3636,23 @@ func TestLastHopPayloadSize(t *testing.T) {
|
||||||
|
|
||||||
var finalHop route.Hop
|
var finalHop route.Hop
|
||||||
if tc.restrictions.BlindedPaymentPathSet != nil {
|
if tc.restrictions.BlindedPaymentPathSet != nil {
|
||||||
path := tc.restrictions.BlindedPaymentPathSet.
|
bPSet := tc.restrictions.BlindedPaymentPathSet
|
||||||
LargestLastHopPayloadPath()
|
path, err := bPSet.LargestLastHopPayloadPath()
|
||||||
|
require.NotNil(t, path)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
blindedPath := path.BlindedPath.BlindedHops
|
blindedPath := path.BlindedPath.BlindedHops
|
||||||
blindedPoint := path.BlindedPath.BlindingPoint
|
blindedPoint := path.BlindedPath.BlindingPoint
|
||||||
|
lastHop := blindedPath[len(blindedPath)-1]
|
||||||
|
require.Equal(t, lastHop.CipherText,
|
||||||
|
tc.expectedEncryptedData)
|
||||||
|
|
||||||
//nolint:ll
|
//nolint:ll
|
||||||
finalHop = route.Hop{
|
finalHop = route.Hop{
|
||||||
AmtToForward: tc.amount,
|
AmtToForward: tc.amount,
|
||||||
OutgoingTimeLock: uint32(tc.finalHopExpiry),
|
OutgoingTimeLock: uint32(tc.finalHopExpiry),
|
||||||
EncryptedData: blindedPath[len(blindedPath)-1].CipherText,
|
EncryptedData: lastHop.CipherText,
|
||||||
}
|
}
|
||||||
if len(blindedPath) == 1 {
|
if len(blindedPath) == 1 {
|
||||||
finalHop.BlindingPoint = blindedPoint
|
finalHop.BlindingPoint = blindedPoint
|
||||||
|
@ -3541,11 +3672,11 @@ func TestLastHopPayloadSize(t *testing.T) {
|
||||||
payLoad, err := createHopPayload(finalHop, 0, true)
|
payLoad, err := createHopPayload(finalHop, 0, true)
|
||||||
require.NoErrorf(t, err, "failed to create hop payload")
|
require.NoErrorf(t, err, "failed to create hop payload")
|
||||||
|
|
||||||
expectedPayloadSize := lastHopPayloadSize(
|
expectedPayloadSize, err := lastHopPayloadSize(
|
||||||
tc.restrictions, tc.finalHopExpiry,
|
tc.restrictions, tc.finalHopExpiry,
|
||||||
tc.amount,
|
tc.amount,
|
||||||
)
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
require.Equal(
|
require.Equal(
|
||||||
t, expectedPayloadSize,
|
t, expectedPayloadSize,
|
||||||
uint64(payLoad.NumBytes()),
|
uint64(payLoad.NumBytes()),
|
||||||
|
|
|
@ -100,8 +100,27 @@ func interpretResult(rt *mcRoute,
|
||||||
|
|
||||||
// processSuccess processes a successful payment attempt.
|
// processSuccess processes a successful payment attempt.
|
||||||
func (i *interpretedResult) processSuccess(route *mcRoute) {
|
func (i *interpretedResult) processSuccess(route *mcRoute) {
|
||||||
// For successes, all nodes must have acted in the right way. Therefore
|
// For successes, all nodes must have acted in the right way.
|
||||||
// we mark all of them with a success result.
|
// Therefore we mark all of them with a success result. However we need
|
||||||
|
// to handle the blinded route part separately because for intermediate
|
||||||
|
// blinded nodes the amount field is set to zero so we use the receiver
|
||||||
|
// amount.
|
||||||
|
introIdx, isBlinded := introductionPointIndex(route)
|
||||||
|
if isBlinded {
|
||||||
|
// Report success for all the pairs until the introduction
|
||||||
|
// point.
|
||||||
|
i.successPairRange(route, 0, introIdx-1)
|
||||||
|
|
||||||
|
// Handle the blinded route part.
|
||||||
|
//
|
||||||
|
// NOTE: The introIdx index here does describe the node after
|
||||||
|
// the introduction point.
|
||||||
|
i.markBlindedRouteSuccess(route, introIdx)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark nodes as successful in the non-blinded case of the payment.
|
||||||
i.successPairRange(route, 0, len(route.hops.Val)-1)
|
i.successPairRange(route, 0, len(route.hops.Val)-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -517,11 +536,22 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate(route *mcRoute,
|
||||||
if introIdx == len(route.hops.Val)-1 {
|
if introIdx == len(route.hops.Val)-1 {
|
||||||
i.finalFailureReason = &reasonError
|
i.finalFailureReason = &reasonError
|
||||||
} else {
|
} else {
|
||||||
// If there are other hops between the recipient and
|
// We penalize the final hop of the blinded route which
|
||||||
// introduction node, then we just penalize the last
|
// is sufficient to not reuse this route again and is
|
||||||
// hop in the blinded route to minimize the storage of
|
// also more memory efficient because the other hops
|
||||||
// results for ephemeral keys.
|
// of the blinded path are ephemeral and will only be
|
||||||
i.failPairBalance(route, len(route.hops.Val)-1)
|
// used in conjunction with the final hop. Moreover we
|
||||||
|
// don't want to punish the introduction node because
|
||||||
|
// the blinded failure does not necessarily mean that
|
||||||
|
// the introduction node was at fault.
|
||||||
|
//
|
||||||
|
// TODO(ziggie): Make sure we only keep mc data for
|
||||||
|
// blinded paths, in both the success and failure case,
|
||||||
|
// in memory during the time of the payment and remove
|
||||||
|
// it afterwards. Blinded paths and their blinded hop
|
||||||
|
// keys are always changing per blinded route so there
|
||||||
|
// is no point in persisting this data.
|
||||||
|
i.failBlindedRoute(route)
|
||||||
}
|
}
|
||||||
|
|
||||||
// In all other cases, we penalize the reporting node. These are all
|
// In all other cases, we penalize the reporting node. These are all
|
||||||
|
@ -828,6 +858,41 @@ func (i *interpretedResult) successPairRange(rt *mcRoute, fromIdx, toIdx int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// failBlindedRoute marks a blinded route as failed for the specific amount to
|
||||||
|
// send by only punishing the last pair.
|
||||||
|
func (i *interpretedResult) failBlindedRoute(rt *mcRoute) {
|
||||||
|
// We fail the last pair of the route, in order to fail the complete
|
||||||
|
// blinded route. This is because the combination of ephemeral pubkeys
|
||||||
|
// is unique to the route. We fail the last pair in order to not punish
|
||||||
|
// the introduction node, since we don't want to disincentivize them
|
||||||
|
// from providing that service.
|
||||||
|
pair, _ := getPair(rt, len(rt.hops.Val)-1)
|
||||||
|
|
||||||
|
// Since all the hops along a blinded path don't have any amount set, we
|
||||||
|
// extract the minimal amount to punish from the value that is tried to
|
||||||
|
// be sent to the receiver.
|
||||||
|
amt := rt.hops.Val[len(rt.hops.Val)-1].amtToFwd.Val
|
||||||
|
|
||||||
|
i.pairResults[pair] = failPairResult(amt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// markBlindedRouteSuccess marks the hops of the blinded route AFTER the
|
||||||
|
// introduction node as successful.
|
||||||
|
//
|
||||||
|
// NOTE: The introIdx must be the index of the first hop of the blinded route
|
||||||
|
// AFTER the introduction node.
|
||||||
|
func (i *interpretedResult) markBlindedRouteSuccess(rt *mcRoute, introIdx int) {
|
||||||
|
// For blinded hops we do not have the forwarding amount so we take the
|
||||||
|
// minimal amount which went through the route by looking at the last
|
||||||
|
// hop.
|
||||||
|
successAmt := rt.hops.Val[len(rt.hops.Val)-1].amtToFwd.Val
|
||||||
|
for idx := introIdx; idx < len(rt.hops.Val); idx++ {
|
||||||
|
pair, _ := getPair(rt, idx)
|
||||||
|
|
||||||
|
i.pairResults[pair] = successPairResult(successAmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// getPair returns a node pair from the route and the amount passed between that
|
// getPair returns a node pair from the route and the amount passed between that
|
||||||
// pair.
|
// pair.
|
||||||
func getPair(rt *mcRoute, channelIdx int) (DirectedNodePair,
|
func getPair(rt *mcRoute, channelIdx int) (DirectedNodePair,
|
||||||
|
|
|
@ -96,13 +96,17 @@ var (
|
||||||
AmtToForward: 99,
|
AmtToForward: 99,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
PubKeyBytes: hops[2],
|
PubKeyBytes: hops[2],
|
||||||
AmtToForward: 95,
|
// Intermediate blinded hops don't have an
|
||||||
|
// amount set.
|
||||||
|
AmtToForward: 0,
|
||||||
BlindingPoint: genTestPubKey(),
|
BlindingPoint: genTestPubKey(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
PubKeyBytes: hops[3],
|
PubKeyBytes: hops[3],
|
||||||
AmtToForward: 88,
|
// Intermediate blinded hops don't have an
|
||||||
|
// amount set.
|
||||||
|
AmtToForward: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
PubKeyBytes: hops[4],
|
PubKeyBytes: hops[4],
|
||||||
|
@ -122,8 +126,10 @@ var (
|
||||||
AmtToForward: 99,
|
AmtToForward: 99,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
PubKeyBytes: hops[2],
|
PubKeyBytes: hops[2],
|
||||||
AmtToForward: 95,
|
// Intermediate blinded hops don't have an
|
||||||
|
// amount set.
|
||||||
|
AmtToForward: 0,
|
||||||
BlindingPoint: genTestPubKey(),
|
BlindingPoint: genTestPubKey(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -140,13 +146,17 @@ var (
|
||||||
TotalAmount: 100,
|
TotalAmount: 100,
|
||||||
Hops: []*route.Hop{
|
Hops: []*route.Hop{
|
||||||
{
|
{
|
||||||
PubKeyBytes: hops[1],
|
PubKeyBytes: hops[1],
|
||||||
AmtToForward: 90,
|
// Intermediate blinded hops don't have an
|
||||||
|
// amount set.
|
||||||
|
AmtToForward: 0,
|
||||||
BlindingPoint: genTestPubKey(),
|
BlindingPoint: genTestPubKey(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
PubKeyBytes: hops[2],
|
PubKeyBytes: hops[2],
|
||||||
AmtToForward: 75,
|
// Intermediate blinded hops don't have an
|
||||||
|
// amount set.
|
||||||
|
AmtToForward: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
PubKeyBytes: hops[3],
|
PubKeyBytes: hops[3],
|
||||||
|
@ -552,7 +562,12 @@ var resultTestCases = []resultTestCase{
|
||||||
pairResults: map[DirectedNodePair]pairResult{
|
pairResults: map[DirectedNodePair]pairResult{
|
||||||
getTestPair(0, 1): successPairResult(100),
|
getTestPair(0, 1): successPairResult(100),
|
||||||
getTestPair(1, 2): successPairResult(99),
|
getTestPair(1, 2): successPairResult(99),
|
||||||
getTestPair(3, 4): failPairResult(88),
|
|
||||||
|
// The amount for the last hop is always the
|
||||||
|
// receiver amount because the amount to forward
|
||||||
|
// is always set to 0 for intermediate blinded
|
||||||
|
// hops.
|
||||||
|
getTestPair(3, 4): failPairResult(77),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -567,7 +582,12 @@ var resultTestCases = []resultTestCase{
|
||||||
expectedResult: &interpretedResult{
|
expectedResult: &interpretedResult{
|
||||||
pairResults: map[DirectedNodePair]pairResult{
|
pairResults: map[DirectedNodePair]pairResult{
|
||||||
getTestPair(0, 1): successPairResult(100),
|
getTestPair(0, 1): successPairResult(100),
|
||||||
getTestPair(2, 3): failPairResult(75),
|
|
||||||
|
// The amount for the last hop is always the
|
||||||
|
// receiver amount because the amount to forward
|
||||||
|
// is always set to 0 for intermediate blinded
|
||||||
|
// hops.
|
||||||
|
getTestPair(2, 3): failPairResult(58),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -682,6 +702,25 @@ var resultTestCases = []resultTestCase{
|
||||||
finalFailureReason: &reasonError,
|
finalFailureReason: &reasonError,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// Test a multi-hop blinded route and that in a success case the amounts
|
||||||
|
// for the blinded route part are correctly set to the receiver amount.
|
||||||
|
{
|
||||||
|
name: "blinded multi-hop success",
|
||||||
|
route: blindedMultiToIntroduction,
|
||||||
|
success: true,
|
||||||
|
expectedResult: &interpretedResult{
|
||||||
|
pairResults: map[DirectedNodePair]pairResult{
|
||||||
|
getTestPair(0, 1): successPairResult(100),
|
||||||
|
|
||||||
|
// For the route blinded part of the route the
|
||||||
|
// success amount is determined by the receiver
|
||||||
|
// amount because the intermediate blinded hops
|
||||||
|
// set the forwarded amount to 0.
|
||||||
|
getTestPair(1, 2): successPairResult(58),
|
||||||
|
getTestPair(2, 3): successPairResult(58),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestResultInterpretation executes a list of test cases that test the result
|
// TestResultInterpretation executes a list of test cases that test the result
|
||||||
|
|
Loading…
Add table
Reference in a new issue