lnrpc/invoicesrpc: build blinded path

This commit adds all the logic for building a blinded path (from a given
route) and packaging it up in a zpay32.BlindedPaymentPath struct so that
it is ready for adding to an invoice. It also includes logic for padding
a path with dummy hops.

Note that in this commit, the logic for choosing an actual path to us
that can then be used in a blinded path is abstracted away. This logic
will be fleshed out in a future commit.
This commit is contained in:
Elle Mouton committed 2024-07-26 09:54:17 +02:00
1 parent 0855e3e71a
commit 4b5327f057
2 files changed
+1005

No files matched your search

+452
View File
@@ -1,6 +1,7 @@
package invoicesrpc
import (
"bytes"
"encoding/hex"
"fmt"
"math/rand"
@@ -10,10 +11,13 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/wire"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/tlv"
"github.com/lightningnetwork/lnd/zpay32"
"github.com/stretchr/testify/mock"
@@ -1402,3 +1406,451 @@ func genBlindedRouteData(rand *rand.Rand) *record.BlindedRouteData {
return &data
}
// TestBuildBlindedPath tests the logic for constructing a blinded path against
// an example mentioned in this spec document:
// https://github.com/lightning/bolts/blob/master/proposals/route-blinding.md
// This example does not use any dummy hops.
func TestBuildBlindedPath(t *testing.T) {
// Alice chooses the following path to herself for blinded path
// construction:
// Carol -> Bob -> Alice.
// Let's construct the corresponding route.Route for this which will be
// returned from the `findRoutes` config callback.
var (
privC, pkC = btcec.PrivKeyFromBytes([]byte{1})
privB, pkB = btcec.PrivKeyFromBytes([]byte{2})
privA, pkA = btcec.PrivKeyFromBytes([]byte{3})
carol = route.NewVertex(pkC)
bob = route.NewVertex(pkB)
alice = route.NewVertex(pkA)
chanCB = uint64(1)
chanBA = uint64(2)
)
realRoute := &route.Route{
SourcePubKey: carol,
Hops: []*route.Hop{
{
PubKeyBytes: bob,
ChannelID: chanCB,
},
{
PubKeyBytes: alice,
ChannelID: chanBA,
},
},
}
realPolicies := map[uint64]*models.ChannelEdgePolicy{
chanCB: {
ChannelID: chanCB,
ToNode: bob,
},
chanBA: {
ChannelID: chanBA,
ToNode: alice,
},
}
paths, err := buildBlindedPaymentPaths(&buildBlindedPathCfg{
findRoutes: func(_ lnwire.MilliSatoshi) ([]*route.Route,
error) {
return []*route.Route{realRoute}, nil
},
fetchChannelEdgesByID: func(chanID uint64) (
*models.ChannelEdgeInfo, *models.ChannelEdgePolicy,
*models.ChannelEdgePolicy, error) {
return nil, realPolicies[chanID], nil, nil
},
bestHeight: func() (uint32, error) {
return 1000, nil
},
// In the spec example, all the policies get replaced with
// the same static values.
addPolicyBuffer: func(_ *blindedHopPolicy) (
*blindedHopPolicy, error) {
return &blindedHopPolicy{
feeRate: 500,
baseFee: 100,
cltvExpiryDelta: 144,
minHTLCMsat: 1000,
maxHTLCMsat: lnwire.MaxMilliSatoshi,
}, nil
},
pathID: []byte{1, 2, 3},
valueMsat: 1000,
minFinalCLTVExpiryDelta: 12,
blocksUntilExpiry: 200,
})
require.NoError(t, err)
require.Len(t, paths, 1)
path := paths[0]
// Check that all the accumulated policy values are correct.
require.EqualValues(t, 201, path.FeeBaseMsat)
require.EqualValues(t, 1001, path.FeeRate)
require.EqualValues(t, 300, path.CltvExpiryDelta)
require.EqualValues(t, 1000, path.HTLCMinMsat)
require.EqualValues(t, lnwire.MaxMilliSatoshi, path.HTLCMaxMsat)
// Now we check the hops.
require.Len(t, path.Hops, 3)
// Assert that all the encrypted recipient blobs have been padded such
// that they are all the same size.
require.Len(t, path.Hops[0].CipherText, len(path.Hops[1].CipherText))
require.Len(t, path.Hops[1].CipherText, len(path.Hops[2].CipherText))
// The first hop, should have the real pub key of the introduction
// node: Carol.
hop := path.Hops[0]
require.True(t, hop.BlindedNodePub.IsEqual(pkC))
// As Carol, let's decode the hop data and assert that all expected
// values have been included.
var (
blindingPoint = path.FirstEphemeralBlindingPoint
data *record.BlindedRouteData
)
// Check that Carol's info is correct.
data, blindingPoint = decryptAndDecodeHopData(
t, privC, blindingPoint, hop.CipherText,
)
require.Equal(
t, lnwire.NewShortChanIDFromInt(chanCB),
data.ShortChannelID.UnwrapOrFail(t).Val,
)
require.Equal(t, record.PaymentRelayInfo{
CltvExpiryDelta: 144,
FeeRate: 500,
BaseFee: 100,
}, data.RelayInfo.UnwrapOrFail(t).Val)
require.Equal(t, record.PaymentConstraints{
MaxCltvExpiry: 1500,
HtlcMinimumMsat: 1000,
}, data.Constraints.UnwrapOrFail(t).Val)
// Check that all Bob's info is correct.
hop = path.Hops[1]
data, blindingPoint = decryptAndDecodeHopData(
t, privB, blindingPoint, hop.CipherText,
)
require.Equal(
t, lnwire.NewShortChanIDFromInt(chanBA),
data.ShortChannelID.UnwrapOrFail(t).Val,
)
require.Equal(t, record.PaymentRelayInfo{
CltvExpiryDelta: 144,
FeeRate: 500,
BaseFee: 100,
}, data.RelayInfo.UnwrapOrFail(t).Val)
require.Equal(t, record.PaymentConstraints{
MaxCltvExpiry: 1356,
HtlcMinimumMsat: 1000,
}, data.Constraints.UnwrapOrFail(t).Val)
// Check that all Alice's info is correct.
hop = path.Hops[2]
data, _ = decryptAndDecodeHopData(
t, privA, blindingPoint, hop.CipherText,
)
require.True(t, data.ShortChannelID.IsNone())
require.True(t, data.RelayInfo.IsNone())
require.Equal(t, record.PaymentConstraints{
MaxCltvExpiry: 1212,
HtlcMinimumMsat: 1000,
}, data.Constraints.UnwrapOrFail(t).Val)
require.Equal(t, []byte{1, 2, 3}, data.PathID.UnwrapOrFail(t).Val)
}
// TestBuildBlindedPathWithDummyHops tests the construction of a blinded path
// which includes dummy hops.
func TestBuildBlindedPathWithDummyHops(t *testing.T) {
// Alice chooses the following path to herself for blinded path
// construction:
// Carol -> Bob -> Alice.
// Let's construct the corresponding route.Route for this which will be
// returned from the `findRoutes` config callback.
var (
privC, pkC = btcec.PrivKeyFromBytes([]byte{1})
privB, pkB = btcec.PrivKeyFromBytes([]byte{2})
privA, pkA = btcec.PrivKeyFromBytes([]byte{3})
carol = route.NewVertex(pkC)
bob = route.NewVertex(pkB)
alice = route.NewVertex(pkA)
chanCB = uint64(1)
chanBA = uint64(2)
)
realRoute := &route.Route{
SourcePubKey: carol,
Hops: []*route.Hop{
{
PubKeyBytes: bob,
ChannelID: chanCB,
},
{
PubKeyBytes: alice,
ChannelID: chanBA,
},
},
}
realPolicies := map[uint64]*models.ChannelEdgePolicy{
chanCB: {
ChannelID: chanCB,
ToNode: bob,
},
chanBA: {
ChannelID: chanBA,
ToNode: alice,
},
}
paths, err := buildBlindedPaymentPaths(&buildBlindedPathCfg{
findRoutes: func(_ lnwire.MilliSatoshi) ([]*route.Route,
error) {
return []*route.Route{realRoute}, nil
},
fetchChannelEdgesByID: func(chanID uint64) (
*models.ChannelEdgeInfo, *models.ChannelEdgePolicy,
*models.ChannelEdgePolicy, error) {
policy, ok := realPolicies[chanID]
if !ok {
return nil, nil, nil,
fmt.Errorf("edge not found")
}
return nil, policy, nil, nil
},
bestHeight: func() (uint32, error) {
return 1000, nil
},
// In the spec example, all the policies get replaced with
// the same static values.
addPolicyBuffer: func(_ *blindedHopPolicy) (
*blindedHopPolicy, error) {
return &blindedHopPolicy{
feeRate: 500,
baseFee: 100,
cltvExpiryDelta: 144,
minHTLCMsat: 1000,
maxHTLCMsat: lnwire.MaxMilliSatoshi,
}, nil
},
pathID: []byte{1, 2, 3},
valueMsat: 1000,
minFinalCLTVExpiryDelta: 12,
blocksUntilExpiry: 200,
// By setting the minimum number of hops to 4, we force 2 dummy
// hops to be added to the real route.
minNumHops: 4,
dummyHopPolicy: &blindedHopPolicy{
cltvExpiryDelta: 50,
feeRate: 100,
baseFee: 100,
minHTLCMsat: 1000,
maxHTLCMsat: lnwire.MaxMilliSatoshi,
},
})
require.NoError(t, err)
require.Len(t, paths, 1)
path := paths[0]
// Check that all the accumulated policy values are correct.
require.EqualValues(t, 403, path.FeeBaseMsat)
require.EqualValues(t, 1203, path.FeeRate)
require.EqualValues(t, 400, path.CltvExpiryDelta)
require.EqualValues(t, 1000, path.HTLCMinMsat)
require.EqualValues(t, lnwire.MaxMilliSatoshi, path.HTLCMaxMsat)
// Now we check the hops.
require.Len(t, path.Hops, 5)
// Assert that all the encrypted recipient blobs have been padded such
// that they are all the same size.
require.Len(t, path.Hops[0].CipherText, len(path.Hops[1].CipherText))
require.Len(t, path.Hops[1].CipherText, len(path.Hops[2].CipherText))
require.Len(t, path.Hops[2].CipherText, len(path.Hops[3].CipherText))
require.Len(t, path.Hops[3].CipherText, len(path.Hops[4].CipherText))
// The first hop, should have the real pub key of the introduction
// node: Carol.
hop := path.Hops[0]
require.True(t, hop.BlindedNodePub.IsEqual(pkC))
// As Carol, let's decode the hop data and assert that all expected
// values have been included.
var (
blindingPoint = path.FirstEphemeralBlindingPoint
data *record.BlindedRouteData
)
// Check that Carol's info is correct.
data, blindingPoint = decryptAndDecodeHopData(
t, privC, blindingPoint, hop.CipherText,
)
require.Equal(
t, lnwire.NewShortChanIDFromInt(chanCB),
data.ShortChannelID.UnwrapOrFail(t).Val,
)
require.Equal(t, record.PaymentRelayInfo{
CltvExpiryDelta: 144,
FeeRate: 500,
BaseFee: 100,
}, data.RelayInfo.UnwrapOrFail(t).Val)
require.Equal(t, record.PaymentConstraints{
MaxCltvExpiry: 1600,
HtlcMinimumMsat: 1000,
}, data.Constraints.UnwrapOrFail(t).Val)
// Check that all Bob's info is correct.
hop = path.Hops[1]
data, blindingPoint = decryptAndDecodeHopData(
t, privB, blindingPoint, hop.CipherText,
)
require.Equal(
t, lnwire.NewShortChanIDFromInt(chanBA),
data.ShortChannelID.UnwrapOrFail(t).Val,
)
require.Equal(t, record.PaymentRelayInfo{
CltvExpiryDelta: 144,
FeeRate: 500,
BaseFee: 100,
}, data.RelayInfo.UnwrapOrFail(t).Val)
require.Equal(t, record.PaymentConstraints{
MaxCltvExpiry: 1456,
HtlcMinimumMsat: 1000,
}, data.Constraints.UnwrapOrFail(t).Val)
// Check that all Alice's info is correct. The payload should contain
// a next_node_id field that is equal to Alice's public key. This
// indicates to Alice that she should continue peeling the onion.
hop = path.Hops[2]
data, blindingPoint = decryptAndDecodeHopData(
t, privA, blindingPoint, hop.CipherText,
)
require.True(t, data.ShortChannelID.IsNone())
require.True(t, data.RelayInfo.IsSome())
require.True(t, data.Constraints.IsSome())
require.Equal(t, pkA, data.NextNodeID.UnwrapOrFail(t).Val)
// Alice should be able to decrypt the next payload with her private
// key. This next payload is yet another dummy hop.
hop = path.Hops[3]
data, blindingPoint = decryptAndDecodeHopData(
t, privA, blindingPoint, hop.CipherText,
)
require.True(t, data.ShortChannelID.IsNone())
require.True(t, data.RelayInfo.IsSome())
require.True(t, data.Constraints.IsSome())
require.Equal(t, pkA, data.NextNodeID.UnwrapOrFail(t).Val)
// Unwrapping one more time should reveal the final hop info for Alice.
hop = path.Hops[4]
data, _ = decryptAndDecodeHopData(
t, privA, blindingPoint, hop.CipherText,
)
require.True(t, data.ShortChannelID.IsNone())
require.True(t, data.RelayInfo.IsNone())
require.Equal(t, record.PaymentConstraints{
MaxCltvExpiry: 1212,
HtlcMinimumMsat: 1000,
}, data.Constraints.UnwrapOrFail(t).Val)
require.Equal(t, []byte{1, 2, 3}, data.PathID.UnwrapOrFail(t).Val)
}
// TestSingleHopBlindedPath tests that blinded path construction is done
// correctly for the case where the destination node is also the introduction
// node.
func TestSingleHopBlindedPath(t *testing.T) {
var (
_, pkC = btcec.PrivKeyFromBytes([]byte{1})
carol = route.NewVertex(pkC)
)
realRoute := &route.Route{
SourcePubKey: carol,
// No hops since Carol is both the introduction node and the
// final destination node.
Hops: []*route.Hop{},
}
paths, err := buildBlindedPaymentPaths(&buildBlindedPathCfg{
findRoutes: func(_ lnwire.MilliSatoshi) ([]*route.Route,
error) {
return []*route.Route{realRoute}, nil
},
bestHeight: func() (uint32, error) {
return 1000, nil
},
pathID: []byte{1, 2, 3},
valueMsat: 1000,
minFinalCLTVExpiryDelta: 12,
blocksUntilExpiry: 200,
})
require.NoError(t, err)
require.Len(t, paths, 1)
path := paths[0]
// Check that all the accumulated policy values are correct. Since this
// is a unique case where the destination node is also the introduction
// node, the accumulated fee and HTLC values should be zero and the
// CLTV expiry delta should be equal to Carol's minFinalCLTVExpiryDelta.
require.EqualValues(t, 0, path.FeeBaseMsat)
require.EqualValues(t, 0, path.FeeRate)
require.EqualValues(t, 0, path.HTLCMinMsat)
require.EqualValues(t, 0, path.HTLCMaxMsat)
require.EqualValues(t, 12, path.CltvExpiryDelta)
}
func decryptAndDecodeHopData(t *testing.T, priv *btcec.PrivateKey,
ephem *btcec.PublicKey, cipherText []byte) (*record.BlindedRouteData,
*btcec.PublicKey) {
router := sphinx.NewRouter(
&keychain.PrivKeyECDH{PrivKey: priv}, nil,
)
decrypted, err := router.DecryptBlindedHopData(ephem, cipherText)
require.NoError(t, err)
buf := bytes.NewBuffer(decrypted)
routeData, err := record.DecodeBlindedRouteData(buf)
require.NoError(t, err)
nextEphem, err := router.NextEphemeral(ephem)
require.NoError(t, err)
return routeData, nextEphem
}