Merge branch 'master' of github.com:lightningnetwork/lnd
This commit is contained in:
commit
af1ff291a7
@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/coreos/bbolt"
|
"github.com/coreos/bbolt"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -145,7 +146,14 @@ func (d *databaseChannelGraph) addRandChannel(node1, node2 *btcec.PublicKey,
|
|||||||
|
|
||||||
fetchNode := func(pub *btcec.PublicKey) (*channeldb.LightningNode, error) {
|
fetchNode := func(pub *btcec.PublicKey) (*channeldb.LightningNode, error) {
|
||||||
if pub != nil {
|
if pub != nil {
|
||||||
dbNode, err := d.db.FetchLightningNode(pub)
|
vertex, err := route.NewVertexFromBytes(
|
||||||
|
pub.SerializeCompressed(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dbNode, err := d.db.FetchLightningNode(nil, vertex)
|
||||||
switch {
|
switch {
|
||||||
case err == channeldb.ErrGraphNodeNotFound:
|
case err == channeldb.ErrGraphNodeNotFound:
|
||||||
fallthrough
|
fallthrough
|
||||||
@ -307,7 +315,7 @@ func (d *databaseChannelGraph) addRandNode() (*btcec.PublicKey, error) {
|
|||||||
// memChannelGraph is an implementation of the autopilot.ChannelGraph backed by
|
// memChannelGraph is an implementation of the autopilot.ChannelGraph backed by
|
||||||
// an in-memory graph.
|
// an in-memory graph.
|
||||||
type memChannelGraph struct {
|
type memChannelGraph struct {
|
||||||
graph map[NodeID]memNode
|
graph map[NodeID]*memNode
|
||||||
}
|
}
|
||||||
|
|
||||||
// A compile time assertion to ensure memChannelGraph meets the
|
// A compile time assertion to ensure memChannelGraph meets the
|
||||||
@ -318,7 +326,7 @@ var _ ChannelGraph = (*memChannelGraph)(nil)
|
|||||||
// implementation.
|
// implementation.
|
||||||
func newMemChannelGraph() *memChannelGraph {
|
func newMemChannelGraph() *memChannelGraph {
|
||||||
return &memChannelGraph{
|
return &memChannelGraph{
|
||||||
graph: make(map[NodeID]memNode),
|
graph: make(map[NodeID]*memNode),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,14 +368,14 @@ func (m *memChannelGraph) addRandChannel(node1, node2 *btcec.PublicKey,
|
|||||||
capacity btcutil.Amount) (*ChannelEdge, *ChannelEdge, error) {
|
capacity btcutil.Amount) (*ChannelEdge, *ChannelEdge, error) {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
vertex1, vertex2 memNode
|
vertex1, vertex2 *memNode
|
||||||
ok bool
|
ok bool
|
||||||
)
|
)
|
||||||
|
|
||||||
if node1 != nil {
|
if node1 != nil {
|
||||||
vertex1, ok = m.graph[NewNodeID(node1)]
|
vertex1, ok = m.graph[NewNodeID(node1)]
|
||||||
if !ok {
|
if !ok {
|
||||||
vertex1 = memNode{
|
vertex1 = &memNode{
|
||||||
pub: node1,
|
pub: node1,
|
||||||
addrs: []net.Addr{
|
addrs: []net.Addr{
|
||||||
&net.TCPAddr{
|
&net.TCPAddr{
|
||||||
@ -381,7 +389,7 @@ func (m *memChannelGraph) addRandChannel(node1, node2 *btcec.PublicKey,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
vertex1 = memNode{
|
vertex1 = &memNode{
|
||||||
pub: newPub,
|
pub: newPub,
|
||||||
addrs: []net.Addr{
|
addrs: []net.Addr{
|
||||||
&net.TCPAddr{
|
&net.TCPAddr{
|
||||||
@ -394,7 +402,7 @@ func (m *memChannelGraph) addRandChannel(node1, node2 *btcec.PublicKey,
|
|||||||
if node2 != nil {
|
if node2 != nil {
|
||||||
vertex2, ok = m.graph[NewNodeID(node2)]
|
vertex2, ok = m.graph[NewNodeID(node2)]
|
||||||
if !ok {
|
if !ok {
|
||||||
vertex2 = memNode{
|
vertex2 = &memNode{
|
||||||
pub: node2,
|
pub: node2,
|
||||||
addrs: []net.Addr{
|
addrs: []net.Addr{
|
||||||
&net.TCPAddr{
|
&net.TCPAddr{
|
||||||
@ -408,7 +416,7 @@ func (m *memChannelGraph) addRandChannel(node1, node2 *btcec.PublicKey,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
vertex2 = memNode{
|
vertex2 = &memNode{
|
||||||
pub: newPub,
|
pub: newPub,
|
||||||
addrs: []net.Addr{
|
addrs: []net.Addr{
|
||||||
&net.TCPAddr{
|
&net.TCPAddr{
|
||||||
@ -446,7 +454,7 @@ func (m *memChannelGraph) addRandNode() (*btcec.PublicKey, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
vertex := memNode{
|
vertex := &memNode{
|
||||||
pub: newPub,
|
pub: newPub,
|
||||||
addrs: []net.Addr{
|
addrs: []net.Addr{
|
||||||
&net.TCPAddr{
|
&net.TCPAddr{
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
"github.com/coreos/bbolt"
|
"github.com/coreos/bbolt"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -513,7 +514,7 @@ func (c *ChannelGraph) LookupAlias(pub *btcec.PublicKey) (string, error) {
|
|||||||
|
|
||||||
// DeleteLightningNode starts a new database transaction to remove a vertex/node
|
// DeleteLightningNode starts a new database transaction to remove a vertex/node
|
||||||
// from the database according to the node's public key.
|
// from the database according to the node's public key.
|
||||||
func (c *ChannelGraph) DeleteLightningNode(nodePub *btcec.PublicKey) error {
|
func (c *ChannelGraph) DeleteLightningNode(nodePub route.Vertex) error {
|
||||||
// TODO(roasbeef): ensure dangling edges are removed...
|
// TODO(roasbeef): ensure dangling edges are removed...
|
||||||
return c.db.Update(func(tx *bbolt.Tx) error {
|
return c.db.Update(func(tx *bbolt.Tx) error {
|
||||||
nodes := tx.Bucket(nodeBucket)
|
nodes := tx.Bucket(nodeBucket)
|
||||||
@ -521,9 +522,7 @@ func (c *ChannelGraph) DeleteLightningNode(nodePub *btcec.PublicKey) error {
|
|||||||
return ErrGraphNodeNotFound
|
return ErrGraphNodeNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.deleteLightningNode(
|
return c.deleteLightningNode(nodes, nodePub[:])
|
||||||
nodes, nodePub.SerializeCompressed(),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2180,10 +2179,17 @@ func (l *LightningNode) isPublic(tx *bbolt.Tx, sourcePubKey []byte) (bool, error
|
|||||||
// FetchLightningNode attempts to look up a target node by its identity public
|
// FetchLightningNode attempts to look up a target node by its identity public
|
||||||
// key. If the node isn't found in the database, then ErrGraphNodeNotFound is
|
// key. If the node isn't found in the database, then ErrGraphNodeNotFound is
|
||||||
// returned.
|
// returned.
|
||||||
func (c *ChannelGraph) FetchLightningNode(pub *btcec.PublicKey) (*LightningNode, error) {
|
//
|
||||||
|
// If the caller wishes to re-use an existing boltdb transaction, then it
|
||||||
|
// should be passed as the first argument. Otherwise the first argument should
|
||||||
|
// be nil and a fresh transaction will be created to execute the graph
|
||||||
|
// traversal.
|
||||||
|
func (c *ChannelGraph) FetchLightningNode(tx *bbolt.Tx, nodePub route.Vertex) (
|
||||||
|
*LightningNode, error) {
|
||||||
|
|
||||||
var node *LightningNode
|
var node *LightningNode
|
||||||
nodePub := pub.SerializeCompressed()
|
|
||||||
err := c.db.View(func(tx *bbolt.Tx) error {
|
fetchNode := func(tx *bbolt.Tx) error {
|
||||||
// First grab the nodes bucket which stores the mapping from
|
// First grab the nodes bucket which stores the mapping from
|
||||||
// pubKey to node information.
|
// pubKey to node information.
|
||||||
nodes := tx.Bucket(nodeBucket)
|
nodes := tx.Bucket(nodeBucket)
|
||||||
@ -2193,7 +2199,7 @@ func (c *ChannelGraph) FetchLightningNode(pub *btcec.PublicKey) (*LightningNode,
|
|||||||
|
|
||||||
// If a key for this serialized public key isn't found, then
|
// If a key for this serialized public key isn't found, then
|
||||||
// the target node doesn't exist within the database.
|
// the target node doesn't exist within the database.
|
||||||
nodeBytes := nodes.Get(nodePub)
|
nodeBytes := nodes.Get(nodePub[:])
|
||||||
if nodeBytes == nil {
|
if nodeBytes == nil {
|
||||||
return ErrGraphNodeNotFound
|
return ErrGraphNodeNotFound
|
||||||
}
|
}
|
||||||
@ -2210,7 +2216,14 @@ func (c *ChannelGraph) FetchLightningNode(pub *btcec.PublicKey) (*LightningNode,
|
|||||||
node = &n
|
node = &n
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if tx == nil {
|
||||||
|
err = c.db.View(fetchNode)
|
||||||
|
} else {
|
||||||
|
err = fetchNode(tx)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/coreos/bbolt"
|
"github.com/coreos/bbolt"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -37,6 +38,8 @@ var (
|
|||||||
_, _ = testSig.S.SetString("18801056069249825825291287104931333862866033135609736119018462340006816851118", 10)
|
_, _ = testSig.S.SetString("18801056069249825825291287104931333862866033135609736119018462340006816851118", 10)
|
||||||
|
|
||||||
testFeatures = lnwire.NewFeatureVector(nil, lnwire.Features)
|
testFeatures = lnwire.NewFeatureVector(nil, lnwire.Features)
|
||||||
|
|
||||||
|
testPub = route.Vertex{2, 202, 4}
|
||||||
)
|
)
|
||||||
|
|
||||||
func createLightningNode(db *DB, priv *btcec.PrivateKey) (*LightningNode, error) {
|
func createLightningNode(db *DB, priv *btcec.PrivateKey) (*LightningNode, error) {
|
||||||
@ -80,7 +83,6 @@ func TestNodeInsertionAndDeletion(t *testing.T) {
|
|||||||
|
|
||||||
// We'd like to test basic insertion/deletion for vertexes from the
|
// We'd like to test basic insertion/deletion for vertexes from the
|
||||||
// graph, so we'll create a test vertex to start with.
|
// graph, so we'll create a test vertex to start with.
|
||||||
_, testPub := btcec.PrivKeyFromBytes(btcec.S256(), key[:])
|
|
||||||
node := &LightningNode{
|
node := &LightningNode{
|
||||||
HaveNodeAnnouncement: true,
|
HaveNodeAnnouncement: true,
|
||||||
AuthSigBytes: testSig.Serialize(),
|
AuthSigBytes: testSig.Serialize(),
|
||||||
@ -90,9 +92,9 @@ func TestNodeInsertionAndDeletion(t *testing.T) {
|
|||||||
Features: testFeatures,
|
Features: testFeatures,
|
||||||
Addresses: testAddrs,
|
Addresses: testAddrs,
|
||||||
ExtraOpaqueData: []byte("extra new data"),
|
ExtraOpaqueData: []byte("extra new data"),
|
||||||
|
PubKeyBytes: testPub,
|
||||||
db: db,
|
db: db,
|
||||||
}
|
}
|
||||||
copy(node.PubKeyBytes[:], testPub.SerializeCompressed())
|
|
||||||
|
|
||||||
// First, insert the node into the graph DB. This should succeed
|
// First, insert the node into the graph DB. This should succeed
|
||||||
// without any errors.
|
// without any errors.
|
||||||
@ -102,7 +104,7 @@ func TestNodeInsertionAndDeletion(t *testing.T) {
|
|||||||
|
|
||||||
// Next, fetch the node from the database to ensure everything was
|
// Next, fetch the node from the database to ensure everything was
|
||||||
// serialized properly.
|
// serialized properly.
|
||||||
dbNode, err := graph.FetchLightningNode(testPub)
|
dbNode, err := graph.FetchLightningNode(nil, testPub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to locate node: %v", err)
|
t.Fatalf("unable to locate node: %v", err)
|
||||||
}
|
}
|
||||||
@ -126,7 +128,7 @@ func TestNodeInsertionAndDeletion(t *testing.T) {
|
|||||||
|
|
||||||
// Finally, attempt to fetch the node again. This should fail as the
|
// Finally, attempt to fetch the node again. This should fail as the
|
||||||
// node should have been deleted from the database.
|
// node should have been deleted from the database.
|
||||||
_, err = graph.FetchLightningNode(testPub)
|
_, err = graph.FetchLightningNode(nil, testPub)
|
||||||
if err != ErrGraphNodeNotFound {
|
if err != ErrGraphNodeNotFound {
|
||||||
t.Fatalf("fetch after delete should fail!")
|
t.Fatalf("fetch after delete should fail!")
|
||||||
}
|
}
|
||||||
@ -147,11 +149,10 @@ func TestPartialNode(t *testing.T) {
|
|||||||
|
|
||||||
// We want to be able to insert nodes into the graph that only has the
|
// We want to be able to insert nodes into the graph that only has the
|
||||||
// PubKey set.
|
// PubKey set.
|
||||||
_, testPub := btcec.PrivKeyFromBytes(btcec.S256(), key[:])
|
|
||||||
node := &LightningNode{
|
node := &LightningNode{
|
||||||
HaveNodeAnnouncement: false,
|
HaveNodeAnnouncement: false,
|
||||||
|
PubKeyBytes: testPub,
|
||||||
}
|
}
|
||||||
copy(node.PubKeyBytes[:], testPub.SerializeCompressed())
|
|
||||||
|
|
||||||
if err := graph.AddLightningNode(node); err != nil {
|
if err := graph.AddLightningNode(node); err != nil {
|
||||||
t.Fatalf("unable to add node: %v", err)
|
t.Fatalf("unable to add node: %v", err)
|
||||||
@ -159,7 +160,7 @@ func TestPartialNode(t *testing.T) {
|
|||||||
|
|
||||||
// Next, fetch the node from the database to ensure everything was
|
// Next, fetch the node from the database to ensure everything was
|
||||||
// serialized properly.
|
// serialized properly.
|
||||||
dbNode, err := graph.FetchLightningNode(testPub)
|
dbNode, err := graph.FetchLightningNode(nil, testPub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to locate node: %v", err)
|
t.Fatalf("unable to locate node: %v", err)
|
||||||
}
|
}
|
||||||
@ -175,9 +176,9 @@ func TestPartialNode(t *testing.T) {
|
|||||||
node = &LightningNode{
|
node = &LightningNode{
|
||||||
HaveNodeAnnouncement: false,
|
HaveNodeAnnouncement: false,
|
||||||
LastUpdate: time.Unix(0, 0),
|
LastUpdate: time.Unix(0, 0),
|
||||||
|
PubKeyBytes: testPub,
|
||||||
db: db,
|
db: db,
|
||||||
}
|
}
|
||||||
copy(node.PubKeyBytes[:], testPub.SerializeCompressed())
|
|
||||||
|
|
||||||
if err := compareNodes(node, dbNode); err != nil {
|
if err := compareNodes(node, dbNode); err != nil {
|
||||||
t.Fatalf("nodes don't match: %v", err)
|
t.Fatalf("nodes don't match: %v", err)
|
||||||
@ -191,7 +192,7 @@ func TestPartialNode(t *testing.T) {
|
|||||||
|
|
||||||
// Finally, attempt to fetch the node again. This should fail as the
|
// Finally, attempt to fetch the node again. This should fail as the
|
||||||
// node should have been deleted from the database.
|
// node should have been deleted from the database.
|
||||||
_, err = graph.FetchLightningNode(testPub)
|
_, err = graph.FetchLightningNode(nil, testPub)
|
||||||
if err != ErrGraphNodeNotFound {
|
if err != ErrGraphNodeNotFound {
|
||||||
t.Fatalf("fetch after delete should fail!")
|
t.Fatalf("fetch after delete should fail!")
|
||||||
}
|
}
|
||||||
@ -2386,11 +2387,8 @@ func TestPruneGraphNodes(t *testing.T) {
|
|||||||
|
|
||||||
// Finally, we'll ensure that node3, the only fully unconnected node as
|
// Finally, we'll ensure that node3, the only fully unconnected node as
|
||||||
// properly deleted from the graph and not another node in its place.
|
// properly deleted from the graph and not another node in its place.
|
||||||
node3Pub, err := node3.PubKey()
|
_, err = graph.FetchLightningNode(nil, node3.PubKeyBytes)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
t.Fatalf("unable to fetch the pubkey of node3: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := graph.FetchLightningNode(node3Pub); err == nil {
|
|
||||||
t.Fatalf("node 3 should have been deleted!")
|
t.Fatalf("node 3 should have been deleted!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2430,18 +2428,9 @@ func TestAddChannelEdgeShellNodes(t *testing.T) {
|
|||||||
t.Fatalf("unable to add edge: %v", err)
|
t.Fatalf("unable to add edge: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
node1Pub, err := node1.PubKey()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to parse node 1 pub: %v", err)
|
|
||||||
}
|
|
||||||
node2Pub, err := node2.PubKey()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to parse node 2 pub: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that node1 was inserted as a full node, while node2 only has
|
// Ensure that node1 was inserted as a full node, while node2 only has
|
||||||
// a shell node present.
|
// a shell node present.
|
||||||
node1, err = graph.FetchLightningNode(node1Pub)
|
node1, err = graph.FetchLightningNode(nil, node1.PubKeyBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to fetch node1: %v", err)
|
t.Fatalf("unable to fetch node1: %v", err)
|
||||||
}
|
}
|
||||||
@ -2449,7 +2438,7 @@ func TestAddChannelEdgeShellNodes(t *testing.T) {
|
|||||||
t.Fatalf("have shell announcement for node1, shouldn't")
|
t.Fatalf("have shell announcement for node1, shouldn't")
|
||||||
}
|
}
|
||||||
|
|
||||||
node2, err = graph.FetchLightningNode(node2Pub)
|
node2, err = graph.FetchLightningNode(nil, node2.PubKeyBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to fetch node2: %v", err)
|
t.Fatalf("unable to fetch node2: %v", err)
|
||||||
}
|
}
|
||||||
@ -2504,8 +2493,7 @@ func TestNodePruningUpdateIndexDeletion(t *testing.T) {
|
|||||||
|
|
||||||
// We'll now delete the node from the graph, this should result in it
|
// We'll now delete the node from the graph, this should result in it
|
||||||
// being removed from the update index as well.
|
// being removed from the update index as well.
|
||||||
nodePub, _ := node1.PubKey()
|
if err := graph.DeleteLightningNode(node1.PubKeyBytes); err != nil {
|
||||||
if err := graph.DeleteLightningNode(nodePub); err != nil {
|
|
||||||
t.Fatalf("unable to delete node: %v", err)
|
t.Fatalf("unable to delete node: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,7 +136,7 @@ func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
|
|||||||
// We need to use a custom dialer so we can also connect to unix sockets
|
// We need to use a custom dialer so we can also connect to unix sockets
|
||||||
// and not just TCP addresses.
|
// and not just TCP addresses.
|
||||||
genericDialer := lncfg.ClientAddressDialer(defaultRPCPort)
|
genericDialer := lncfg.ClientAddressDialer(defaultRPCPort)
|
||||||
opts = append(opts, grpc.WithDialer(genericDialer))
|
opts = append(opts, grpc.WithContextDialer(genericDialer))
|
||||||
opts = append(opts, grpc.WithDefaultCallOptions(maxMsgRecvSize))
|
opts = append(opts, grpc.WithDefaultCallOptions(maxMsgRecvSize))
|
||||||
|
|
||||||
conn, err := grpc.Dial(ctx.GlobalString("rpcserver"), opts...)
|
conn, err := grpc.Dial(ctx.GlobalString("rpcserver"), opts...)
|
||||||
|
@ -167,10 +167,10 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
|
|||||||
preimageSubscription := h.PreimageDB.SubscribeUpdates()
|
preimageSubscription := h.PreimageDB.SubscribeUpdates()
|
||||||
defer preimageSubscription.CancelSubscription()
|
defer preimageSubscription.CancelSubscription()
|
||||||
|
|
||||||
// Define closure to process hodl events either direct or triggered by
|
// Define closure to process htlc resolutions either direct or triggered by
|
||||||
// later notifcation.
|
// later notification.
|
||||||
processHodlEvent := func(e invoices.HodlEvent) (ContractResolver,
|
processHtlcResolution := func(e invoices.HtlcResolution) (
|
||||||
error) {
|
ContractResolver, error) {
|
||||||
|
|
||||||
if e.Preimage == nil {
|
if e.Preimage == nil {
|
||||||
log.Infof("%T(%v): Exit hop HTLC canceled "+
|
log.Infof("%T(%v): Exit hop HTLC canceled "+
|
||||||
@ -201,21 +201,24 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
|
|||||||
HtlcID: h.htlc.HtlcIndex,
|
HtlcID: h.htlc.HtlcIndex,
|
||||||
}
|
}
|
||||||
|
|
||||||
event, err := h.Registry.NotifyExitHopHtlc(
|
resolution, err := h.Registry.NotifyExitHopHtlc(
|
||||||
h.htlc.RHash, h.htlc.Amt, h.htlcExpiry, currentHeight,
|
h.htlc.RHash, h.htlc.Amt, h.htlcExpiry, currentHeight,
|
||||||
circuitKey, hodlChan, payload,
|
circuitKey, hodlChan, payload,
|
||||||
)
|
)
|
||||||
switch err {
|
if err != nil {
|
||||||
case channeldb.ErrInvoiceNotFound:
|
return nil, err
|
||||||
case nil:
|
}
|
||||||
|
|
||||||
defer h.Registry.HodlUnsubscribeAll(hodlChan)
|
defer h.Registry.HodlUnsubscribeAll(hodlChan)
|
||||||
|
|
||||||
// Resolve the htlc directly if possible.
|
// If the resolution is non-nil (indicating that a settle or cancel has
|
||||||
if event != nil {
|
// occurred), and the invoice is known to the registry (indicating that
|
||||||
return processHodlEvent(*event)
|
// the htlc is paying one of our invoices and is not a forward), try to
|
||||||
}
|
// resolve it directly.
|
||||||
default:
|
if resolution != nil &&
|
||||||
return nil, err
|
resolution.Outcome != invoices.ResultInvoiceNotFound {
|
||||||
|
|
||||||
|
return processHtlcResolution(*resolution)
|
||||||
}
|
}
|
||||||
|
|
||||||
// With the epochs and preimage subscriptions initialized, we'll query
|
// With the epochs and preimage subscriptions initialized, we'll query
|
||||||
@ -252,9 +255,9 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
|
|||||||
return &h.htlcSuccessResolver, nil
|
return &h.htlcSuccessResolver, nil
|
||||||
|
|
||||||
case hodlItem := <-hodlChan:
|
case hodlItem := <-hodlChan:
|
||||||
hodlEvent := hodlItem.(invoices.HodlEvent)
|
htlcResolution := hodlItem.(invoices.HtlcResolution)
|
||||||
|
|
||||||
return processHodlEvent(hodlEvent)
|
return processHtlcResolution(htlcResolution)
|
||||||
|
|
||||||
case newBlock, ok := <-blockEpochs.Epochs:
|
case newBlock, ok := <-blockEpochs.Epochs:
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -25,6 +25,7 @@ var (
|
|||||||
testResHash = testResPreimage.Hash()
|
testResHash = testResPreimage.Hash()
|
||||||
testResCircuitKey = channeldb.CircuitKey{}
|
testResCircuitKey = channeldb.CircuitKey{}
|
||||||
testOnionBlob = []byte{4, 5, 6}
|
testOnionBlob = []byte{4, 5, 6}
|
||||||
|
testAcceptHeight int32 = 1234
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestHtlcIncomingResolverFwdPreimageKnown tests resolution of a forwarded htlc
|
// TestHtlcIncomingResolverFwdPreimageKnown tests resolution of a forwarded htlc
|
||||||
@ -34,7 +35,10 @@ func TestHtlcIncomingResolverFwdPreimageKnown(t *testing.T) {
|
|||||||
defer timeout(t)()
|
defer timeout(t)()
|
||||||
|
|
||||||
ctx := newIncomingResolverTestContext(t)
|
ctx := newIncomingResolverTestContext(t)
|
||||||
ctx.registry.notifyErr = channeldb.ErrInvoiceNotFound
|
ctx.registry.notifyResolution = invoices.NewFailureResolution(
|
||||||
|
testResCircuitKey, testHtlcExpiry,
|
||||||
|
invoices.ResultInvoiceNotFound,
|
||||||
|
)
|
||||||
ctx.witnessBeacon.lookupPreimage[testResHash] = testResPreimage
|
ctx.witnessBeacon.lookupPreimage[testResHash] = testResPreimage
|
||||||
ctx.resolve()
|
ctx.resolve()
|
||||||
ctx.waitForResult(true)
|
ctx.waitForResult(true)
|
||||||
@ -48,7 +52,10 @@ func TestHtlcIncomingResolverFwdContestedSuccess(t *testing.T) {
|
|||||||
defer timeout(t)()
|
defer timeout(t)()
|
||||||
|
|
||||||
ctx := newIncomingResolverTestContext(t)
|
ctx := newIncomingResolverTestContext(t)
|
||||||
ctx.registry.notifyErr = channeldb.ErrInvoiceNotFound
|
ctx.registry.notifyResolution = invoices.NewFailureResolution(
|
||||||
|
testResCircuitKey, testHtlcExpiry,
|
||||||
|
invoices.ResultInvoiceNotFound,
|
||||||
|
)
|
||||||
ctx.resolve()
|
ctx.resolve()
|
||||||
|
|
||||||
// Simulate a new block coming in. HTLC is not yet expired.
|
// Simulate a new block coming in. HTLC is not yet expired.
|
||||||
@ -65,7 +72,10 @@ func TestHtlcIncomingResolverFwdContestedTimeout(t *testing.T) {
|
|||||||
defer timeout(t)()
|
defer timeout(t)()
|
||||||
|
|
||||||
ctx := newIncomingResolverTestContext(t)
|
ctx := newIncomingResolverTestContext(t)
|
||||||
ctx.registry.notifyErr = channeldb.ErrInvoiceNotFound
|
ctx.registry.notifyResolution = invoices.NewFailureResolution(
|
||||||
|
testResCircuitKey, testHtlcExpiry,
|
||||||
|
invoices.ResultInvoiceNotFound,
|
||||||
|
)
|
||||||
ctx.resolve()
|
ctx.resolve()
|
||||||
|
|
||||||
// Simulate a new block coming in. HTLC expires.
|
// Simulate a new block coming in. HTLC expires.
|
||||||
@ -81,8 +91,10 @@ func TestHtlcIncomingResolverFwdTimeout(t *testing.T) {
|
|||||||
defer timeout(t)()
|
defer timeout(t)()
|
||||||
|
|
||||||
ctx := newIncomingResolverTestContext(t)
|
ctx := newIncomingResolverTestContext(t)
|
||||||
|
ctx.registry.notifyResolution = invoices.NewFailureResolution(
|
||||||
ctx.registry.notifyErr = channeldb.ErrInvoiceNotFound
|
testResCircuitKey, testHtlcExpiry,
|
||||||
|
invoices.ResultInvoiceNotFound,
|
||||||
|
)
|
||||||
ctx.witnessBeacon.lookupPreimage[testResHash] = testResPreimage
|
ctx.witnessBeacon.lookupPreimage[testResHash] = testResPreimage
|
||||||
ctx.resolver.htlcExpiry = 90
|
ctx.resolver.htlcExpiry = 90
|
||||||
ctx.resolve()
|
ctx.resolve()
|
||||||
@ -96,10 +108,11 @@ func TestHtlcIncomingResolverExitSettle(t *testing.T) {
|
|||||||
defer timeout(t)()
|
defer timeout(t)()
|
||||||
|
|
||||||
ctx := newIncomingResolverTestContext(t)
|
ctx := newIncomingResolverTestContext(t)
|
||||||
ctx.registry.notifyEvent = &invoices.HodlEvent{
|
ctx.registry.notifyResolution = invoices.NewSettleResolution(
|
||||||
CircuitKey: testResCircuitKey,
|
testResPreimage, testResCircuitKey, testAcceptHeight,
|
||||||
Preimage: &testResPreimage,
|
invoices.ResultReplayToSettled,
|
||||||
}
|
)
|
||||||
|
|
||||||
ctx.resolve()
|
ctx.resolve()
|
||||||
|
|
||||||
data := <-ctx.registry.notifyChan
|
data := <-ctx.registry.notifyChan
|
||||||
@ -126,9 +139,11 @@ func TestHtlcIncomingResolverExitCancel(t *testing.T) {
|
|||||||
defer timeout(t)()
|
defer timeout(t)()
|
||||||
|
|
||||||
ctx := newIncomingResolverTestContext(t)
|
ctx := newIncomingResolverTestContext(t)
|
||||||
ctx.registry.notifyEvent = &invoices.HodlEvent{
|
ctx.registry.notifyResolution = invoices.NewFailureResolution(
|
||||||
CircuitKey: testResCircuitKey,
|
testResCircuitKey, testAcceptHeight,
|
||||||
}
|
invoices.ResultInvoiceAlreadyCanceled,
|
||||||
|
)
|
||||||
|
|
||||||
ctx.resolve()
|
ctx.resolve()
|
||||||
ctx.waitForResult(false)
|
ctx.waitForResult(false)
|
||||||
}
|
}
|
||||||
@ -143,10 +158,10 @@ func TestHtlcIncomingResolverExitSettleHodl(t *testing.T) {
|
|||||||
ctx.resolve()
|
ctx.resolve()
|
||||||
|
|
||||||
notifyData := <-ctx.registry.notifyChan
|
notifyData := <-ctx.registry.notifyChan
|
||||||
notifyData.hodlChan <- invoices.HodlEvent{
|
notifyData.hodlChan <- *invoices.NewSettleResolution(
|
||||||
CircuitKey: testResCircuitKey,
|
testResPreimage, testResCircuitKey, testAcceptHeight,
|
||||||
Preimage: &testResPreimage,
|
invoices.ResultSettled,
|
||||||
}
|
)
|
||||||
|
|
||||||
ctx.waitForResult(true)
|
ctx.waitForResult(true)
|
||||||
}
|
}
|
||||||
@ -172,9 +187,10 @@ func TestHtlcIncomingResolverExitCancelHodl(t *testing.T) {
|
|||||||
ctx := newIncomingResolverTestContext(t)
|
ctx := newIncomingResolverTestContext(t)
|
||||||
ctx.resolve()
|
ctx.resolve()
|
||||||
notifyData := <-ctx.registry.notifyChan
|
notifyData := <-ctx.registry.notifyChan
|
||||||
notifyData.hodlChan <- invoices.HodlEvent{
|
notifyData.hodlChan <- *invoices.NewFailureResolution(
|
||||||
CircuitKey: testResCircuitKey,
|
testResCircuitKey, testAcceptHeight, invoices.ResultCanceled,
|
||||||
}
|
)
|
||||||
|
|
||||||
ctx.waitForResult(false)
|
ctx.waitForResult(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,9 +27,9 @@ type Registry interface {
|
|||||||
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
|
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
|
||||||
expiry uint32, currentHeight int32,
|
expiry uint32, currentHeight int32,
|
||||||
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
|
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
|
||||||
payload invoices.Payload) (*invoices.HodlEvent, error)
|
payload invoices.Payload) (*invoices.HtlcResolution, error)
|
||||||
|
|
||||||
// HodlUnsubscribeAll unsubscribes from all hodl events.
|
// HodlUnsubscribeAll unsubscribes from all htlc resolutions.
|
||||||
HodlUnsubscribeAll(subscriber chan<- interface{})
|
HodlUnsubscribeAll(subscriber chan<- interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,13 +18,13 @@ type notifyExitHopData struct {
|
|||||||
type mockRegistry struct {
|
type mockRegistry struct {
|
||||||
notifyChan chan notifyExitHopData
|
notifyChan chan notifyExitHopData
|
||||||
notifyErr error
|
notifyErr error
|
||||||
notifyEvent *invoices.HodlEvent
|
notifyResolution *invoices.HtlcResolution
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash,
|
func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash,
|
||||||
paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
|
paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
|
||||||
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
|
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
|
||||||
payload invoices.Payload) (*invoices.HodlEvent, error) {
|
payload invoices.Payload) (*invoices.HtlcResolution, error) {
|
||||||
|
|
||||||
r.notifyChan <- notifyExitHopData{
|
r.notifyChan <- notifyExitHopData{
|
||||||
hodlChan: hodlChan,
|
hodlChan: hodlChan,
|
||||||
@ -34,7 +34,7 @@ func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash,
|
|||||||
currentHeight: currentHeight,
|
currentHeight: currentHeight,
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.notifyEvent, r.notifyErr
|
return r.notifyResolution, r.notifyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mockRegistry) HodlUnsubscribeAll(subscriber chan<- interface{}) {}
|
func (r *mockRegistry) HodlUnsubscribeAll(subscriber chan<- interface{}) {}
|
||||||
|
@ -27,7 +27,7 @@ type InvoiceDatabase interface {
|
|||||||
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
|
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
|
||||||
expiry uint32, currentHeight int32,
|
expiry uint32, currentHeight int32,
|
||||||
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
|
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
|
||||||
payload invoices.Payload) (*invoices.HodlEvent, error)
|
payload invoices.Payload) (*invoices.HtlcResolution, error)
|
||||||
|
|
||||||
// CancelInvoice attempts to cancel the invoice corresponding to the
|
// CancelInvoice attempts to cancel the invoice corresponding to the
|
||||||
// passed payment hash.
|
// passed payment hash.
|
||||||
@ -36,7 +36,7 @@ type InvoiceDatabase interface {
|
|||||||
// SettleHodlInvoice settles a hold invoice.
|
// SettleHodlInvoice settles a hold invoice.
|
||||||
SettleHodlInvoice(preimage lntypes.Preimage) error
|
SettleHodlInvoice(preimage lntypes.Preimage) error
|
||||||
|
|
||||||
// HodlUnsubscribeAll unsubscribes from all hodl events.
|
// HodlUnsubscribeAll unsubscribes from all htlc resolutions.
|
||||||
HodlUnsubscribeAll(subscriber chan<- interface{})
|
HodlUnsubscribeAll(subscriber chan<- interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -489,8 +489,8 @@ func (l *channelLink) Stop() {
|
|||||||
|
|
||||||
l.log.Info("stopping")
|
l.log.Info("stopping")
|
||||||
|
|
||||||
// As the link is stopping, we are no longer interested in hodl events
|
// As the link is stopping, we are no longer interested in htlc
|
||||||
// coming from the invoice registry.
|
// resolutions coming from the invoice registry.
|
||||||
l.cfg.Registry.HodlUnsubscribeAll(l.hodlQueue.ChanIn())
|
l.cfg.Registry.HodlUnsubscribeAll(l.hodlQueue.ChanIn())
|
||||||
|
|
||||||
if l.cfg.ChainEvents.Cancel != nil {
|
if l.cfg.ChainEvents.Cancel != nil {
|
||||||
@ -1126,11 +1126,11 @@ out:
|
|||||||
case msg := <-l.upstream:
|
case msg := <-l.upstream:
|
||||||
l.handleUpstreamMsg(msg)
|
l.handleUpstreamMsg(msg)
|
||||||
|
|
||||||
// A hodl event is received. This means that we now have a
|
// A htlc resolution is received. This means that we now have a
|
||||||
// resolution for a previously accepted htlc.
|
// resolution for a previously accepted htlc.
|
||||||
case hodlItem := <-l.hodlQueue.ChanOut():
|
case hodlItem := <-l.hodlQueue.ChanOut():
|
||||||
hodlEvent := hodlItem.(invoices.HodlEvent)
|
htlcResolution := hodlItem.(invoices.HtlcResolution)
|
||||||
err := l.processHodlQueue(hodlEvent)
|
err := l.processHodlQueue(htlcResolution)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.fail(LinkFailureError{code: ErrInternalError},
|
l.fail(LinkFailureError{code: ErrInternalError},
|
||||||
fmt.Sprintf("process hodl queue: %v",
|
fmt.Sprintf("process hodl queue: %v",
|
||||||
@ -1145,24 +1145,26 @@ out:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// processHodlQueue processes a received hodl event and continues reading from
|
// processHodlQueue processes a received htlc resolution and continues reading
|
||||||
// the hodl queue until no more events remain. When this function returns
|
// from the hodl queue until no more resolutions remain. When this function
|
||||||
// without an error, the commit tx should be updated.
|
// returns without an error, the commit tx should be updated.
|
||||||
func (l *channelLink) processHodlQueue(firstHodlEvent invoices.HodlEvent) error {
|
func (l *channelLink) processHodlQueue(
|
||||||
|
firstResolution invoices.HtlcResolution) error {
|
||||||
|
|
||||||
// Try to read all waiting resolution messages, so that they can all be
|
// Try to read all waiting resolution messages, so that they can all be
|
||||||
// processed in a single commitment tx update.
|
// processed in a single commitment tx update.
|
||||||
hodlEvent := firstHodlEvent
|
htlcResolution := firstResolution
|
||||||
loop:
|
loop:
|
||||||
for {
|
for {
|
||||||
// Lookup all hodl htlcs that can be failed or settled with this event.
|
// Lookup all hodl htlcs that can be failed or settled with this event.
|
||||||
// The hodl htlc must be present in the map.
|
// The hodl htlc must be present in the map.
|
||||||
circuitKey := hodlEvent.CircuitKey
|
circuitKey := htlcResolution.CircuitKey
|
||||||
hodlHtlc, ok := l.hodlMap[circuitKey]
|
hodlHtlc, ok := l.hodlMap[circuitKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("hodl htlc not found: %v", circuitKey)
|
return fmt.Errorf("hodl htlc not found: %v", circuitKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := l.processHodlEvent(hodlEvent, hodlHtlc); err != nil {
|
if err := l.processHtlcResolution(htlcResolution, hodlHtlc); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1171,7 +1173,7 @@ loop:
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case item := <-l.hodlQueue.ChanOut():
|
case item := <-l.hodlQueue.ChanOut():
|
||||||
hodlEvent = item.(invoices.HodlEvent)
|
htlcResolution = item.(invoices.HtlcResolution)
|
||||||
default:
|
default:
|
||||||
break loop
|
break loop
|
||||||
}
|
}
|
||||||
@ -1185,30 +1187,30 @@ loop:
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// processHodlEvent applies a received hodl event to the provided htlc. When
|
// processHtlcResolution applies a received htlc resolution to the provided
|
||||||
// this function returns without an error, the commit tx should be updated.
|
// htlc. When this function returns without an error, the commit tx should be
|
||||||
func (l *channelLink) processHodlEvent(hodlEvent invoices.HodlEvent,
|
// updated.
|
||||||
|
func (l *channelLink) processHtlcResolution(resolution invoices.HtlcResolution,
|
||||||
htlc hodlHtlc) error {
|
htlc hodlHtlc) error {
|
||||||
|
|
||||||
circuitKey := hodlEvent.CircuitKey
|
circuitKey := resolution.CircuitKey
|
||||||
|
|
||||||
// Determine required action for the resolution.
|
// Determine required action for the resolution. If the event's preimage is
|
||||||
if hodlEvent.Preimage != nil {
|
// non-nil, the htlc must be settled. Otherwise, it should be canceled.
|
||||||
l.log.Debugf("received hodl settle event for %v", circuitKey)
|
if resolution.Preimage != nil {
|
||||||
|
l.log.Debugf("received settle resolution for %v", circuitKey)
|
||||||
|
|
||||||
return l.settleHTLC(
|
return l.settleHTLC(
|
||||||
*hodlEvent.Preimage, htlc.pd.HtlcIndex,
|
*resolution.Preimage, htlc.pd.HtlcIndex,
|
||||||
htlc.pd.SourceRef,
|
htlc.pd.SourceRef,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
l.log.Debugf("received hodl cancel event for %v", circuitKey)
|
l.log.Debugf("received cancel resolution for %v with outcome: %v",
|
||||||
|
circuitKey, resolution.Outcome)
|
||||||
|
|
||||||
// In case of a cancel, always return
|
// Get the lnwire failure message based on the resolution result.
|
||||||
// incorrect_or_unknown_payment_details in order to avoid leaking info.
|
failure := getResolutionFailure(resolution, htlc.pd.Amount)
|
||||||
failure := lnwire.NewFailIncorrectDetails(
|
|
||||||
htlc.pd.Amount, uint32(hodlEvent.AcceptHeight),
|
|
||||||
)
|
|
||||||
|
|
||||||
l.sendHTLCError(
|
l.sendHTLCError(
|
||||||
htlc.pd.HtlcIndex, failure, htlc.obfuscator,
|
htlc.pd.HtlcIndex, failure, htlc.obfuscator,
|
||||||
@ -1217,6 +1219,25 @@ func (l *channelLink) processHodlEvent(hodlEvent invoices.HodlEvent,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getResolutionFailure returns the wire message that a htlc resolution should
|
||||||
|
// be failed with.
|
||||||
|
func getResolutionFailure(resolution invoices.HtlcResolution,
|
||||||
|
amount lnwire.MilliSatoshi) lnwire.FailureMessage {
|
||||||
|
|
||||||
|
// If the resolution has been resolved as part of a MPP timeout, we need
|
||||||
|
// to fail the htlc with lnwire.FailMppTimeout.
|
||||||
|
if resolution.Outcome == invoices.ResultMppTimeout {
|
||||||
|
return &lnwire.FailMPPTimeout{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the htlc is not a MPP timeout, we fail it with FailIncorrectDetails
|
||||||
|
// This covers hodl cancels (which return it to avoid leaking information
|
||||||
|
// and other invoice failures such as underpayment or expiry too soon.
|
||||||
|
return lnwire.NewFailIncorrectDetails(
|
||||||
|
amount, uint32(resolution.AcceptHeight),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// randomFeeUpdateTimeout returns a random timeout between the bounds defined
|
// randomFeeUpdateTimeout returns a random timeout between the bounds defined
|
||||||
// within the link's configuration that will be used to determine when the link
|
// within the link's configuration that will be used to determine when the link
|
||||||
// should propose an update to its commitment fee rate.
|
// should propose an update to its commitment fee rate.
|
||||||
@ -2817,21 +2838,7 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
|
|||||||
invoiceHash, pd.Amount, pd.Timeout, int32(heightNow),
|
invoiceHash, pd.Amount, pd.Timeout, int32(heightNow),
|
||||||
circuitKey, l.hodlQueue.ChanIn(), payload,
|
circuitKey, l.hodlQueue.ChanIn(), payload,
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
switch err {
|
|
||||||
|
|
||||||
// Cancel htlc if we don't have an invoice for it.
|
|
||||||
case channeldb.ErrInvoiceNotFound:
|
|
||||||
failure := lnwire.NewFailIncorrectDetails(pd.Amount, heightNow)
|
|
||||||
l.sendHTLCError(pd.HtlcIndex, failure, obfuscator, pd.SourceRef)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
|
|
||||||
// No error.
|
|
||||||
case nil:
|
|
||||||
|
|
||||||
// Pass error to caller.
|
|
||||||
default:
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2841,15 +2848,15 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
|
|||||||
obfuscator: obfuscator,
|
obfuscator: obfuscator,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the event is nil, the invoice is being held, so we save payment
|
||||||
|
// descriptor for future reference.
|
||||||
if event == nil {
|
if event == nil {
|
||||||
// Save payment descriptor for future reference.
|
|
||||||
l.hodlMap[circuitKey] = htlc
|
l.hodlMap[circuitKey] = htlc
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process the received resolution.
|
// Process the received resolution.
|
||||||
return l.processHodlEvent(*event, htlc)
|
return l.processHtlcResolution(*event, htlc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// settleHTLC settles the HTLC on the channel.
|
// settleHTLC settles the HTLC on the channel.
|
||||||
|
@ -819,7 +819,7 @@ func (i *mockInvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error
|
|||||||
func (i *mockInvoiceRegistry) NotifyExitHopHtlc(rhash lntypes.Hash,
|
func (i *mockInvoiceRegistry) NotifyExitHopHtlc(rhash lntypes.Hash,
|
||||||
amt lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
|
amt lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
|
||||||
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
|
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
|
||||||
payload invoices.Payload) (*invoices.HodlEvent, error) {
|
payload invoices.Payload) (*invoices.HtlcResolution, error) {
|
||||||
|
|
||||||
event, err := i.registry.NotifyExitHopHtlc(
|
event, err := i.registry.NotifyExitHopHtlc(
|
||||||
rhash, amt, expiry, currentHeight, circuitKey, hodlChan,
|
rhash, amt, expiry, currentHeight, circuitKey, hodlChan,
|
||||||
|
@ -35,10 +35,10 @@ const (
|
|||||||
DefaultHtlcHoldDuration = 120 * time.Second
|
DefaultHtlcHoldDuration = 120 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// HodlEvent describes how an htlc should be resolved. If HodlEvent.Preimage is
|
// HtlcResolution describes how an htlc should be resolved. If the preimage
|
||||||
// set, the event indicates a settle event. If Preimage is nil, it is a cancel
|
// field is set, the event indicates a settle event. If Preimage is nil, it is
|
||||||
// event.
|
// a cancel event.
|
||||||
type HodlEvent struct {
|
type HtlcResolution struct {
|
||||||
// Preimage is the htlc preimage. Its value is nil in case of a cancel.
|
// Preimage is the htlc preimage. Its value is nil in case of a cancel.
|
||||||
Preimage *lntypes.Preimage
|
Preimage *lntypes.Preimage
|
||||||
|
|
||||||
@ -48,6 +48,33 @@ type HodlEvent struct {
|
|||||||
|
|
||||||
// AcceptHeight is the original height at which the htlc was accepted.
|
// AcceptHeight is the original height at which the htlc was accepted.
|
||||||
AcceptHeight int32
|
AcceptHeight int32
|
||||||
|
|
||||||
|
// Outcome indicates the outcome of the invoice registry update.
|
||||||
|
Outcome ResolutionResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFailureResolution returns a htlc failure resolution.
|
||||||
|
func NewFailureResolution(key channeldb.CircuitKey,
|
||||||
|
acceptHeight int32, outcome ResolutionResult) *HtlcResolution {
|
||||||
|
|
||||||
|
return &HtlcResolution{
|
||||||
|
CircuitKey: key,
|
||||||
|
AcceptHeight: acceptHeight,
|
||||||
|
Outcome: outcome,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSettleResolution returns a htlc resolution which is associated with a
|
||||||
|
// settle.
|
||||||
|
func NewSettleResolution(preimage lntypes.Preimage, key channeldb.CircuitKey,
|
||||||
|
acceptHeight int32, outcome ResolutionResult) *HtlcResolution {
|
||||||
|
|
||||||
|
return &HtlcResolution{
|
||||||
|
Preimage: &preimage,
|
||||||
|
CircuitKey: key,
|
||||||
|
AcceptHeight: acceptHeight,
|
||||||
|
Outcome: outcome,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegistryConfig contains the configuration parameters for invoice registry.
|
// RegistryConfig contains the configuration parameters for invoice registry.
|
||||||
@ -325,7 +352,7 @@ func (i *InvoiceRegistry) invoiceEventLoop() {
|
|||||||
case <-nextReleaseTick:
|
case <-nextReleaseTick:
|
||||||
event := autoReleaseHeap.Pop().(*htlcReleaseEvent)
|
event := autoReleaseHeap.Pop().(*htlcReleaseEvent)
|
||||||
err := i.cancelSingleHtlc(
|
err := i.cancelSingleHtlc(
|
||||||
event.hash, event.key,
|
event.hash, event.key, ResultMppTimeout,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("HTLC timer: %v", err)
|
log.Errorf("HTLC timer: %v", err)
|
||||||
@ -574,9 +601,11 @@ func (i *InvoiceRegistry) startHtlcTimer(hash lntypes.Hash,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// cancelSingleHtlc cancels a single accepted htlc on an invoice.
|
// cancelSingleHtlc cancels a single accepted htlc on an invoice. It takes
|
||||||
|
// a resolution result which will be used to notify subscribed links and
|
||||||
|
// resolvers of the details of the htlc cancellation.
|
||||||
func (i *InvoiceRegistry) cancelSingleHtlc(hash lntypes.Hash,
|
func (i *InvoiceRegistry) cancelSingleHtlc(hash lntypes.Hash,
|
||||||
key channeldb.CircuitKey) error {
|
key channeldb.CircuitKey, result ResolutionResult) error {
|
||||||
|
|
||||||
i.Lock()
|
i.Lock()
|
||||||
defer i.Unlock()
|
defer i.Unlock()
|
||||||
@ -652,11 +681,11 @@ func (i *InvoiceRegistry) cancelSingleHtlc(hash lntypes.Hash,
|
|||||||
return fmt.Errorf("htlc %v not found", key)
|
return fmt.Errorf("htlc %v not found", key)
|
||||||
}
|
}
|
||||||
if htlc.State == channeldb.HtlcStateCanceled {
|
if htlc.State == channeldb.HtlcStateCanceled {
|
||||||
i.notifyHodlSubscribers(HodlEvent{
|
resolution := *NewFailureResolution(
|
||||||
CircuitKey: key,
|
key, int32(htlc.AcceptHeight), result,
|
||||||
AcceptHeight: int32(htlc.AcceptHeight),
|
)
|
||||||
Preimage: nil,
|
|
||||||
})
|
i.notifyHodlSubscribers(resolution)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -679,7 +708,7 @@ func (i *InvoiceRegistry) cancelSingleHtlc(hash lntypes.Hash,
|
|||||||
func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
||||||
amtPaid lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
|
amtPaid lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
|
||||||
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
|
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
|
||||||
payload Payload) (*HodlEvent, error) {
|
payload Payload) (*HtlcResolution, error) {
|
||||||
|
|
||||||
i.Lock()
|
i.Lock()
|
||||||
defer i.Unlock()
|
defer i.Unlock()
|
||||||
@ -706,7 +735,7 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
|||||||
// We'll attempt to settle an invoice matching this rHash on disk (if
|
// We'll attempt to settle an invoice matching this rHash on disk (if
|
||||||
// one exists). The callback will update the invoice state and/or htlcs.
|
// one exists). The callback will update the invoice state and/or htlcs.
|
||||||
var (
|
var (
|
||||||
result updateResult
|
result ResolutionResult
|
||||||
updateSubscribers bool
|
updateSubscribers bool
|
||||||
)
|
)
|
||||||
invoice, err := i.cdb.UpdateInvoice(
|
invoice, err := i.cdb.UpdateInvoice(
|
||||||
@ -729,11 +758,21 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
|||||||
return updateDesc, nil
|
return updateDesc, nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
switch err {
|
||||||
debugLog(err.Error())
|
case channeldb.ErrInvoiceNotFound:
|
||||||
|
// If the invoice was not found, return a failure resolution
|
||||||
|
// with an invoice not found result.
|
||||||
|
return NewFailureResolution(
|
||||||
|
circuitKey, currentHeight, ResultInvoiceNotFound,
|
||||||
|
), nil
|
||||||
|
|
||||||
|
case nil:
|
||||||
|
|
||||||
|
default:
|
||||||
|
debugLog(err.Error())
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog(result.String())
|
debugLog(result.String())
|
||||||
|
|
||||||
if updateSubscribers {
|
if updateSubscribers {
|
||||||
@ -745,10 +784,9 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
|||||||
|
|
||||||
// If it isn't recorded, cancel htlc.
|
// If it isn't recorded, cancel htlc.
|
||||||
if !ok {
|
if !ok {
|
||||||
return &HodlEvent{
|
return NewFailureResolution(
|
||||||
CircuitKey: circuitKey,
|
circuitKey, currentHeight, result,
|
||||||
AcceptHeight: currentHeight,
|
), nil
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine accepted height of this htlc. If the htlc reached the
|
// Determine accepted height of this htlc. If the htlc reached the
|
||||||
@ -759,10 +797,9 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
|||||||
|
|
||||||
switch invoiceHtlc.State {
|
switch invoiceHtlc.State {
|
||||||
case channeldb.HtlcStateCanceled:
|
case channeldb.HtlcStateCanceled:
|
||||||
return &HodlEvent{
|
return NewFailureResolution(
|
||||||
CircuitKey: circuitKey,
|
circuitKey, acceptHeight, result,
|
||||||
AcceptHeight: acceptHeight,
|
), nil
|
||||||
}, nil
|
|
||||||
|
|
||||||
case channeldb.HtlcStateSettled:
|
case channeldb.HtlcStateSettled:
|
||||||
// Also settle any previously accepted htlcs. The invoice state
|
// Also settle any previously accepted htlcs. The invoice state
|
||||||
@ -773,18 +810,24 @@ func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
i.notifyHodlSubscribers(HodlEvent{
|
// Notify subscribers that the htlcs should be settled
|
||||||
CircuitKey: key,
|
// with our peer. Note that the outcome of the
|
||||||
Preimage: &invoice.Terms.PaymentPreimage,
|
// resolution is set based on the outcome of the single
|
||||||
AcceptHeight: int32(htlc.AcceptHeight),
|
// htlc that we just settled, so may not be accurate
|
||||||
})
|
// for all htlcs.
|
||||||
|
resolution := *NewSettleResolution(
|
||||||
|
invoice.Terms.PaymentPreimage, key,
|
||||||
|
acceptHeight, result,
|
||||||
|
)
|
||||||
|
|
||||||
|
i.notifyHodlSubscribers(resolution)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &HodlEvent{
|
resolution := NewSettleResolution(
|
||||||
CircuitKey: circuitKey,
|
invoice.Terms.PaymentPreimage, circuitKey,
|
||||||
Preimage: &invoice.Terms.PaymentPreimage,
|
acceptHeight, result,
|
||||||
AcceptHeight: acceptHeight,
|
)
|
||||||
}, nil
|
return resolution, nil
|
||||||
|
|
||||||
case channeldb.HtlcStateAccepted:
|
case channeldb.HtlcStateAccepted:
|
||||||
// (Re)start the htlc timer if the invoice is still open. It can
|
// (Re)start the htlc timer if the invoice is still open. It can
|
||||||
@ -836,7 +879,9 @@ func (i *InvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error {
|
|||||||
hash := preimage.Hash()
|
hash := preimage.Hash()
|
||||||
invoice, err := i.cdb.UpdateInvoice(hash, updateInvoice)
|
invoice, err := i.cdb.UpdateInvoice(hash, updateInvoice)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("SettleHodlInvoice with preimage %v: %v", preimage, err)
|
log.Errorf("SettleHodlInvoice with preimage %v: %v",
|
||||||
|
preimage, err)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -854,11 +899,11 @@ func (i *InvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
i.notifyHodlSubscribers(HodlEvent{
|
resolution := *NewSettleResolution(
|
||||||
CircuitKey: key,
|
preimage, key, int32(htlc.AcceptHeight), ResultSettled,
|
||||||
Preimage: &preimage,
|
)
|
||||||
AcceptHeight: int32(htlc.AcceptHeight),
|
|
||||||
})
|
i.notifyHodlSubscribers(resolution)
|
||||||
}
|
}
|
||||||
i.notifyClients(hash, invoice, invoice.State)
|
i.notifyClients(hash, invoice, invoice.State)
|
||||||
|
|
||||||
@ -873,7 +918,8 @@ func (i *InvoiceRegistry) CancelInvoice(payHash lntypes.Hash) error {
|
|||||||
|
|
||||||
// cancelInvoice attempts to cancel the invoice corresponding to the passed
|
// cancelInvoice attempts to cancel the invoice corresponding to the passed
|
||||||
// payment hash. Accepted invoices will only be canceled if explicitly
|
// payment hash. Accepted invoices will only be canceled if explicitly
|
||||||
// requested to do so.
|
// requested to do so. It notifies subscribing links and resolvers that
|
||||||
|
// the associated htlcs were canceled if they change state.
|
||||||
func (i *InvoiceRegistry) cancelInvoiceImpl(payHash lntypes.Hash,
|
func (i *InvoiceRegistry) cancelInvoiceImpl(payHash lntypes.Hash,
|
||||||
cancelAccepted bool) error {
|
cancelAccepted bool) error {
|
||||||
|
|
||||||
@ -932,10 +978,11 @@ func (i *InvoiceRegistry) cancelInvoiceImpl(payHash lntypes.Hash,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
i.notifyHodlSubscribers(HodlEvent{
|
i.notifyHodlSubscribers(
|
||||||
CircuitKey: key,
|
*NewFailureResolution(
|
||||||
AcceptHeight: int32(htlc.AcceptHeight),
|
key, int32(htlc.AcceptHeight), ResultCanceled,
|
||||||
})
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
i.notifyClients(payHash, invoice, channeldb.ContractCanceled)
|
i.notifyClients(payHash, invoice, channeldb.ContractCanceled)
|
||||||
|
|
||||||
@ -1201,9 +1248,10 @@ func (i *InvoiceRegistry) SubscribeSingleInvoice(
|
|||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// notifyHodlSubscribers sends out the hodl event to all current subscribers.
|
// notifyHodlSubscribers sends out the htlc resolution to all current
|
||||||
func (i *InvoiceRegistry) notifyHodlSubscribers(hodlEvent HodlEvent) {
|
// subscribers.
|
||||||
subscribers, ok := i.hodlSubscriptions[hodlEvent.CircuitKey]
|
func (i *InvoiceRegistry) notifyHodlSubscribers(htlcResolution HtlcResolution) {
|
||||||
|
subscribers, ok := i.hodlSubscriptions[htlcResolution.CircuitKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1213,18 +1261,18 @@ func (i *InvoiceRegistry) notifyHodlSubscribers(hodlEvent HodlEvent) {
|
|||||||
// single resolution for each hash.
|
// single resolution for each hash.
|
||||||
for subscriber := range subscribers {
|
for subscriber := range subscribers {
|
||||||
select {
|
select {
|
||||||
case subscriber <- hodlEvent:
|
case subscriber <- htlcResolution:
|
||||||
case <-i.quit:
|
case <-i.quit:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(
|
delete(
|
||||||
i.hodlReverseSubscriptions[subscriber],
|
i.hodlReverseSubscriptions[subscriber],
|
||||||
hodlEvent.CircuitKey,
|
htlcResolution.CircuitKey,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(i.hodlSubscriptions, hodlEvent.CircuitKey)
|
delete(i.hodlSubscriptions, htlcResolution.CircuitKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// hodlSubscribe adds a new invoice subscription.
|
// hodlSubscribe adds a new invoice subscription.
|
||||||
|
@ -66,7 +66,7 @@ func TestSettleInvoice(t *testing.T) {
|
|||||||
hodlChan := make(chan interface{}, 1)
|
hodlChan := make(chan interface{}, 1)
|
||||||
|
|
||||||
// Try to settle invoice with an htlc that expires too soon.
|
// Try to settle invoice with an htlc that expires too soon.
|
||||||
event, err := ctx.registry.NotifyExitHopHtlc(
|
resolution, err := ctx.registry.NotifyExitHopHtlc(
|
||||||
testInvoicePaymentHash, testInvoice.Terms.Value,
|
testInvoicePaymentHash, testInvoice.Terms.Value,
|
||||||
uint32(testCurrentHeight)+testInvoiceCltvDelta-1,
|
uint32(testCurrentHeight)+testInvoiceCltvDelta-1,
|
||||||
testCurrentHeight, getCircuitKey(10), hodlChan, testPayload,
|
testCurrentHeight, getCircuitKey(10), hodlChan, testPayload,
|
||||||
@ -74,23 +74,30 @@ func TestSettleInvoice(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if event.Preimage != nil {
|
if resolution.Preimage != nil {
|
||||||
t.Fatal("expected cancel event")
|
t.Fatal("expected cancel resolution")
|
||||||
}
|
}
|
||||||
if event.AcceptHeight != testCurrentHeight {
|
if resolution.AcceptHeight != testCurrentHeight {
|
||||||
t.Fatalf("expected acceptHeight %v, but got %v",
|
t.Fatalf("expected acceptHeight %v, but got %v",
|
||||||
testCurrentHeight, event.AcceptHeight)
|
testCurrentHeight, resolution.AcceptHeight)
|
||||||
|
}
|
||||||
|
if resolution.Outcome != ResultExpiryTooSoon {
|
||||||
|
t.Fatalf("expected expiry too soon, got: %v",
|
||||||
|
resolution.Outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settle invoice with a slightly higher amount.
|
// Settle invoice with a slightly higher amount.
|
||||||
amtPaid := lnwire.MilliSatoshi(100500)
|
amtPaid := lnwire.MilliSatoshi(100500)
|
||||||
_, err = ctx.registry.NotifyExitHopHtlc(
|
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||||
testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
||||||
getCircuitKey(0), hodlChan, testPayload,
|
getCircuitKey(0), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if resolution.Outcome != ResultSettled {
|
||||||
|
t.Fatalf("expected settled, got: %v", resolution.Outcome)
|
||||||
|
}
|
||||||
|
|
||||||
// We expect the settled state to be sent to the single invoice
|
// We expect the settled state to be sent to the single invoice
|
||||||
// subscriber.
|
// subscriber.
|
||||||
@ -120,42 +127,54 @@ func TestSettleInvoice(t *testing.T) {
|
|||||||
|
|
||||||
// Try to settle again with the same htlc id. We need this idempotent
|
// Try to settle again with the same htlc id. We need this idempotent
|
||||||
// behaviour after a restart.
|
// behaviour after a restart.
|
||||||
event, err = ctx.registry.NotifyExitHopHtlc(
|
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||||
testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
||||||
getCircuitKey(0), hodlChan, testPayload,
|
getCircuitKey(0), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
|
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
|
||||||
}
|
}
|
||||||
if event.Preimage == nil {
|
if resolution.Preimage == nil {
|
||||||
t.Fatal("expected settle event")
|
t.Fatal("expected settle resolution")
|
||||||
|
}
|
||||||
|
if resolution.Outcome != ResultReplayToSettled {
|
||||||
|
t.Fatalf("expected replay settled, got: %v",
|
||||||
|
resolution.Outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to settle again with a new higher-valued htlc. This payment
|
// Try to settle again with a new higher-valued htlc. This payment
|
||||||
// should also be accepted, to prevent any change in behaviour for a
|
// should also be accepted, to prevent any change in behaviour for a
|
||||||
// paid invoice that may open up a probe vector.
|
// paid invoice that may open up a probe vector.
|
||||||
event, err = ctx.registry.NotifyExitHopHtlc(
|
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||||
testInvoicePaymentHash, amtPaid+600, testHtlcExpiry, testCurrentHeight,
|
testInvoicePaymentHash, amtPaid+600, testHtlcExpiry, testCurrentHeight,
|
||||||
getCircuitKey(1), hodlChan, testPayload,
|
getCircuitKey(1), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
|
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
|
||||||
}
|
}
|
||||||
if event.Preimage == nil {
|
if resolution.Preimage == nil {
|
||||||
t.Fatal("expected settle event")
|
t.Fatal("expected settle resolution")
|
||||||
|
}
|
||||||
|
if resolution.Outcome != ResultDuplicateToSettled {
|
||||||
|
t.Fatalf("expected duplicate settled, got: %v",
|
||||||
|
resolution.Outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to settle again with a lower amount. This should fail just as it
|
// Try to settle again with a lower amount. This should fail just as it
|
||||||
// would have failed if it were the first payment.
|
// would have failed if it were the first payment.
|
||||||
event, err = ctx.registry.NotifyExitHopHtlc(
|
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||||
testInvoicePaymentHash, amtPaid-600, testHtlcExpiry, testCurrentHeight,
|
testInvoicePaymentHash, amtPaid-600, testHtlcExpiry, testCurrentHeight,
|
||||||
getCircuitKey(2), hodlChan, testPayload,
|
getCircuitKey(2), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
|
t.Fatalf("unexpected NotifyExitHopHtlc error: %v", err)
|
||||||
}
|
}
|
||||||
if event.Preimage != nil {
|
if resolution.Preimage != nil {
|
||||||
t.Fatal("expected cancel event")
|
t.Fatal("expected cancel resolution")
|
||||||
|
}
|
||||||
|
if resolution.Outcome != ResultAmountTooLow {
|
||||||
|
t.Fatalf("expected amount too low, got: %v",
|
||||||
|
resolution.Outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that settled amount is equal to the sum of values of the htlcs
|
// Check that settled amount is equal to the sum of values of the htlcs
|
||||||
@ -177,7 +196,7 @@ func TestSettleInvoice(t *testing.T) {
|
|||||||
// As this is a direct sette, we expect nothing on the hodl chan.
|
// As this is a direct sette, we expect nothing on the hodl chan.
|
||||||
select {
|
select {
|
||||||
case <-hodlChan:
|
case <-hodlChan:
|
||||||
t.Fatal("unexpected event")
|
t.Fatal("unexpected resolution")
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -270,9 +289,9 @@ func TestCancelInvoice(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Notify arrival of a new htlc paying to this invoice. This should
|
// Notify arrival of a new htlc paying to this invoice. This should
|
||||||
// result in a cancel event.
|
// result in a cancel resolution.
|
||||||
hodlChan := make(chan interface{})
|
hodlChan := make(chan interface{})
|
||||||
event, err := ctx.registry.NotifyExitHopHtlc(
|
resolution, err := ctx.registry.NotifyExitHopHtlc(
|
||||||
testInvoicePaymentHash, amt, testHtlcExpiry, testCurrentHeight,
|
testInvoicePaymentHash, amt, testHtlcExpiry, testCurrentHeight,
|
||||||
getCircuitKey(0), hodlChan, testPayload,
|
getCircuitKey(0), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
@ -280,12 +299,16 @@ func TestCancelInvoice(t *testing.T) {
|
|||||||
t.Fatal("expected settlement of a canceled invoice to succeed")
|
t.Fatal("expected settlement of a canceled invoice to succeed")
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.Preimage != nil {
|
if resolution.Preimage != nil {
|
||||||
t.Fatal("expected cancel hodl event")
|
t.Fatal("expected cancel htlc resolution")
|
||||||
}
|
}
|
||||||
if event.AcceptHeight != testCurrentHeight {
|
if resolution.AcceptHeight != testCurrentHeight {
|
||||||
t.Fatalf("expected acceptHeight %v, but got %v",
|
t.Fatalf("expected acceptHeight %v, but got %v",
|
||||||
testCurrentHeight, event.AcceptHeight)
|
testCurrentHeight, resolution.AcceptHeight)
|
||||||
|
}
|
||||||
|
if resolution.Outcome != ResultInvoiceAlreadyCanceled {
|
||||||
|
t.Fatalf("expected invoice already canceled, got: %v",
|
||||||
|
resolution.Outcome)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,54 +377,58 @@ func TestSettleHoldInvoice(t *testing.T) {
|
|||||||
|
|
||||||
// NotifyExitHopHtlc without a preimage present in the invoice registry
|
// NotifyExitHopHtlc without a preimage present in the invoice registry
|
||||||
// should be possible.
|
// should be possible.
|
||||||
event, err := registry.NotifyExitHopHtlc(
|
resolution, err := registry.NotifyExitHopHtlc(
|
||||||
testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
||||||
getCircuitKey(0), hodlChan, testPayload,
|
getCircuitKey(0), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected settle to succeed but got %v", err)
|
t.Fatalf("expected settle to succeed but got %v", err)
|
||||||
}
|
}
|
||||||
if event != nil {
|
if resolution != nil {
|
||||||
t.Fatalf("expected htlc to be held")
|
t.Fatalf("expected htlc to be held")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test idempotency.
|
// Test idempotency.
|
||||||
event, err = registry.NotifyExitHopHtlc(
|
resolution, err = registry.NotifyExitHopHtlc(
|
||||||
testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
||||||
getCircuitKey(0), hodlChan, testPayload,
|
getCircuitKey(0), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected settle to succeed but got %v", err)
|
t.Fatalf("expected settle to succeed but got %v", err)
|
||||||
}
|
}
|
||||||
if event != nil {
|
if resolution != nil {
|
||||||
t.Fatalf("expected htlc to be held")
|
t.Fatalf("expected htlc to be held")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test replay at a higher height. We expect the same result because it
|
// Test replay at a higher height. We expect the same result because it
|
||||||
// is a replay.
|
// is a replay.
|
||||||
event, err = registry.NotifyExitHopHtlc(
|
resolution, err = registry.NotifyExitHopHtlc(
|
||||||
testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight+10,
|
testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight+10,
|
||||||
getCircuitKey(0), hodlChan, testPayload,
|
getCircuitKey(0), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected settle to succeed but got %v", err)
|
t.Fatalf("expected settle to succeed but got %v", err)
|
||||||
}
|
}
|
||||||
if event != nil {
|
if resolution != nil {
|
||||||
t.Fatalf("expected htlc to be held")
|
t.Fatalf("expected htlc to be held")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test a new htlc coming in that doesn't meet the final cltv delta
|
// Test a new htlc coming in that doesn't meet the final cltv delta
|
||||||
// requirement. It should be rejected.
|
// requirement. It should be rejected.
|
||||||
event, err = registry.NotifyExitHopHtlc(
|
resolution, err = registry.NotifyExitHopHtlc(
|
||||||
testInvoicePaymentHash, amtPaid, 1, testCurrentHeight,
|
testInvoicePaymentHash, amtPaid, 1, testCurrentHeight,
|
||||||
getCircuitKey(1), hodlChan, testPayload,
|
getCircuitKey(1), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected settle to succeed but got %v", err)
|
t.Fatalf("expected settle to succeed but got %v", err)
|
||||||
}
|
}
|
||||||
if event == nil || event.Preimage != nil {
|
if resolution == nil || resolution.Preimage != nil {
|
||||||
t.Fatalf("expected htlc to be canceled")
|
t.Fatalf("expected htlc to be canceled")
|
||||||
}
|
}
|
||||||
|
if resolution.Outcome != ResultExpiryTooSoon {
|
||||||
|
t.Fatalf("expected expiry too soon, got: %v",
|
||||||
|
resolution.Outcome)
|
||||||
|
}
|
||||||
|
|
||||||
// We expect the accepted state to be sent to the single invoice
|
// We expect the accepted state to be sent to the single invoice
|
||||||
// subscriber. For all invoice subscribers, we don't expect an update.
|
// subscriber. For all invoice subscribers, we don't expect an update.
|
||||||
@ -421,13 +448,17 @@ func TestSettleHoldInvoice(t *testing.T) {
|
|||||||
t.Fatal("expected set preimage to succeed")
|
t.Fatal("expected set preimage to succeed")
|
||||||
}
|
}
|
||||||
|
|
||||||
hodlEvent := (<-hodlChan).(HodlEvent)
|
htlcResolution := (<-hodlChan).(HtlcResolution)
|
||||||
if *hodlEvent.Preimage != testInvoicePreimage {
|
if *htlcResolution.Preimage != testInvoicePreimage {
|
||||||
t.Fatal("unexpected preimage in hodl event")
|
t.Fatal("unexpected preimage in hodl resolution")
|
||||||
}
|
}
|
||||||
if hodlEvent.AcceptHeight != testCurrentHeight {
|
if htlcResolution.AcceptHeight != testCurrentHeight {
|
||||||
t.Fatalf("expected acceptHeight %v, but got %v",
|
t.Fatalf("expected acceptHeight %v, but got %v",
|
||||||
testCurrentHeight, event.AcceptHeight)
|
testCurrentHeight, resolution.AcceptHeight)
|
||||||
|
}
|
||||||
|
if htlcResolution.Outcome != ResultSettled {
|
||||||
|
t.Fatalf("expected result settled, got: %v",
|
||||||
|
htlcResolution.Outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We expect a settled notification to be sent out for both all and
|
// We expect a settled notification to be sent out for both all and
|
||||||
@ -496,14 +527,14 @@ func TestCancelHoldInvoice(t *testing.T) {
|
|||||||
|
|
||||||
// NotifyExitHopHtlc without a preimage present in the invoice registry
|
// NotifyExitHopHtlc without a preimage present in the invoice registry
|
||||||
// should be possible.
|
// should be possible.
|
||||||
event, err := registry.NotifyExitHopHtlc(
|
resolution, err := registry.NotifyExitHopHtlc(
|
||||||
testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight,
|
||||||
getCircuitKey(0), hodlChan, testPayload,
|
getCircuitKey(0), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected settle to succeed but got %v", err)
|
t.Fatalf("expected settle to succeed but got %v", err)
|
||||||
}
|
}
|
||||||
if event != nil {
|
if resolution != nil {
|
||||||
t.Fatalf("expected htlc to be held")
|
t.Fatalf("expected htlc to be held")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -513,35 +544,39 @@ func TestCancelHoldInvoice(t *testing.T) {
|
|||||||
t.Fatal("cancel invoice failed")
|
t.Fatal("cancel invoice failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
hodlEvent := (<-hodlChan).(HodlEvent)
|
htlcResolution := (<-hodlChan).(HtlcResolution)
|
||||||
if hodlEvent.Preimage != nil {
|
if htlcResolution.Preimage != nil {
|
||||||
t.Fatal("expected cancel hodl event")
|
t.Fatal("expected cancel htlc resolution")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Offering the same htlc again at a higher height should still result
|
// Offering the same htlc again at a higher height should still result
|
||||||
// in a rejection. The accept height is expected to be the original
|
// in a rejection. The accept height is expected to be the original
|
||||||
// accept height.
|
// accept height.
|
||||||
event, err = registry.NotifyExitHopHtlc(
|
resolution, err = registry.NotifyExitHopHtlc(
|
||||||
testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight+1,
|
testInvoicePaymentHash, amtPaid, testHtlcExpiry, testCurrentHeight+1,
|
||||||
getCircuitKey(0), hodlChan, testPayload,
|
getCircuitKey(0), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected settle to succeed but got %v", err)
|
t.Fatalf("expected settle to succeed but got %v", err)
|
||||||
}
|
}
|
||||||
if event.Preimage != nil {
|
if resolution.Preimage != nil {
|
||||||
t.Fatalf("expected htlc to be canceled")
|
t.Fatalf("expected htlc to be canceled")
|
||||||
}
|
}
|
||||||
if event.AcceptHeight != testCurrentHeight {
|
if resolution.AcceptHeight != testCurrentHeight {
|
||||||
t.Fatalf("expected acceptHeight %v, but got %v",
|
t.Fatalf("expected acceptHeight %v, but got %v",
|
||||||
testCurrentHeight, event.AcceptHeight)
|
testCurrentHeight, resolution.AcceptHeight)
|
||||||
|
}
|
||||||
|
if resolution.Outcome != ResultReplayToCanceled {
|
||||||
|
t.Fatalf("expected replay to canceled, got %v",
|
||||||
|
resolution.Outcome)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestUnknownInvoice tests that invoice registry returns an error when the
|
// TestUnknownInvoice tests that invoice registry returns an error when the
|
||||||
// invoice is unknown. This is to guard against returning a cancel hodl event
|
// invoice is unknown. This is to guard against returning a cancel htlc
|
||||||
// for forwarded htlcs. In the link, NotifyExitHopHtlc is only called if we are
|
// resolution for forwarded htlcs. In the link, NotifyExitHopHtlc is only called
|
||||||
// the exit hop, but in htlcIncomingContestResolver it is called with forwarded
|
// if we are the exit hop, but in htlcIncomingContestResolver it is called with
|
||||||
// htlc hashes as well.
|
// forwarded htlc hashes as well.
|
||||||
func TestUnknownInvoice(t *testing.T) {
|
func TestUnknownInvoice(t *testing.T) {
|
||||||
ctx := newTestContext(t)
|
ctx := newTestContext(t)
|
||||||
defer ctx.cleanup()
|
defer ctx.cleanup()
|
||||||
@ -550,17 +585,23 @@ func TestUnknownInvoice(t *testing.T) {
|
|||||||
// succeed.
|
// succeed.
|
||||||
hodlChan := make(chan interface{})
|
hodlChan := make(chan interface{})
|
||||||
amt := lnwire.MilliSatoshi(100000)
|
amt := lnwire.MilliSatoshi(100000)
|
||||||
_, err := ctx.registry.NotifyExitHopHtlc(
|
result, err := ctx.registry.NotifyExitHopHtlc(
|
||||||
testInvoicePaymentHash, amt, testHtlcExpiry, testCurrentHeight,
|
testInvoicePaymentHash, amt, testHtlcExpiry, testCurrentHeight,
|
||||||
getCircuitKey(0), hodlChan, testPayload,
|
getCircuitKey(0), hodlChan, testPayload,
|
||||||
)
|
)
|
||||||
if err != channeldb.ErrInvoiceNotFound {
|
if err != nil {
|
||||||
t.Fatal("expected invoice not found error")
|
t.Fatal("unexpected error")
|
||||||
|
}
|
||||||
|
if result.Outcome != ResultInvoiceNotFound {
|
||||||
|
t.Fatalf("expected ResultInvoiceNotFound, got: %v",
|
||||||
|
result.Outcome)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSettleMpp tests settling of an invoice with multiple partial payments.
|
// TestMppPayment tests settling of an invoice with multiple partial payments.
|
||||||
func TestSettleMpp(t *testing.T) {
|
// It covers the case where there is a mpp timeout before the whole invoice is
|
||||||
|
// paid and the case where the invoice is settled in time.
|
||||||
|
func TestMppPayment(t *testing.T) {
|
||||||
defer timeout()()
|
defer timeout()()
|
||||||
|
|
||||||
ctx := newTestContext(t)
|
ctx := newTestContext(t)
|
||||||
@ -578,7 +619,7 @@ func TestSettleMpp(t *testing.T) {
|
|||||||
|
|
||||||
// Send htlc 1.
|
// Send htlc 1.
|
||||||
hodlChan1 := make(chan interface{}, 1)
|
hodlChan1 := make(chan interface{}, 1)
|
||||||
event, err := ctx.registry.NotifyExitHopHtlc(
|
resolution, err := ctx.registry.NotifyExitHopHtlc(
|
||||||
testInvoicePaymentHash, testInvoice.Terms.Value/2,
|
testInvoicePaymentHash, testInvoice.Terms.Value/2,
|
||||||
testHtlcExpiry,
|
testHtlcExpiry,
|
||||||
testCurrentHeight, getCircuitKey(10), hodlChan1, mppPayload,
|
testCurrentHeight, getCircuitKey(10), hodlChan1, mppPayload,
|
||||||
@ -586,21 +627,25 @@ func TestSettleMpp(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if event != nil {
|
if resolution != nil {
|
||||||
t.Fatal("expected no direct resolution")
|
t.Fatal("expected no direct resolution")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simulate mpp timeout releasing htlc 1.
|
// Simulate mpp timeout releasing htlc 1.
|
||||||
ctx.clock.SetTime(testTime.Add(30 * time.Second))
|
ctx.clock.SetTime(testTime.Add(30 * time.Second))
|
||||||
|
|
||||||
hodlEvent := (<-hodlChan1).(HodlEvent)
|
htlcResolution := (<-hodlChan1).(HtlcResolution)
|
||||||
if hodlEvent.Preimage != nil {
|
if htlcResolution.Preimage != nil {
|
||||||
t.Fatal("expected cancel event")
|
t.Fatal("expected cancel resolution")
|
||||||
|
}
|
||||||
|
if htlcResolution.Outcome != ResultMppTimeout {
|
||||||
|
t.Fatalf("expected mpp timeout, got: %v",
|
||||||
|
htlcResolution.Outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send htlc 2.
|
// Send htlc 2.
|
||||||
hodlChan2 := make(chan interface{}, 1)
|
hodlChan2 := make(chan interface{}, 1)
|
||||||
event, err = ctx.registry.NotifyExitHopHtlc(
|
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||||
testInvoicePaymentHash, testInvoice.Terms.Value/2,
|
testInvoicePaymentHash, testInvoice.Terms.Value/2,
|
||||||
testHtlcExpiry,
|
testHtlcExpiry,
|
||||||
testCurrentHeight, getCircuitKey(11), hodlChan2, mppPayload,
|
testCurrentHeight, getCircuitKey(11), hodlChan2, mppPayload,
|
||||||
@ -608,13 +653,13 @@ func TestSettleMpp(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if event != nil {
|
if resolution != nil {
|
||||||
t.Fatal("expected no direct resolution")
|
t.Fatal("expected no direct resolution")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send htlc 3.
|
// Send htlc 3.
|
||||||
hodlChan3 := make(chan interface{}, 1)
|
hodlChan3 := make(chan interface{}, 1)
|
||||||
event, err = ctx.registry.NotifyExitHopHtlc(
|
resolution, err = ctx.registry.NotifyExitHopHtlc(
|
||||||
testInvoicePaymentHash, testInvoice.Terms.Value/2,
|
testInvoicePaymentHash, testInvoice.Terms.Value/2,
|
||||||
testHtlcExpiry,
|
testHtlcExpiry,
|
||||||
testCurrentHeight, getCircuitKey(12), hodlChan3, mppPayload,
|
testCurrentHeight, getCircuitKey(12), hodlChan3, mppPayload,
|
||||||
@ -622,12 +667,16 @@ func TestSettleMpp(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if event == nil {
|
if resolution == nil {
|
||||||
t.Fatal("expected a settle event")
|
t.Fatal("expected a settle resolution")
|
||||||
|
}
|
||||||
|
if resolution.Outcome != ResultSettled {
|
||||||
|
t.Fatalf("expected result settled, got: %v",
|
||||||
|
resolution.Outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that settled amount is equal to the sum of values of the htlcs
|
// Check that settled amount is equal to the sum of values of the htlcs
|
||||||
// 0 and 1.
|
// 2 and 3.
|
||||||
inv, err := ctx.registry.LookupInvoice(testInvoicePaymentHash)
|
inv, err := ctx.registry.LookupInvoice(testInvoicePaymentHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -8,86 +8,141 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/record"
|
"github.com/lightningnetwork/lnd/record"
|
||||||
)
|
)
|
||||||
|
|
||||||
// updateResult is the result of the invoice update call.
|
// ResolutionResult provides metadata which about an invoice update which can
|
||||||
type updateResult uint8
|
// be used to take custom actions on resolution of the htlc. Only results which
|
||||||
|
// are actionable by the link are exported.
|
||||||
|
type ResolutionResult uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
resultInvalid updateResult = iota
|
resultInvalid ResolutionResult = iota
|
||||||
resultReplayToCanceled
|
|
||||||
resultReplayToAccepted
|
// ResultReplayToCanceled is returned when we replay a canceled invoice.
|
||||||
resultReplayToSettled
|
ResultReplayToCanceled
|
||||||
resultInvoiceAlreadyCanceled
|
|
||||||
resultAmountTooLow
|
// ResultReplayToAccepted is returned when we replay an accepted invoice.
|
||||||
resultExpiryTooSoon
|
ResultReplayToAccepted
|
||||||
resultDuplicateToAccepted
|
|
||||||
resultDuplicateToSettled
|
// ResultReplayToSettled is returned when we replay a settled invoice.
|
||||||
resultAccepted
|
ResultReplayToSettled
|
||||||
resultSettled
|
|
||||||
resultInvoiceNotOpen
|
// ResultInvoiceAlreadyCanceled is returned when trying to pay an invoice
|
||||||
resultPartialAccepted
|
// that is already canceled.
|
||||||
resultMppInProgress
|
ResultInvoiceAlreadyCanceled
|
||||||
resultAddressMismatch
|
|
||||||
resultHtlcSetTotalMismatch
|
// ResultAmountTooLow is returned when an invoice is underpaid.
|
||||||
resultHtlcSetTotalTooLow
|
ResultAmountTooLow
|
||||||
resultHtlcSetOverpayment
|
|
||||||
|
// ResultExpiryTooSoon is returned when we do not accept an invoice payment
|
||||||
|
// because it expires too soon.
|
||||||
|
ResultExpiryTooSoon
|
||||||
|
|
||||||
|
// ResultDuplicateToAccepted is returned when we accept a duplicate htlc.
|
||||||
|
ResultDuplicateToAccepted
|
||||||
|
|
||||||
|
// ResultDuplicateToSettled is returned when we settle an invoice which has
|
||||||
|
// already been settled at least once.
|
||||||
|
ResultDuplicateToSettled
|
||||||
|
|
||||||
|
// ResultAccepted is returned when we accept a hodl invoice.
|
||||||
|
ResultAccepted
|
||||||
|
|
||||||
|
// ResultSettled is returned when we settle an invoice.
|
||||||
|
ResultSettled
|
||||||
|
|
||||||
|
// ResultCanceled is returned when we cancel an invoice and its associated
|
||||||
|
// htlcs.
|
||||||
|
ResultCanceled
|
||||||
|
|
||||||
|
// ResultInvoiceNotOpen is returned when a mpp invoice is not open.
|
||||||
|
ResultInvoiceNotOpen
|
||||||
|
|
||||||
|
// ResultPartialAccepted is returned when we have partially received
|
||||||
|
// payment.
|
||||||
|
ResultPartialAccepted
|
||||||
|
|
||||||
|
// ResultMppInProgress is returned when we are busy receiving a mpp payment.
|
||||||
|
ResultMppInProgress
|
||||||
|
|
||||||
|
// ResultMppTimeout is returned when an invoice paid with multiple partial
|
||||||
|
// payments times out before it is fully paid.
|
||||||
|
ResultMppTimeout
|
||||||
|
|
||||||
|
// ResultAddressMismatch is returned when the payment address for a mpp
|
||||||
|
// invoice does not match.
|
||||||
|
ResultAddressMismatch
|
||||||
|
|
||||||
|
// ResultHtlcSetTotalMismatch is returned when the amount paid by a htlc
|
||||||
|
// does not match its set total.
|
||||||
|
ResultHtlcSetTotalMismatch
|
||||||
|
|
||||||
|
// ResultHtlcSetTotalTooLow is returned when a mpp set total is too low for
|
||||||
|
// an invoice.
|
||||||
|
ResultHtlcSetTotalTooLow
|
||||||
|
|
||||||
|
// ResultHtlcSetOverpayment is returned when a mpp set is overpaid.
|
||||||
|
ResultHtlcSetOverpayment
|
||||||
|
|
||||||
|
// ResultInvoiceNotFound is returned when an attempt is made to pay an
|
||||||
|
// invoice that is unknown to us.
|
||||||
|
ResultInvoiceNotFound
|
||||||
)
|
)
|
||||||
|
|
||||||
// String returns a human-readable representation of the invoice update result.
|
// String returns a human-readable representation of the invoice update result.
|
||||||
func (u updateResult) String() string {
|
func (u ResolutionResult) String() string {
|
||||||
switch u {
|
switch u {
|
||||||
|
|
||||||
case resultInvalid:
|
case resultInvalid:
|
||||||
return "invalid"
|
return "invalid"
|
||||||
|
|
||||||
case resultReplayToCanceled:
|
case ResultReplayToCanceled:
|
||||||
return "replayed htlc to canceled invoice"
|
return "replayed htlc to canceled invoice"
|
||||||
|
|
||||||
case resultReplayToAccepted:
|
case ResultReplayToAccepted:
|
||||||
return "replayed htlc to accepted invoice"
|
return "replayed htlc to accepted invoice"
|
||||||
|
|
||||||
case resultReplayToSettled:
|
case ResultReplayToSettled:
|
||||||
return "replayed htlc to settled invoice"
|
return "replayed htlc to settled invoice"
|
||||||
|
|
||||||
case resultInvoiceAlreadyCanceled:
|
case ResultInvoiceAlreadyCanceled:
|
||||||
return "invoice already canceled"
|
return "invoice already canceled"
|
||||||
|
|
||||||
case resultAmountTooLow:
|
case ResultAmountTooLow:
|
||||||
return "amount too low"
|
return "amount too low"
|
||||||
|
|
||||||
case resultExpiryTooSoon:
|
case ResultExpiryTooSoon:
|
||||||
return "expiry too soon"
|
return "expiry too soon"
|
||||||
|
|
||||||
case resultDuplicateToAccepted:
|
case ResultDuplicateToAccepted:
|
||||||
return "accepting duplicate payment to accepted invoice"
|
return "accepting duplicate payment to accepted invoice"
|
||||||
|
|
||||||
case resultDuplicateToSettled:
|
case ResultDuplicateToSettled:
|
||||||
return "accepting duplicate payment to settled invoice"
|
return "accepting duplicate payment to settled invoice"
|
||||||
|
|
||||||
case resultAccepted:
|
case ResultAccepted:
|
||||||
return "accepted"
|
return "accepted"
|
||||||
|
|
||||||
case resultSettled:
|
case ResultSettled:
|
||||||
return "settled"
|
return "settled"
|
||||||
|
|
||||||
case resultInvoiceNotOpen:
|
case ResultInvoiceNotOpen:
|
||||||
return "invoice no longer open"
|
return "invoice no longer open"
|
||||||
|
|
||||||
case resultPartialAccepted:
|
case ResultPartialAccepted:
|
||||||
return "partial payment accepted"
|
return "partial payment accepted"
|
||||||
|
|
||||||
case resultMppInProgress:
|
case ResultMppInProgress:
|
||||||
return "mpp reception in progress"
|
return "mpp reception in progress"
|
||||||
|
|
||||||
case resultAddressMismatch:
|
case ResultAddressMismatch:
|
||||||
return "payment address mismatch"
|
return "payment address mismatch"
|
||||||
|
|
||||||
case resultHtlcSetTotalMismatch:
|
case ResultHtlcSetTotalMismatch:
|
||||||
return "htlc total amt doesn't match set total"
|
return "htlc total amt doesn't match set total"
|
||||||
|
|
||||||
case resultHtlcSetTotalTooLow:
|
case ResultHtlcSetTotalTooLow:
|
||||||
return "set total too low for invoice"
|
return "set total too low for invoice"
|
||||||
|
|
||||||
case resultHtlcSetOverpayment:
|
case ResultHtlcSetOverpayment:
|
||||||
return "mpp is overpaying set total"
|
return "mpp is overpaying set total"
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -110,20 +165,20 @@ type invoiceUpdateCtx struct {
|
|||||||
// updateInvoice is a callback for DB.UpdateInvoice that contains the invoice
|
// updateInvoice is a callback for DB.UpdateInvoice that contains the invoice
|
||||||
// settlement logic.
|
// settlement logic.
|
||||||
func updateInvoice(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
func updateInvoice(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
||||||
*channeldb.InvoiceUpdateDesc, updateResult, error) {
|
*channeldb.InvoiceUpdateDesc, ResolutionResult, error) {
|
||||||
|
|
||||||
// Don't update the invoice when this is a replayed htlc.
|
// Don't update the invoice when this is a replayed htlc.
|
||||||
htlc, ok := inv.Htlcs[ctx.circuitKey]
|
htlc, ok := inv.Htlcs[ctx.circuitKey]
|
||||||
if ok {
|
if ok {
|
||||||
switch htlc.State {
|
switch htlc.State {
|
||||||
case channeldb.HtlcStateCanceled:
|
case channeldb.HtlcStateCanceled:
|
||||||
return nil, resultReplayToCanceled, nil
|
return nil, ResultReplayToCanceled, nil
|
||||||
|
|
||||||
case channeldb.HtlcStateAccepted:
|
case channeldb.HtlcStateAccepted:
|
||||||
return nil, resultReplayToAccepted, nil
|
return nil, ResultReplayToAccepted, nil
|
||||||
|
|
||||||
case channeldb.HtlcStateSettled:
|
case channeldb.HtlcStateSettled:
|
||||||
return nil, resultReplayToSettled, nil
|
return nil, ResultReplayToSettled, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, 0, errors.New("unknown htlc state")
|
return nil, 0, errors.New("unknown htlc state")
|
||||||
@ -140,7 +195,7 @@ func updateInvoice(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
|||||||
// updateMpp is a callback for DB.UpdateInvoice that contains the invoice
|
// updateMpp is a callback for DB.UpdateInvoice that contains the invoice
|
||||||
// settlement logic for mpp payments.
|
// settlement logic for mpp payments.
|
||||||
func updateMpp(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
func updateMpp(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
||||||
*channeldb.InvoiceUpdateDesc, updateResult, error) {
|
*channeldb.InvoiceUpdateDesc, ResolutionResult, error) {
|
||||||
|
|
||||||
// Start building the accept descriptor.
|
// Start building the accept descriptor.
|
||||||
acceptDesc := &channeldb.HtlcAcceptDesc{
|
acceptDesc := &channeldb.HtlcAcceptDesc{
|
||||||
@ -156,23 +211,23 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
|||||||
// Because non-mpp payments don't have a payment address, this is needed
|
// Because non-mpp payments don't have a payment address, this is needed
|
||||||
// to thwart probing.
|
// to thwart probing.
|
||||||
if inv.State != channeldb.ContractOpen {
|
if inv.State != channeldb.ContractOpen {
|
||||||
return nil, resultInvoiceNotOpen, nil
|
return nil, ResultInvoiceNotOpen, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the payment address that authorizes the payment.
|
// Check the payment address that authorizes the payment.
|
||||||
if ctx.mpp.PaymentAddr() != inv.Terms.PaymentAddr {
|
if ctx.mpp.PaymentAddr() != inv.Terms.PaymentAddr {
|
||||||
return nil, resultAddressMismatch, nil
|
return nil, ResultAddressMismatch, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't accept zero-valued sets.
|
// Don't accept zero-valued sets.
|
||||||
if ctx.mpp.TotalMsat() == 0 {
|
if ctx.mpp.TotalMsat() == 0 {
|
||||||
return nil, resultHtlcSetTotalTooLow, nil
|
return nil, ResultHtlcSetTotalTooLow, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that the total amt of the htlc set is high enough. In case this
|
// Check that the total amt of the htlc set is high enough. In case this
|
||||||
// is a zero-valued invoice, it will always be enough.
|
// is a zero-valued invoice, it will always be enough.
|
||||||
if ctx.mpp.TotalMsat() < inv.Terms.Value {
|
if ctx.mpp.TotalMsat() < inv.Terms.Value {
|
||||||
return nil, resultHtlcSetTotalTooLow, nil
|
return nil, ResultHtlcSetTotalTooLow, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether total amt matches other htlcs in the set.
|
// Check whether total amt matches other htlcs in the set.
|
||||||
@ -186,7 +241,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ctx.mpp.TotalMsat() != htlc.MppTotalAmt {
|
if ctx.mpp.TotalMsat() != htlc.MppTotalAmt {
|
||||||
return nil, resultHtlcSetTotalMismatch, nil
|
return nil, ResultHtlcSetTotalMismatch, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
newSetTotal += htlc.Amt
|
newSetTotal += htlc.Amt
|
||||||
@ -197,16 +252,16 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
|||||||
|
|
||||||
// Make sure the communicated set total isn't overpaid.
|
// Make sure the communicated set total isn't overpaid.
|
||||||
if newSetTotal > ctx.mpp.TotalMsat() {
|
if newSetTotal > ctx.mpp.TotalMsat() {
|
||||||
return nil, resultHtlcSetOverpayment, nil
|
return nil, ResultHtlcSetOverpayment, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// The invoice is still open. Check the expiry.
|
// The invoice is still open. Check the expiry.
|
||||||
if ctx.expiry < uint32(ctx.currentHeight+ctx.finalCltvRejectDelta) {
|
if ctx.expiry < uint32(ctx.currentHeight+ctx.finalCltvRejectDelta) {
|
||||||
return nil, resultExpiryTooSoon, nil
|
return nil, ResultExpiryTooSoon, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.expiry < uint32(ctx.currentHeight+inv.Terms.FinalCltvDelta) {
|
if ctx.expiry < uint32(ctx.currentHeight+inv.Terms.FinalCltvDelta) {
|
||||||
return nil, resultExpiryTooSoon, nil
|
return nil, ResultExpiryTooSoon, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record HTLC in the invoice database.
|
// Record HTLC in the invoice database.
|
||||||
@ -221,7 +276,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
|||||||
// If the invoice cannot be settled yet, only record the htlc.
|
// If the invoice cannot be settled yet, only record the htlc.
|
||||||
setComplete := newSetTotal == ctx.mpp.TotalMsat()
|
setComplete := newSetTotal == ctx.mpp.TotalMsat()
|
||||||
if !setComplete {
|
if !setComplete {
|
||||||
return &update, resultPartialAccepted, nil
|
return &update, ResultPartialAccepted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check to see if we can settle or this is an hold invoice and
|
// Check to see if we can settle or this is an hold invoice and
|
||||||
@ -231,7 +286,7 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
|||||||
update.State = &channeldb.InvoiceStateUpdateDesc{
|
update.State = &channeldb.InvoiceStateUpdateDesc{
|
||||||
NewState: channeldb.ContractAccepted,
|
NewState: channeldb.ContractAccepted,
|
||||||
}
|
}
|
||||||
return &update, resultAccepted, nil
|
return &update, ResultAccepted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
update.State = &channeldb.InvoiceStateUpdateDesc{
|
update.State = &channeldb.InvoiceStateUpdateDesc{
|
||||||
@ -239,18 +294,18 @@ func updateMpp(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
|||||||
Preimage: inv.Terms.PaymentPreimage,
|
Preimage: inv.Terms.PaymentPreimage,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &update, resultSettled, nil
|
return &update, ResultSettled, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateLegacy is a callback for DB.UpdateInvoice that contains the invoice
|
// updateLegacy is a callback for DB.UpdateInvoice that contains the invoice
|
||||||
// settlement logic for legacy payments.
|
// settlement logic for legacy payments.
|
||||||
func updateLegacy(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
func updateLegacy(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
||||||
*channeldb.InvoiceUpdateDesc, updateResult, error) {
|
*channeldb.InvoiceUpdateDesc, ResolutionResult, error) {
|
||||||
|
|
||||||
// If the invoice is already canceled, there is no further
|
// If the invoice is already canceled, there is no further
|
||||||
// checking to do.
|
// checking to do.
|
||||||
if inv.State == channeldb.ContractCanceled {
|
if inv.State == channeldb.ContractCanceled {
|
||||||
return nil, resultInvoiceAlreadyCanceled, nil
|
return nil, ResultInvoiceAlreadyCanceled, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If an invoice amount is specified, check that enough is paid. Also
|
// If an invoice amount is specified, check that enough is paid. Also
|
||||||
@ -258,7 +313,7 @@ func updateLegacy(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
|||||||
// or accepted. In case this is a zero-valued invoice, it will always be
|
// or accepted. In case this is a zero-valued invoice, it will always be
|
||||||
// enough.
|
// enough.
|
||||||
if ctx.amtPaid < inv.Terms.Value {
|
if ctx.amtPaid < inv.Terms.Value {
|
||||||
return nil, resultAmountTooLow, nil
|
return nil, ResultAmountTooLow, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(joostjager): Check invoice mpp required feature
|
// TODO(joostjager): Check invoice mpp required feature
|
||||||
@ -271,17 +326,17 @@ func updateLegacy(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
|||||||
if htlc.State == channeldb.HtlcStateAccepted &&
|
if htlc.State == channeldb.HtlcStateAccepted &&
|
||||||
htlc.MppTotalAmt > 0 {
|
htlc.MppTotalAmt > 0 {
|
||||||
|
|
||||||
return nil, resultMppInProgress, nil
|
return nil, ResultMppInProgress, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The invoice is still open. Check the expiry.
|
// The invoice is still open. Check the expiry.
|
||||||
if ctx.expiry < uint32(ctx.currentHeight+ctx.finalCltvRejectDelta) {
|
if ctx.expiry < uint32(ctx.currentHeight+ctx.finalCltvRejectDelta) {
|
||||||
return nil, resultExpiryTooSoon, nil
|
return nil, ResultExpiryTooSoon, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.expiry < uint32(ctx.currentHeight+inv.Terms.FinalCltvDelta) {
|
if ctx.expiry < uint32(ctx.currentHeight+inv.Terms.FinalCltvDelta) {
|
||||||
return nil, resultExpiryTooSoon, nil
|
return nil, ResultExpiryTooSoon, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record HTLC in the invoice database.
|
// Record HTLC in the invoice database.
|
||||||
@ -302,10 +357,10 @@ func updateLegacy(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
|||||||
// We do accept or settle the HTLC.
|
// We do accept or settle the HTLC.
|
||||||
switch inv.State {
|
switch inv.State {
|
||||||
case channeldb.ContractAccepted:
|
case channeldb.ContractAccepted:
|
||||||
return &update, resultDuplicateToAccepted, nil
|
return &update, ResultDuplicateToAccepted, nil
|
||||||
|
|
||||||
case channeldb.ContractSettled:
|
case channeldb.ContractSettled:
|
||||||
return &update, resultDuplicateToSettled, nil
|
return &update, ResultDuplicateToSettled, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check to see if we can settle or this is an hold invoice and we need
|
// Check to see if we can settle or this is an hold invoice and we need
|
||||||
@ -315,7 +370,7 @@ func updateLegacy(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
|||||||
update.State = &channeldb.InvoiceStateUpdateDesc{
|
update.State = &channeldb.InvoiceStateUpdateDesc{
|
||||||
NewState: channeldb.ContractAccepted,
|
NewState: channeldb.ContractAccepted,
|
||||||
}
|
}
|
||||||
return &update, resultAccepted, nil
|
return &update, ResultAccepted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
update.State = &channeldb.InvoiceStateUpdateDesc{
|
update.State = &channeldb.InvoiceStateUpdateDesc{
|
||||||
@ -323,5 +378,5 @@ func updateLegacy(ctx *invoiceUpdateCtx, inv *channeldb.Invoice) (
|
|||||||
Preimage: inv.Terms.PaymentPreimage,
|
Preimage: inv.Terms.PaymentPreimage,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &update, resultSettled, nil
|
return &update, ResultSettled, nil
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
package lncfg
|
package lncfg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec"
|
"github.com/btcsuite/btcd/btcec"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
@ -274,8 +274,10 @@ func verifyPort(address string, defaultPort string) string {
|
|||||||
|
|
||||||
// ClientAddressDialer creates a gRPC dialer that can also dial unix socket
|
// ClientAddressDialer creates a gRPC dialer that can also dial unix socket
|
||||||
// addresses instead of just TCP addresses.
|
// addresses instead of just TCP addresses.
|
||||||
func ClientAddressDialer(defaultPort string) func(string, time.Duration) (net.Conn, error) {
|
func ClientAddressDialer(defaultPort string) func(context.Context,
|
||||||
return func(addr string, timeout time.Duration) (net.Conn, error) {
|
string) (net.Conn, error) {
|
||||||
|
|
||||||
|
return func(ctx context.Context, addr string) (net.Conn, error) {
|
||||||
parsedAddr, err := ParseAddressString(
|
parsedAddr, err := ParseAddressString(
|
||||||
addr, defaultPort, net.ResolveTCPAddr,
|
addr, defaultPort, net.ResolveTCPAddr,
|
||||||
)
|
)
|
||||||
@ -283,8 +285,9 @@ func ClientAddressDialer(defaultPort string) func(string, time.Duration) (net.Co
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return net.DialTimeout(
|
d := net.Dialer{}
|
||||||
parsedAddr.Network(), parsedAddr.String(), timeout,
|
return d.DialContext(
|
||||||
|
ctx, parsedAddr.Network(), parsedAddr.String(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,6 +109,7 @@ const (
|
|||||||
Failure_PERMANENT_NODE_FAILURE Failure_FailureCode = 20
|
Failure_PERMANENT_NODE_FAILURE Failure_FailureCode = 20
|
||||||
Failure_PERMANENT_CHANNEL_FAILURE Failure_FailureCode = 21
|
Failure_PERMANENT_CHANNEL_FAILURE Failure_FailureCode = 21
|
||||||
Failure_EXPIRY_TOO_FAR Failure_FailureCode = 22
|
Failure_EXPIRY_TOO_FAR Failure_FailureCode = 22
|
||||||
|
Failure_MPP_TIMEOUT Failure_FailureCode = 23
|
||||||
//*
|
//*
|
||||||
//The error source is known, but the failure itself couldn't be decoded.
|
//The error source is known, but the failure itself couldn't be decoded.
|
||||||
Failure_UNKNOWN_FAILURE Failure_FailureCode = 998
|
Failure_UNKNOWN_FAILURE Failure_FailureCode = 998
|
||||||
@ -142,6 +143,7 @@ var Failure_FailureCode_name = map[int32]string{
|
|||||||
20: "PERMANENT_NODE_FAILURE",
|
20: "PERMANENT_NODE_FAILURE",
|
||||||
21: "PERMANENT_CHANNEL_FAILURE",
|
21: "PERMANENT_CHANNEL_FAILURE",
|
||||||
22: "EXPIRY_TOO_FAR",
|
22: "EXPIRY_TOO_FAR",
|
||||||
|
23: "MPP_TIMEOUT",
|
||||||
998: "UNKNOWN_FAILURE",
|
998: "UNKNOWN_FAILURE",
|
||||||
999: "UNREADABLE_FAILURE",
|
999: "UNREADABLE_FAILURE",
|
||||||
}
|
}
|
||||||
@ -170,6 +172,7 @@ var Failure_FailureCode_value = map[string]int32{
|
|||||||
"PERMANENT_NODE_FAILURE": 20,
|
"PERMANENT_NODE_FAILURE": 20,
|
||||||
"PERMANENT_CHANNEL_FAILURE": 21,
|
"PERMANENT_CHANNEL_FAILURE": 21,
|
||||||
"EXPIRY_TOO_FAR": 22,
|
"EXPIRY_TOO_FAR": 22,
|
||||||
|
"MPP_TIMEOUT": 23,
|
||||||
"UNKNOWN_FAILURE": 998,
|
"UNKNOWN_FAILURE": 998,
|
||||||
"UNREADABLE_FAILURE": 999,
|
"UNREADABLE_FAILURE": 999,
|
||||||
}
|
}
|
||||||
@ -258,7 +261,9 @@ type SendPaymentRequest struct {
|
|||||||
//*
|
//*
|
||||||
//Features assumed to be supported by the final node. All transitive feature
|
//Features assumed to be supported by the final node. All transitive feature
|
||||||
//depdencies must also be set properly. For a given feature bit pair, either
|
//depdencies must also be set properly. For a given feature bit pair, either
|
||||||
//optional or remote may be set, but not both.
|
//optional or remote may be set, but not both. If this field is nil or empty,
|
||||||
|
//the router will try to load destination features from the graph as a
|
||||||
|
//fallback.
|
||||||
DestFeatures []lnrpc.FeatureBit `protobuf:"varint,16,rep,packed,name=dest_features,json=destFeatures,proto3,enum=lnrpc.FeatureBit" json:"dest_features,omitempty"`
|
DestFeatures []lnrpc.FeatureBit `protobuf:"varint,16,rep,packed,name=dest_features,json=destFeatures,proto3,enum=lnrpc.FeatureBit" json:"dest_features,omitempty"`
|
||||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
XXX_unrecognized []byte `json:"-"`
|
XXX_unrecognized []byte `json:"-"`
|
||||||
@ -1524,138 +1529,139 @@ func init() {
|
|||||||
func init() { proto.RegisterFile("routerrpc/router.proto", fileDescriptor_7a0613f69d37b0a5) }
|
func init() { proto.RegisterFile("routerrpc/router.proto", fileDescriptor_7a0613f69d37b0a5) }
|
||||||
|
|
||||||
var fileDescriptor_7a0613f69d37b0a5 = []byte{
|
var fileDescriptor_7a0613f69d37b0a5 = []byte{
|
||||||
// 2093 bytes of a gzipped FileDescriptorProto
|
// 2102 bytes of a gzipped FileDescriptorProto
|
||||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x58, 0x4f, 0x73, 0xdb, 0xb8,
|
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x58, 0xcb, 0x73, 0xdb, 0xc6,
|
||||||
0x15, 0x5f, 0x5a, 0xff, 0x9f, 0xfe, 0xd1, 0xb0, 0xe3, 0x30, 0x72, 0xbc, 0xf1, 0x32, 0x69, 0x56,
|
0x19, 0x0f, 0xc4, 0xf7, 0xc7, 0x17, 0xb4, 0x92, 0x65, 0x98, 0xb2, 0x62, 0x05, 0x76, 0x1d, 0x8e,
|
||||||
0x93, 0xc9, 0xda, 0xa9, 0xdb, 0xcd, 0x64, 0x7a, 0x68, 0x47, 0x96, 0xa8, 0x35, 0x1d, 0x89, 0x72,
|
0xc7, 0x91, 0x5c, 0xb5, 0xf1, 0x78, 0x7a, 0x68, 0x87, 0x22, 0xc1, 0x08, 0x32, 0x09, 0xca, 0x4b,
|
||||||
0x20, 0x29, 0xbb, 0xe9, 0x1e, 0x30, 0xb4, 0x04, 0x5b, 0x1c, 0x53, 0xa4, 0x96, 0x84, 0xb2, 0xf1,
|
0xd2, 0x89, 0x9b, 0xc3, 0x0e, 0x44, 0xae, 0x44, 0x8c, 0x40, 0x80, 0x01, 0x96, 0x8e, 0xf5, 0x0f,
|
||||||
0x77, 0xe8, 0xf7, 0x68, 0x0f, 0x6d, 0x2f, 0xfd, 0x4e, 0xed, 0xbd, 0x33, 0x3d, 0xf4, 0xd6, 0x01,
|
0xf4, 0xd4, 0xff, 0xa3, 0xbd, 0xb4, 0x97, 0x9e, 0xfb, 0xef, 0xb4, 0xf7, 0xde, 0x7a, 0xeb, 0xec,
|
||||||
0x40, 0x4a, 0x94, 0x2c, 0x67, 0x7b, 0xb2, 0xf0, 0x7b, 0x3f, 0xbc, 0x07, 0xe2, 0xe1, 0xfd, 0xf0,
|
0x2e, 0x40, 0x82, 0x14, 0xe5, 0xf4, 0x44, 0xec, 0xef, 0x7b, 0xed, 0xee, 0xb7, 0xdf, 0x8b, 0xb0,
|
||||||
0x60, 0xd8, 0x0b, 0xfc, 0x39, 0xa3, 0x41, 0x30, 0x1b, 0x1d, 0xcb, 0x5f, 0x47, 0xb3, 0xc0, 0x67,
|
0x17, 0xf8, 0x73, 0x46, 0x83, 0x60, 0x36, 0x3a, 0x96, 0x5f, 0x47, 0xb3, 0xc0, 0x67, 0x3e, 0x2a,
|
||||||
0x3e, 0x2a, 0x2c, 0xf0, 0x5a, 0x21, 0x98, 0x8d, 0x24, 0xaa, 0xff, 0x37, 0x03, 0xa8, 0x4f, 0xbd,
|
0x2c, 0xf0, 0x5a, 0x21, 0x98, 0x8d, 0x24, 0xaa, 0xff, 0x37, 0x03, 0xa8, 0x4f, 0xbd, 0xf1, 0x85,
|
||||||
0xf1, 0x85, 0x7d, 0x3b, 0xa5, 0x1e, 0xc3, 0xf4, 0xa7, 0x39, 0x0d, 0x19, 0x42, 0x90, 0x1e, 0xd3,
|
0x7d, 0x3b, 0xa5, 0x1e, 0xc3, 0xf4, 0xa7, 0x39, 0x0d, 0x19, 0x42, 0x90, 0x1e, 0xd3, 0x90, 0x69,
|
||||||
0x90, 0x69, 0xca, 0xa1, 0x52, 0x2f, 0x61, 0xf1, 0x1b, 0xa9, 0x90, 0xb2, 0xa7, 0x4c, 0xdb, 0x3a,
|
0xca, 0xa1, 0x52, 0x2f, 0x61, 0xf1, 0x8d, 0x54, 0x48, 0xd9, 0x53, 0xa6, 0x6d, 0x1d, 0x2a, 0xf5,
|
||||||
0x54, 0xea, 0x29, 0xcc, 0x7f, 0xa2, 0x47, 0x90, 0xb7, 0xa7, 0x8c, 0x4c, 0x43, 0x9b, 0x69, 0x25,
|
0x14, 0xe6, 0x9f, 0xe8, 0x11, 0xe4, 0xed, 0x29, 0x23, 0xd3, 0xd0, 0x66, 0x5a, 0x49, 0xc0, 0x39,
|
||||||
0x01, 0xe7, 0xec, 0x29, 0xeb, 0x86, 0x36, 0x43, 0x5f, 0x41, 0x69, 0x26, 0x5d, 0x92, 0x89, 0x1d,
|
0x7b, 0xca, 0xba, 0xa1, 0xcd, 0xd0, 0x57, 0x50, 0x9a, 0x49, 0x95, 0x64, 0x62, 0x87, 0x13, 0x2d,
|
||||||
0x4e, 0xb4, 0x94, 0x70, 0x54, 0x8c, 0xb0, 0x33, 0x3b, 0x9c, 0xa0, 0x3a, 0xa8, 0x57, 0x8e, 0x67,
|
0x25, 0x14, 0x15, 0x23, 0xec, 0xcc, 0x0e, 0x27, 0xa8, 0x0e, 0xea, 0x95, 0xe3, 0xd9, 0x2e, 0x19,
|
||||||
0xbb, 0x64, 0xe4, 0xb2, 0x8f, 0x64, 0x4c, 0x5d, 0x66, 0x6b, 0xe9, 0x43, 0xa5, 0x9e, 0xc1, 0x15,
|
0xb9, 0xec, 0x23, 0x19, 0x53, 0x97, 0xd9, 0x5a, 0xfa, 0x50, 0xa9, 0x67, 0x70, 0x45, 0xe0, 0x4d,
|
||||||
0x81, 0x37, 0x5d, 0xf6, 0xb1, 0xc5, 0x51, 0xf4, 0x35, 0x54, 0x63, 0x67, 0x81, 0x5c, 0xa0, 0x96,
|
0x97, 0x7d, 0x6c, 0x71, 0x14, 0x7d, 0x0d, 0xd5, 0x58, 0x59, 0x20, 0x37, 0xa8, 0x65, 0x0e, 0x95,
|
||||||
0x39, 0x54, 0xea, 0x05, 0x5c, 0x99, 0xad, 0x2e, 0xfb, 0x6b, 0xa8, 0x32, 0x67, 0x4a, 0xfd, 0x39,
|
0x7a, 0x01, 0x57, 0x66, 0xab, 0xdb, 0xfe, 0x1a, 0xaa, 0xcc, 0x99, 0x52, 0x7f, 0xce, 0x48, 0x48,
|
||||||
0x23, 0x21, 0x1d, 0xf9, 0xde, 0x38, 0xd4, 0xb2, 0xd2, 0x63, 0x04, 0xf7, 0x25, 0x8a, 0x74, 0x28,
|
0x47, 0xbe, 0x37, 0x0e, 0xb5, 0xac, 0xd4, 0x18, 0xc1, 0x7d, 0x89, 0x22, 0x1d, 0xca, 0x57, 0x94,
|
||||||
0x5f, 0x51, 0x4a, 0x5c, 0x67, 0xea, 0x30, 0xc2, 0x97, 0x9f, 0x13, 0xcb, 0x2f, 0x5e, 0x51, 0xda,
|
0x12, 0xd7, 0x99, 0x3a, 0x8c, 0xf0, 0xed, 0xe7, 0xc4, 0xf6, 0x8b, 0x57, 0x94, 0x76, 0x38, 0xd6,
|
||||||
0xe1, 0x58, 0xdf, 0x66, 0xe8, 0x19, 0x54, 0x96, 0x1c, 0xf1, 0x8d, 0x65, 0x41, 0x2a, 0xc5, 0x24,
|
0xb7, 0x19, 0x7a, 0x06, 0x95, 0x25, 0x8f, 0x38, 0x63, 0x59, 0x30, 0x95, 0x62, 0x26, 0x71, 0xd0,
|
||||||
0xf1, 0xa1, 0x2f, 0x41, 0xf5, 0xe7, 0xec, 0xda, 0x77, 0xbc, 0x6b, 0x32, 0x9a, 0xd8, 0x1e, 0x71,
|
0x97, 0xa0, 0xfa, 0x73, 0x76, 0xed, 0x3b, 0xde, 0x35, 0x19, 0x4d, 0x6c, 0x8f, 0x38, 0x63, 0x2d,
|
||||||
0xc6, 0x5a, 0xfe, 0x50, 0xa9, 0xa7, 0x4f, 0xb7, 0x5e, 0x29, 0xb8, 0x12, 0xdb, 0x9a, 0x13, 0xdb,
|
0x7f, 0xa8, 0xd4, 0xd3, 0xa7, 0x5b, 0xaf, 0x14, 0x5c, 0x89, 0x69, 0xcd, 0x89, 0xed, 0x99, 0x63,
|
||||||
0x33, 0xc7, 0xe8, 0x39, 0x54, 0x5d, 0x3b, 0x64, 0x64, 0xe2, 0xcf, 0xc8, 0x6c, 0x7e, 0x79, 0x43,
|
0xf4, 0x1c, 0xaa, 0xae, 0x1d, 0x32, 0x32, 0xf1, 0x67, 0x64, 0x36, 0xbf, 0xbc, 0xa1, 0xb7, 0x5a,
|
||||||
0x6f, 0xb5, 0x8a, 0xd8, 0x99, 0x32, 0x87, 0xcf, 0xfc, 0xd9, 0x85, 0x00, 0xd1, 0x01, 0x80, 0xd8,
|
0x45, 0xdc, 0x4c, 0x99, 0xc3, 0x67, 0xfe, 0xec, 0x42, 0x80, 0xe8, 0x00, 0x40, 0xdc, 0x8a, 0x30,
|
||||||
0x15, 0x11, 0x5c, 0x2b, 0x88, 0x6f, 0x28, 0x70, 0x44, 0x04, 0x46, 0x27, 0x50, 0x14, 0xd9, 0x24,
|
0xae, 0x15, 0xc4, 0x19, 0x0a, 0x1c, 0x11, 0x86, 0xd1, 0x09, 0x14, 0x85, 0x37, 0xc9, 0xc4, 0xf1,
|
||||||
0x13, 0xc7, 0x63, 0xa1, 0x06, 0x87, 0xa9, 0x7a, 0xf1, 0x44, 0x3d, 0x72, 0x3d, 0x9e, 0x58, 0xcc,
|
0x58, 0xa8, 0xc1, 0x61, 0xaa, 0x5e, 0x3c, 0x51, 0x8f, 0x5c, 0x8f, 0x3b, 0x16, 0x73, 0xca, 0x99,
|
||||||
0x2d, 0x67, 0x8e, 0xc7, 0x70, 0x92, 0x84, 0xc6, 0xb0, 0xc3, 0xd3, 0x48, 0x46, 0xf3, 0x90, 0xf9,
|
0xe3, 0x31, 0x9c, 0x64, 0x42, 0x63, 0xd8, 0xe1, 0x6e, 0x24, 0xa3, 0x79, 0xc8, 0xfc, 0x29, 0x09,
|
||||||
0x53, 0x12, 0xd0, 0x91, 0x1f, 0x8c, 0x43, 0xad, 0x28, 0xe6, 0xfe, 0xf6, 0x68, 0x71, 0x3a, 0x8e,
|
0xe8, 0xc8, 0x0f, 0xc6, 0xa1, 0x56, 0x14, 0xb2, 0xbf, 0x3d, 0x5a, 0xbc, 0x8e, 0xa3, 0xbb, 0xcf,
|
||||||
0xee, 0x1e, 0x87, 0xa3, 0x16, 0x0d, 0x59, 0x53, 0xcc, 0xc3, 0x72, 0x9a, 0xe1, 0xb1, 0xe0, 0x16,
|
0xe1, 0xa8, 0x45, 0x43, 0xd6, 0x14, 0x72, 0x58, 0x8a, 0x19, 0x1e, 0x0b, 0x6e, 0xf1, 0xf6, 0x78,
|
||||||
0x6f, 0x8f, 0xd7, 0x71, 0xf4, 0x12, 0x90, 0xed, 0xba, 0xfe, 0xcf, 0x24, 0xa4, 0xee, 0x15, 0x89,
|
0x1d, 0x47, 0x2f, 0x01, 0xd9, 0xae, 0xeb, 0xff, 0x4c, 0x42, 0xea, 0x5e, 0x91, 0xc8, 0x3d, 0x5a,
|
||||||
0xd2, 0xa3, 0x55, 0x0f, 0x95, 0x7a, 0x1e, 0xab, 0xc2, 0xd2, 0xa7, 0xee, 0x55, 0xe4, 0x1e, 0xbd,
|
0xf5, 0x50, 0xa9, 0xe7, 0xb1, 0x2a, 0x28, 0x7d, 0xea, 0x5e, 0x45, 0xea, 0xd1, 0x6b, 0x28, 0x8b,
|
||||||
0x86, 0xb2, 0x58, 0xd3, 0x15, 0xb5, 0xd9, 0x3c, 0xa0, 0xa1, 0xa6, 0x1e, 0xa6, 0xea, 0x95, 0x93,
|
0x3d, 0x5d, 0x51, 0x9b, 0xcd, 0x03, 0x1a, 0x6a, 0xea, 0x61, 0xaa, 0x5e, 0x39, 0xd9, 0x8e, 0x4e,
|
||||||
0xed, 0xe8, 0x4b, 0xda, 0x12, 0x3e, 0x75, 0x18, 0x2e, 0x71, 0x5e, 0x34, 0x0e, 0x6b, 0x2d, 0xd8,
|
0xd2, 0x96, 0xf0, 0xa9, 0xc3, 0x70, 0x89, 0xf3, 0x45, 0xeb, 0xb0, 0xd6, 0x82, 0xbd, 0xcd, 0x5b,
|
||||||
0xdb, 0xbc, 0x24, 0x7e, 0x48, 0xf9, 0xa6, 0xf2, 0x73, 0x9b, 0xc6, 0xfc, 0x27, 0xda, 0x85, 0xcc,
|
0xe2, 0x8f, 0x94, 0x5f, 0x2a, 0x7f, 0xb7, 0x69, 0xcc, 0x3f, 0xd1, 0x2e, 0x64, 0x3e, 0xda, 0xee,
|
||||||
0x47, 0xdb, 0x9d, 0x53, 0x71, 0x70, 0x4b, 0x58, 0x0e, 0x7e, 0xb7, 0xf5, 0x46, 0xd1, 0xdf, 0xc0,
|
0x9c, 0x8a, 0x87, 0x5b, 0xc2, 0x72, 0xf1, 0xbb, 0xad, 0x37, 0x8a, 0xfe, 0x06, 0x76, 0x06, 0x81,
|
||||||
0xce, 0x20, 0xb0, 0x47, 0x37, 0x6b, 0x67, 0x7f, 0xfd, 0xe8, 0x2a, 0x77, 0x8e, 0xae, 0xfe, 0x17,
|
0x3d, 0xba, 0x59, 0x7b, 0xfb, 0xeb, 0x4f, 0x57, 0xb9, 0xf3, 0x74, 0xf5, 0xbf, 0x2a, 0x50, 0x8e,
|
||||||
0x05, 0xca, 0xd1, 0xac, 0x3e, 0xb3, 0xd9, 0x3c, 0x44, 0xdf, 0x40, 0x26, 0x64, 0x36, 0xa3, 0x82,
|
0xa4, 0xfa, 0xcc, 0x66, 0xf3, 0x10, 0x7d, 0x03, 0x99, 0x90, 0xd9, 0x8c, 0x0a, 0xee, 0xca, 0xc9,
|
||||||
0x5d, 0x39, 0x79, 0x98, 0xd8, 0xcf, 0x04, 0x91, 0x62, 0xc9, 0x42, 0x35, 0xc8, 0xcf, 0x02, 0xea,
|
0xc3, 0xc4, 0x7d, 0x26, 0x18, 0x29, 0x96, 0x5c, 0xa8, 0x06, 0xf9, 0x59, 0x40, 0x9d, 0xa9, 0x7d,
|
||||||
0x4c, 0xed, 0xeb, 0x78, 0x5d, 0x8b, 0x31, 0xd2, 0x21, 0x23, 0x26, 0x8b, 0x9a, 0x29, 0x9e, 0x94,
|
0x1d, 0xef, 0x6b, 0xb1, 0x46, 0x3a, 0x64, 0x84, 0xb0, 0x88, 0x99, 0xe2, 0x49, 0x29, 0xe9, 0x56,
|
||||||
0x92, 0x69, 0xc5, 0xd2, 0x84, 0xea, 0x90, 0x99, 0x30, 0x77, 0x14, 0x6a, 0x69, 0x91, 0x3e, 0x14,
|
0x2c, 0x49, 0xa8, 0x0e, 0x99, 0x09, 0x73, 0x47, 0xa1, 0x96, 0x16, 0xee, 0x43, 0x11, 0xcf, 0xd9,
|
||||||
0x71, 0xce, 0x06, 0x9d, 0x66, 0x83, 0x31, 0x3a, 0x9d, 0x31, 0x2c, 0x09, 0xfa, 0xef, 0xa1, 0x2a,
|
0xa0, 0xd3, 0x6c, 0x30, 0x46, 0xa7, 0x33, 0x86, 0x25, 0x83, 0xfe, 0x7b, 0xa8, 0x0a, 0xc9, 0x36,
|
||||||
0x66, 0xb6, 0x29, 0xfd, 0x5c, 0x71, 0x3f, 0x04, 0x5e, 0xba, 0xa2, 0x14, 0x64, 0x81, 0x67, 0xed,
|
0xa5, 0x9f, 0x0b, 0xee, 0x87, 0xc0, 0x43, 0x57, 0x84, 0x82, 0x0c, 0xf0, 0xac, 0x3d, 0xe5, 0x51,
|
||||||
0x29, 0xaf, 0x02, 0x7d, 0x0c, 0xea, 0x72, 0x7e, 0x38, 0xf3, 0xbd, 0x90, 0x47, 0x57, 0xf9, 0x32,
|
0xa0, 0x8f, 0x41, 0x5d, 0xca, 0x87, 0x33, 0xdf, 0x0b, 0xb9, 0x75, 0x95, 0x6f, 0x83, 0x3f, 0x79,
|
||||||
0xf8, 0x91, 0xe7, 0x15, 0x22, 0x6a, 0x43, 0x11, 0xb3, 0x2a, 0x11, 0xde, 0xa6, 0x54, 0x54, 0xc7,
|
0x1e, 0x21, 0x22, 0x36, 0x14, 0x21, 0x55, 0x89, 0xf0, 0x36, 0xa5, 0x22, 0x3a, 0x9e, 0xcb, 0x80,
|
||||||
0x73, 0x59, 0x90, 0xc4, 0xf5, 0x47, 0x37, 0xbc, 0xc4, 0xed, 0xdb, 0xc8, 0x7d, 0x99, 0xc3, 0x1d,
|
0x24, 0xae, 0x3f, 0xba, 0xe1, 0x21, 0x6e, 0xdf, 0x46, 0xea, 0xcb, 0x1c, 0xee, 0xf8, 0xa3, 0x9b,
|
||||||
0x7f, 0x74, 0xd3, 0xe2, 0xa0, 0xfe, 0xa3, 0x54, 0xa1, 0x81, 0x2f, 0xbf, 0xf2, 0xff, 0xce, 0xc4,
|
0x16, 0x07, 0xf5, 0x1f, 0x65, 0x16, 0x1a, 0xf8, 0xf2, 0x94, 0xff, 0xb7, 0x27, 0x96, 0x97, 0xb5,
|
||||||
0x72, 0xb3, 0xb6, 0xee, 0xdd, 0x2c, 0x9d, 0xc0, 0xce, 0x8a, 0xf3, 0xe8, 0x2b, 0x92, 0x39, 0x50,
|
0x75, 0xef, 0x65, 0xe9, 0x04, 0x76, 0x56, 0x94, 0x47, 0xa7, 0x48, 0xfa, 0x40, 0x59, 0xf3, 0xc1,
|
||||||
0xd6, 0x72, 0xf0, 0x12, 0x72, 0x57, 0xb6, 0xe3, 0xce, 0x83, 0xd8, 0x31, 0x4a, 0x24, 0xb4, 0x2d,
|
0x4b, 0xc8, 0x5d, 0xd9, 0x8e, 0x3b, 0x0f, 0x62, 0xc5, 0x28, 0xe1, 0xd0, 0xb6, 0xa4, 0xe0, 0x98,
|
||||||
0x2d, 0x38, 0xa6, 0xe8, 0xff, 0xc9, 0x41, 0x2e, 0x02, 0xd1, 0x09, 0xa4, 0x47, 0xfe, 0x38, 0x3e,
|
0x45, 0xff, 0x53, 0x1e, 0x72, 0x11, 0x88, 0x4e, 0x20, 0x3d, 0xf2, 0xc7, 0xf1, 0x3b, 0xf8, 0xf2,
|
||||||
0x07, 0x5f, 0xde, 0x9d, 0x16, 0xff, 0x6d, 0xfa, 0x63, 0x8a, 0x05, 0x17, 0xfd, 0x01, 0x2a, 0x5c,
|
0xae, 0x58, 0xfc, 0xdb, 0xf4, 0xc7, 0x14, 0x0b, 0x5e, 0xf4, 0x07, 0xa8, 0xf0, 0xd4, 0xe1, 0x51,
|
||||||
0x3a, 0x3c, 0xea, 0x92, 0xf9, 0x6c, 0x6c, 0x2f, 0x52, 0xaf, 0x25, 0x66, 0x37, 0x25, 0x61, 0x28,
|
0x97, 0xcc, 0x67, 0x63, 0x7b, 0xe1, 0x7a, 0x2d, 0x21, 0xdd, 0x94, 0x0c, 0x43, 0x41, 0xc7, 0xe5,
|
||||||
0xec, 0xb8, 0x3c, 0x4a, 0x0e, 0xd1, 0x3e, 0x14, 0x78, 0xb6, 0x65, 0x26, 0xd2, 0xe2, 0xec, 0xe7,
|
0x51, 0x72, 0x89, 0xf6, 0xa1, 0xc0, 0xbd, 0x2d, 0x3d, 0x91, 0x16, 0x6f, 0x3f, 0xcf, 0x01, 0xe1,
|
||||||
0x39, 0x20, 0x72, 0xa0, 0x43, 0xd9, 0xf7, 0x1c, 0xdf, 0x23, 0xe1, 0xc4, 0x26, 0x27, 0xdf, 0xbe,
|
0x03, 0x1d, 0xca, 0xbe, 0xe7, 0xf8, 0x1e, 0x09, 0x27, 0x36, 0x39, 0xf9, 0xf6, 0xb5, 0xc8, 0x9d,
|
||||||
0x16, 0xda, 0x59, 0xc2, 0x45, 0x01, 0xf6, 0x27, 0xf6, 0xc9, 0xb7, 0xaf, 0xd1, 0x13, 0x28, 0x0a,
|
0x25, 0x5c, 0x14, 0x60, 0x7f, 0x62, 0x9f, 0x7c, 0xfb, 0x1a, 0x3d, 0x81, 0xa2, 0xc8, 0x37, 0xf4,
|
||||||
0xbd, 0xa1, 0x9f, 0x66, 0x4e, 0x70, 0x2b, 0x44, 0xb3, 0x8c, 0x85, 0x04, 0x19, 0x02, 0xe1, 0x55,
|
0xd3, 0xcc, 0x09, 0x6e, 0x45, 0xd2, 0x2c, 0x63, 0x91, 0x82, 0x0c, 0x81, 0xf0, 0x28, 0xba, 0x72,
|
||||||
0x74, 0xe5, 0xda, 0xd7, 0xa1, 0x10, 0xca, 0x32, 0x96, 0x03, 0xf4, 0x0a, 0x76, 0xa3, 0x3d, 0x20,
|
0xed, 0xeb, 0x50, 0x24, 0xca, 0x32, 0x96, 0x0b, 0xf4, 0x0a, 0x76, 0xa3, 0x3b, 0x20, 0xa1, 0x3f,
|
||||||
0xa1, 0x3f, 0x0f, 0x46, 0x94, 0x38, 0xde, 0x98, 0x7e, 0x12, 0x02, 0x58, 0xc6, 0x28, 0xb2, 0xf5,
|
0x0f, 0x46, 0x94, 0x38, 0xde, 0x98, 0x7e, 0x12, 0x09, 0xb0, 0x8c, 0x51, 0x44, 0xeb, 0x0b, 0x92,
|
||||||
0x85, 0xc9, 0xe4, 0x16, 0xb4, 0x07, 0xd9, 0x09, 0x75, 0xae, 0x27, 0x52, 0xd4, 0xca, 0x38, 0x1a,
|
0xc9, 0x29, 0x68, 0x0f, 0xb2, 0x13, 0xea, 0x5c, 0x4f, 0x64, 0x52, 0x2b, 0xe3, 0x68, 0xa5, 0xff,
|
||||||
0xe9, 0x7f, 0xcd, 0x40, 0x31, 0xb1, 0x31, 0xa8, 0x04, 0x79, 0x6c, 0xf4, 0x0d, 0xfc, 0xde, 0x68,
|
0x33, 0x03, 0xc5, 0xc4, 0xc5, 0xa0, 0x12, 0xe4, 0xb1, 0xd1, 0x37, 0xf0, 0x7b, 0xa3, 0xa5, 0x7e,
|
||||||
0xa9, 0x5f, 0xa0, 0x3a, 0x3c, 0x33, 0xad, 0x66, 0x0f, 0x63, 0xa3, 0x39, 0x20, 0x3d, 0x4c, 0x86,
|
0x81, 0xea, 0xf0, 0xcc, 0xb4, 0x9a, 0x3d, 0x8c, 0x8d, 0xe6, 0x80, 0xf4, 0x30, 0x19, 0x5a, 0x6f,
|
||||||
0xd6, 0x5b, 0xab, 0xf7, 0xbd, 0x45, 0x2e, 0x1a, 0x1f, 0xba, 0x86, 0x35, 0x20, 0x2d, 0x63, 0xd0,
|
0xad, 0xde, 0xf7, 0x16, 0xb9, 0x68, 0x7c, 0xe8, 0x1a, 0xd6, 0x80, 0xb4, 0x8c, 0x41, 0xc3, 0xec,
|
||||||
0x30, 0x3b, 0x7d, 0x55, 0x41, 0x8f, 0x41, 0x5b, 0x32, 0x63, 0x73, 0xa3, 0xdb, 0x1b, 0x5a, 0x03,
|
0xf4, 0x55, 0x05, 0x3d, 0x06, 0x6d, 0xc9, 0x19, 0x93, 0x1b, 0xdd, 0xde, 0xd0, 0x1a, 0xa8, 0x5b,
|
||||||
0x75, 0x0b, 0x3d, 0x81, 0xfd, 0xb6, 0x69, 0x35, 0x3a, 0x64, 0xc9, 0x69, 0x76, 0x06, 0xef, 0x89,
|
0xe8, 0x09, 0xec, 0xb7, 0x4d, 0xab, 0xd1, 0x21, 0x4b, 0x9e, 0x66, 0x67, 0xf0, 0x9e, 0x18, 0x3f,
|
||||||
0xf1, 0xc3, 0x85, 0x89, 0x3f, 0xa8, 0xa9, 0x4d, 0x04, 0x5e, 0x53, 0xb1, 0x87, 0x34, 0x7a, 0x04,
|
0x5c, 0x98, 0xf8, 0x83, 0x9a, 0xda, 0xc4, 0xc0, 0x63, 0x2a, 0xd6, 0x90, 0x46, 0x8f, 0xe0, 0x81,
|
||||||
0x0f, 0x24, 0x41, 0x4e, 0x21, 0x83, 0x5e, 0x8f, 0xf4, 0x7b, 0x3d, 0x4b, 0xcd, 0xa0, 0x6d, 0x28,
|
0x64, 0x90, 0x22, 0x64, 0xd0, 0xeb, 0x91, 0x7e, 0xaf, 0x67, 0xa9, 0x19, 0xb4, 0x0d, 0x65, 0xd3,
|
||||||
0x9b, 0xd6, 0xfb, 0x46, 0xc7, 0x6c, 0x11, 0x6c, 0x34, 0x3a, 0x5d, 0x35, 0x8b, 0x76, 0xa0, 0xba,
|
0x7a, 0xdf, 0xe8, 0x98, 0x2d, 0x82, 0x8d, 0x46, 0xa7, 0xab, 0x66, 0xd1, 0x0e, 0x54, 0xd7, 0xf9,
|
||||||
0xce, 0xcb, 0x71, 0x17, 0x31, 0xaf, 0x67, 0x99, 0x3d, 0x8b, 0xbc, 0x37, 0x70, 0xdf, 0xec, 0x59,
|
0x72, 0x5c, 0x45, 0xcc, 0xd7, 0xb3, 0xcc, 0x9e, 0x45, 0xde, 0x1b, 0xb8, 0x6f, 0xf6, 0x2c, 0x35,
|
||||||
0x6a, 0x1e, 0xed, 0x01, 0x5a, 0x35, 0x9d, 0x75, 0x1b, 0x4d, 0xb5, 0x80, 0x1e, 0xc0, 0xf6, 0x2a,
|
0x8f, 0xf6, 0x00, 0xad, 0x92, 0xce, 0xba, 0x8d, 0xa6, 0x5a, 0x40, 0x0f, 0x60, 0x7b, 0x15, 0x7f,
|
||||||
0xfe, 0xd6, 0xf8, 0xa0, 0x02, 0xd2, 0x60, 0x57, 0x2e, 0x8c, 0x9c, 0x1a, 0x9d, 0xde, 0xf7, 0xa4,
|
0x6b, 0x7c, 0x50, 0x01, 0x69, 0xb0, 0x2b, 0x37, 0x46, 0x4e, 0x8d, 0x4e, 0xef, 0x7b, 0xd2, 0x35,
|
||||||
0x6b, 0x5a, 0x66, 0x77, 0xd8, 0x55, 0x8b, 0x68, 0x17, 0xd4, 0xb6, 0x61, 0x10, 0xd3, 0xea, 0x0f,
|
0x2d, 0xb3, 0x3b, 0xec, 0xaa, 0x45, 0xb4, 0x0b, 0x6a, 0xdb, 0x30, 0x88, 0x69, 0xf5, 0x87, 0xed,
|
||||||
0xdb, 0x6d, 0xb3, 0x69, 0x1a, 0xd6, 0x40, 0x2d, 0xc9, 0xc8, 0x9b, 0x3e, 0xbc, 0xcc, 0x27, 0x34,
|
0xb6, 0xd9, 0x34, 0x0d, 0x6b, 0xa0, 0x96, 0xa4, 0xe5, 0x4d, 0x07, 0x2f, 0x73, 0x81, 0xe6, 0x59,
|
||||||
0xcf, 0x1a, 0x96, 0x65, 0x74, 0x48, 0xcb, 0xec, 0x37, 0x4e, 0x3b, 0x46, 0x4b, 0xad, 0xa0, 0x03,
|
0xc3, 0xb2, 0x8c, 0x0e, 0x69, 0x99, 0xfd, 0xc6, 0x69, 0xc7, 0x68, 0xa9, 0x15, 0x74, 0x00, 0x8f,
|
||||||
0x78, 0x34, 0x30, 0xba, 0x17, 0x3d, 0xdc, 0xc0, 0x1f, 0x48, 0x6c, 0x6f, 0x37, 0xcc, 0xce, 0x10,
|
0x06, 0x46, 0xf7, 0xa2, 0x87, 0x1b, 0xf8, 0x03, 0x89, 0xe9, 0xed, 0x86, 0xd9, 0x19, 0x62, 0x43,
|
||||||
0x1b, 0x6a, 0x15, 0x7d, 0x05, 0x07, 0xd8, 0x78, 0x37, 0x34, 0xb1, 0xd1, 0x22, 0x56, 0xaf, 0x65,
|
0xad, 0xa2, 0xaf, 0xe0, 0x00, 0x1b, 0xef, 0x86, 0x26, 0x36, 0x5a, 0xc4, 0xea, 0xb5, 0x0c, 0xd2,
|
||||||
0x90, 0xb6, 0xd1, 0x18, 0x0c, 0xb1, 0x41, 0xba, 0x66, 0xbf, 0x6f, 0x5a, 0xdf, 0xa9, 0x2a, 0x7a,
|
0x36, 0x1a, 0x83, 0x21, 0x36, 0x48, 0xd7, 0xec, 0xf7, 0x4d, 0xeb, 0x3b, 0x55, 0x45, 0xcf, 0xe0,
|
||||||
0x06, 0x87, 0x0b, 0xca, 0xc2, 0xc1, 0x1a, 0x6b, 0x9b, 0x7f, 0x5f, 0x9c, 0x52, 0xcb, 0xf8, 0x61,
|
0x70, 0xc1, 0xb2, 0x50, 0xb0, 0xc6, 0xb5, 0xcd, 0xcf, 0x17, 0xbb, 0xd4, 0x32, 0x7e, 0x18, 0x90,
|
||||||
0x40, 0x2e, 0x0c, 0x03, 0xab, 0x08, 0xd5, 0x60, 0x6f, 0x19, 0x5e, 0x06, 0x88, 0x62, 0xef, 0x70,
|
0x0b, 0xc3, 0xc0, 0x2a, 0x42, 0x35, 0xd8, 0x5b, 0x9a, 0x97, 0x06, 0x22, 0xdb, 0x3b, 0x9c, 0x76,
|
||||||
0xdb, 0x85, 0x81, 0xbb, 0x0d, 0x8b, 0x27, 0x78, 0xc5, 0xb6, 0xcb, 0x97, 0xbd, 0xb4, 0xad, 0x2f,
|
0x61, 0xe0, 0x6e, 0xc3, 0xe2, 0x0e, 0x5e, 0xa1, 0xed, 0xf2, 0x6d, 0x2f, 0x69, 0xeb, 0xdb, 0x7e,
|
||||||
0xfb, 0x01, 0x42, 0x50, 0x49, 0x64, 0xa5, 0xdd, 0xc0, 0xea, 0x1e, 0xda, 0x85, 0x6a, 0xbc, 0x82,
|
0x80, 0x10, 0x54, 0x12, 0x5e, 0x69, 0x37, 0xb0, 0xba, 0x87, 0xaa, 0x50, 0xec, 0x5e, 0x5c, 0x90,
|
||||||
0x98, 0xf8, 0xcf, 0x1c, 0x7a, 0x08, 0x68, 0x68, 0x61, 0xa3, 0xd1, 0xe2, 0x1b, 0xb2, 0x30, 0xfc,
|
0x81, 0xd9, 0x35, 0x7a, 0xc3, 0x81, 0xfa, 0x10, 0xed, 0x42, 0x35, 0xde, 0x52, 0x2c, 0xf9, 0xaf,
|
||||||
0x2b, 0x77, 0x9e, 0xce, 0x6f, 0xa9, 0x29, 0xfd, 0x1f, 0x29, 0x28, 0xaf, 0xd4, 0x25, 0x7a, 0x0c,
|
0x1c, 0x7a, 0x08, 0x68, 0x68, 0x61, 0xa3, 0xd1, 0xe2, 0x37, 0xb4, 0x20, 0xfc, 0x3b, 0x77, 0x9e,
|
||||||
0x85, 0xd0, 0xb9, 0xf6, 0xc4, 0x35, 0x15, 0x89, 0xca, 0x12, 0x10, 0xb7, 0xfa, 0xc4, 0x76, 0x3c,
|
0xce, 0x6f, 0xa9, 0x29, 0xfd, 0x1f, 0x29, 0x28, 0xaf, 0x04, 0x2a, 0x7a, 0x0c, 0x85, 0xd0, 0xb9,
|
||||||
0xa9, 0x66, 0x52, 0xf7, 0x0b, 0x02, 0x11, 0x5a, 0xb6, 0x0f, 0xb9, 0xb8, 0x83, 0x48, 0x2d, 0x3a,
|
0xf6, 0x44, 0xdd, 0x8a, 0xb2, 0xcc, 0x12, 0x10, 0x65, 0x7e, 0x62, 0x3b, 0x9e, 0x4c, 0x6f, 0xb2,
|
||||||
0x88, 0xec, 0x48, 0x76, 0x0e, 0x8f, 0xa1, 0xc0, 0x25, 0x33, 0x64, 0xf6, 0x74, 0x26, 0x4a, 0xbc,
|
0x10, 0x14, 0x04, 0x22, 0x92, 0xdb, 0x3e, 0xe4, 0xe2, 0x96, 0x22, 0xb5, 0x68, 0x29, 0xb2, 0x23,
|
||||||
0x8c, 0x97, 0x00, 0x7a, 0x0a, 0xe5, 0x29, 0x0d, 0x43, 0xfb, 0x9a, 0x12, 0x59, 0xa6, 0x20, 0x18,
|
0xd9, 0x4a, 0x3c, 0x86, 0x02, 0xcf, 0xa1, 0x21, 0xb3, 0xa7, 0x33, 0x11, 0xf3, 0x65, 0xbc, 0x04,
|
||||||
0xa5, 0x08, 0x6c, 0x8b, 0x6a, 0x7d, 0x0a, 0xb1, 0x6c, 0x44, 0xa4, 0x8c, 0x24, 0x45, 0xa0, 0x24,
|
0xd0, 0x53, 0x28, 0x4f, 0x69, 0x18, 0xda, 0xd7, 0x94, 0xc8, 0xb8, 0x05, 0xc1, 0x51, 0x8a, 0xc0,
|
||||||
0xad, 0x2b, 0x36, 0xb3, 0x23, 0x35, 0x48, 0x2a, 0x36, 0xb3, 0xd1, 0x0b, 0xd8, 0x96, 0x92, 0xe3,
|
0xb6, 0x08, 0xdf, 0xa7, 0x10, 0xe7, 0x91, 0x88, 0x29, 0x23, 0x99, 0x22, 0x50, 0x32, 0xad, 0xa7,
|
||||||
0x78, 0xce, 0x74, 0x3e, 0x95, 0xd2, 0x93, 0x13, 0xd2, 0x53, 0x15, 0xd2, 0x23, 0x71, 0xa1, 0x40,
|
0x70, 0x66, 0x47, 0xe9, 0x21, 0x99, 0xc2, 0x99, 0x8d, 0x5e, 0xc0, 0xb6, 0xcc, 0x41, 0x8e, 0xe7,
|
||||||
0x8f, 0x20, 0x7f, 0x69, 0x87, 0x94, 0x5f, 0x16, 0x91, 0x34, 0xe4, 0xf8, 0xb8, 0x4d, 0x29, 0x37,
|
0x4c, 0xe7, 0x53, 0x99, 0x8b, 0x72, 0x22, 0x17, 0x55, 0x45, 0x2e, 0x92, 0xb8, 0x48, 0x49, 0x8f,
|
||||||
0xf1, 0x2b, 0x24, 0xe0, 0xa2, 0x27, 0x15, 0x21, 0x77, 0x45, 0x29, 0xe6, 0x7b, 0xb9, 0x88, 0x60,
|
0x20, 0x7f, 0x69, 0x87, 0x94, 0x57, 0x8f, 0x28, 0x57, 0xe4, 0xf8, 0xba, 0x4d, 0x29, 0x27, 0xf1,
|
||||||
0x7f, 0x5a, 0x46, 0x28, 0x26, 0x22, 0x48, 0x5c, 0x44, 0x78, 0x01, 0xdb, 0xf4, 0x13, 0x0b, 0x6c,
|
0x9a, 0x12, 0xf0, 0x2c, 0x28, 0x53, 0x44, 0xee, 0x8a, 0x52, 0xcc, 0xef, 0x72, 0x61, 0xc1, 0xfe,
|
||||||
0xe2, 0xcf, 0xec, 0x9f, 0xe6, 0x94, 0x8c, 0x6d, 0x66, 0x8b, 0x96, 0xb4, 0x84, 0xab, 0xc2, 0xd0,
|
0xb4, 0xb4, 0x50, 0x4c, 0x58, 0x90, 0xb8, 0xb0, 0xf0, 0x02, 0xb6, 0xe9, 0x27, 0x16, 0xd8, 0xc4,
|
||||||
0x13, 0x78, 0xcb, 0x66, 0xb6, 0xfe, 0x18, 0x6a, 0x98, 0x86, 0x94, 0x75, 0x9d, 0x30, 0x74, 0x7c,
|
0x9f, 0xd9, 0x3f, 0xcd, 0x29, 0x19, 0xdb, 0xcc, 0x16, 0x3d, 0x6a, 0x09, 0x57, 0x05, 0xa1, 0x27,
|
||||||
0xaf, 0xe9, 0x7b, 0x2c, 0xf0, 0xdd, 0xe8, 0xce, 0xd1, 0x0f, 0x60, 0x7f, 0xa3, 0x55, 0x5e, 0x1a,
|
0xf0, 0x96, 0xcd, 0x6c, 0xfd, 0x31, 0xd4, 0x30, 0x0d, 0x29, 0xeb, 0x3a, 0x61, 0xe8, 0xf8, 0x5e,
|
||||||
0x7c, 0xf2, 0xbb, 0x39, 0x0d, 0x6e, 0x37, 0x4f, 0x7e, 0x07, 0xfb, 0x1b, 0xad, 0xd1, 0x8d, 0xf3,
|
0xd3, 0xf7, 0x58, 0xe0, 0xbb, 0x51, 0x11, 0xd2, 0x0f, 0x60, 0x7f, 0x23, 0x55, 0x56, 0x11, 0x2e,
|
||||||
0x12, 0x32, 0x33, 0xdb, 0x09, 0x42, 0x6d, 0x4b, 0xdc, 0xda, 0x7b, 0x2b, 0x4d, 0x82, 0x13, 0x9c,
|
0xfc, 0x6e, 0x4e, 0x83, 0xdb, 0xcd, 0xc2, 0xef, 0x60, 0x7f, 0x23, 0x35, 0x2a, 0x41, 0x2f, 0x21,
|
||||||
0x39, 0x21, 0xf3, 0x83, 0x5b, 0x2c, 0x49, 0xe7, 0xe9, 0xbc, 0xa2, 0x6e, 0xe9, 0x7f, 0x52, 0xa0,
|
0x33, 0xb3, 0x9d, 0x20, 0xd4, 0xb6, 0x44, 0x19, 0xdf, 0x5b, 0xe9, 0x1a, 0x9c, 0xe0, 0xcc, 0x09,
|
||||||
0x98, 0x30, 0xf2, 0x73, 0xe0, 0xf9, 0x63, 0x4a, 0xae, 0x02, 0x7f, 0x1a, 0x9f, 0xb0, 0x05, 0x80,
|
0x99, 0x1f, 0xdc, 0x62, 0xc9, 0x74, 0x9e, 0xce, 0x2b, 0xea, 0x96, 0xfe, 0x67, 0x05, 0x8a, 0x09,
|
||||||
0x34, 0xc8, 0x89, 0x01, 0xf3, 0xa3, 0xe3, 0x15, 0x0f, 0xd1, 0x37, 0x90, 0x9b, 0x48, 0x17, 0x22,
|
0x22, 0x7f, 0x07, 0x9e, 0x3f, 0xa6, 0xe4, 0x2a, 0xf0, 0xa7, 0xf1, 0x0b, 0x5b, 0x00, 0x48, 0x83,
|
||||||
0x4b, 0xc5, 0x93, 0x9d, 0xb5, 0xe8, 0x7c, 0x6f, 0x70, 0xcc, 0x39, 0x4f, 0xe7, 0x53, 0x6a, 0xfa,
|
0x9c, 0x58, 0x30, 0x3f, 0x7a, 0x5e, 0xf1, 0x12, 0x7d, 0x03, 0xb9, 0x89, 0x54, 0x21, 0xbc, 0x54,
|
||||||
0x3c, 0x9d, 0x4f, 0xab, 0x99, 0xf3, 0x74, 0x3e, 0xa3, 0x66, 0xcf, 0xd3, 0xf9, 0xac, 0x9a, 0xd3,
|
0x3c, 0xd9, 0x59, 0xb3, 0xce, 0xef, 0x06, 0xc7, 0x3c, 0xe7, 0xe9, 0x7c, 0x4a, 0x4d, 0x9f, 0xa7,
|
||||||
0xff, 0xad, 0x40, 0x3e, 0x66, 0xf3, 0xb5, 0x70, 0x89, 0x27, 0xfc, 0x64, 0x44, 0x0d, 0xc0, 0x12,
|
0xf3, 0x69, 0x35, 0x73, 0x9e, 0xce, 0x67, 0xd4, 0xec, 0x79, 0x3a, 0x9f, 0x55, 0x73, 0xfa, 0x7f,
|
||||||
0x40, 0x3a, 0x94, 0xc4, 0x60, 0xb5, 0xaf, 0x58, 0xc1, 0xd0, 0x33, 0x28, 0x2f, 0xc6, 0x8b, 0xcb,
|
0x14, 0xc8, 0xc7, 0xdc, 0x7c, 0x2f, 0x3c, 0xe7, 0x13, 0xfe, 0x32, 0xa2, 0x8e, 0x60, 0x09, 0x20,
|
||||||
0x2b, 0x85, 0x57, 0x41, 0xee, 0x29, 0x9c, 0x8f, 0x46, 0x34, 0x0c, 0x65, 0xa8, 0x8c, 0xf4, 0x94,
|
0x1d, 0x4a, 0x62, 0xb1, 0xda, 0x68, 0xac, 0x60, 0xe8, 0x19, 0x94, 0x17, 0xeb, 0x45, 0x35, 0x4b,
|
||||||
0xc4, 0x50, 0x1d, 0xaa, 0xf1, 0x38, 0x0e, 0x98, 0x15, 0xb4, 0x75, 0x18, 0xbd, 0x00, 0x35, 0x09,
|
0xe1, 0x55, 0x90, 0x6b, 0x0a, 0xe7, 0xa3, 0x11, 0x0d, 0x43, 0x69, 0x2a, 0x23, 0x35, 0x25, 0x31,
|
||||||
0x4d, 0x97, 0xed, 0xff, 0x1d, 0x5c, 0x6e, 0x83, 0x3e, 0x85, 0x87, 0x22, 0xad, 0x17, 0x81, 0x7f,
|
0x54, 0x87, 0x6a, 0xbc, 0x8e, 0x0d, 0x66, 0x05, 0xdb, 0x3a, 0x8c, 0x5e, 0x80, 0x9a, 0x84, 0xa6,
|
||||||
0x69, 0x5f, 0x3a, 0xae, 0xc3, 0x6e, 0xe3, 0x16, 0x85, 0x6f, 0x41, 0xe0, 0x4f, 0x89, 0x17, 0xdf,
|
0xcb, 0x79, 0xe0, 0x0e, 0x2e, 0xaf, 0x41, 0x9f, 0xc2, 0x43, 0xe1, 0xd6, 0x8b, 0xc0, 0xbf, 0xb4,
|
||||||
0xf9, 0x25, 0xbc, 0x04, 0x78, 0x3a, 0x98, 0x2f, 0x6d, 0x51, 0x3a, 0xa2, 0x21, 0x6f, 0x3e, 0x16,
|
0x2f, 0x1d, 0xd7, 0x61, 0xb7, 0x71, 0xcf, 0xc2, 0xaf, 0x20, 0xf0, 0xa7, 0xc4, 0x8b, 0x9b, 0x80,
|
||||||
0xc1, 0x53, 0x22, 0xf8, 0x62, 0xac, 0xdf, 0x80, 0x76, 0x37, 0x5c, 0x74, 0x84, 0x0e, 0xa1, 0x38,
|
0x12, 0x5e, 0x02, 0xdc, 0x1d, 0xcc, 0x97, 0xb4, 0xc8, 0x1d, 0xd1, 0x92, 0x77, 0x23, 0x0b, 0xe3,
|
||||||
0x5b, 0xc2, 0x22, 0xa2, 0x82, 0x93, 0x50, 0x32, 0xd1, 0x5b, 0xbf, 0x9c, 0x68, 0xfd, 0xcf, 0x0a,
|
0x29, 0x61, 0x7c, 0xb1, 0xd6, 0x6f, 0x40, 0xbb, 0x6b, 0x2e, 0x7a, 0x42, 0x87, 0x50, 0x9c, 0x2d,
|
||||||
0x6c, 0x9f, 0xce, 0x1d, 0x77, 0xbc, 0xd2, 0x79, 0x25, 0x5f, 0x76, 0xca, 0xea, 0xcb, 0x6e, 0xd3,
|
0x61, 0x61, 0x51, 0xc1, 0x49, 0x28, 0xe9, 0xe8, 0xad, 0x5f, 0x76, 0xb4, 0xfe, 0x17, 0x05, 0xb6,
|
||||||
0xb3, 0x6d, 0x6b, 0xe3, 0xb3, 0x6d, 0xd3, 0xd3, 0x28, 0x75, 0xef, 0xd3, 0xe8, 0x09, 0x14, 0x97,
|
0x4f, 0xe7, 0x8e, 0x3b, 0x5e, 0x69, 0xc5, 0x92, 0xa3, 0x9e, 0xb2, 0x3a, 0xea, 0x6d, 0x9a, 0xe3,
|
||||||
0xaf, 0x22, 0xd9, 0xd8, 0x96, 0x30, 0x4c, 0xe2, 0x27, 0x51, 0xa8, 0xbf, 0x01, 0x94, 0x5c, 0x68,
|
0xb6, 0x36, 0xce, 0x71, 0x9b, 0x66, 0xa5, 0xd4, 0xbd, 0xb3, 0xd2, 0x13, 0x28, 0x2e, 0xc7, 0x24,
|
||||||
0xb4, 0x21, 0x8b, 0x06, 0x50, 0xb9, 0xb7, 0x01, 0x7c, 0xf1, 0x77, 0x05, 0x4a, 0xc9, 0x2e, 0x1c,
|
0xd9, 0xe9, 0x96, 0x30, 0x4c, 0xe2, 0x19, 0x29, 0xd4, 0xdf, 0x00, 0x4a, 0x6e, 0x34, 0xba, 0x90,
|
||||||
0x95, 0xa1, 0x60, 0x5a, 0xa4, 0xdd, 0x31, 0xbf, 0x3b, 0x1b, 0xa8, 0x5f, 0xf0, 0x61, 0x7f, 0xd8,
|
0x45, 0x47, 0xa8, 0xdc, 0xdb, 0x11, 0xbe, 0xf8, 0xbb, 0x02, 0xa5, 0x64, 0x5b, 0x8e, 0xca, 0x50,
|
||||||
0x6c, 0x1a, 0x46, 0xcb, 0x68, 0xa9, 0x0a, 0xbf, 0x1f, 0xb8, 0xd4, 0x1b, 0x2d, 0x32, 0x30, 0xbb,
|
0x30, 0x2d, 0xd2, 0xee, 0x98, 0xdf, 0x9d, 0x0d, 0xd4, 0x2f, 0xf8, 0xb2, 0x3f, 0x6c, 0x36, 0x0d,
|
||||||
0x46, 0x6f, 0xc8, 0x3b, 0x87, 0x1d, 0xa8, 0x46, 0x98, 0xd5, 0x23, 0xb8, 0x37, 0x1c, 0x18, 0x6a,
|
0xa3, 0x65, 0xb4, 0x54, 0x85, 0x17, 0x0c, 0x9e, 0xea, 0x8d, 0xd6, 0xa2, 0x3e, 0x6c, 0xf1, 0xd2,
|
||||||
0x0a, 0xa9, 0x50, 0x8a, 0x40, 0x03, 0xe3, 0x1e, 0x56, 0xd3, 0xfc, 0xba, 0x8b, 0x90, 0xbb, 0x5d,
|
0x1e, 0x61, 0x56, 0x8f, 0xe0, 0xde, 0x70, 0x60, 0xa8, 0x29, 0xa4, 0x42, 0x29, 0x02, 0x0d, 0x8c,
|
||||||
0x48, 0xdc, 0xa4, 0x64, 0x44, 0x97, 0x11, 0xb3, 0x96, 0x17, 0x34, 0x39, 0x6d, 0x74, 0x1a, 0x56,
|
0x7b, 0x58, 0x4d, 0xf3, 0xfa, 0x17, 0x21, 0x77, 0xdb, 0x92, 0xb8, 0x6b, 0xc9, 0x88, 0xb6, 0x23,
|
||||||
0xd3, 0x50, 0xb3, 0x27, 0x7f, 0xcb, 0x40, 0x56, 0x7c, 0x41, 0x80, 0xce, 0xa0, 0x98, 0x78, 0x90,
|
0xe6, 0x5a, 0x56, 0x6c, 0x72, 0xda, 0xe8, 0x34, 0xac, 0xa6, 0xa1, 0x66, 0x4f, 0xfe, 0x96, 0x81,
|
||||||
0xa1, 0x83, 0xcf, 0x3e, 0xd4, 0x6a, 0xda, 0xe6, 0x77, 0xc7, 0x3c, 0x7c, 0xa5, 0xa0, 0x73, 0x28,
|
0xac, 0x38, 0x41, 0x80, 0xce, 0xa0, 0x98, 0x98, 0xd0, 0xd0, 0xc1, 0x67, 0x27, 0xb7, 0x9a, 0xb6,
|
||||||
0x25, 0x9f, 0x3b, 0x28, 0xd9, 0x9b, 0x6e, 0x78, 0x07, 0x7d, 0xd6, 0xd7, 0x5b, 0x50, 0x8d, 0x90,
|
0x79, 0x10, 0x99, 0x87, 0xaf, 0x14, 0x74, 0x0e, 0xa5, 0xe4, 0xfc, 0x83, 0x92, 0xcd, 0xea, 0x86,
|
||||||
0x39, 0x53, 0xde, 0x8b, 0x46, 0xaf, 0x03, 0x54, 0x4b, 0xf0, 0xd7, 0x9e, 0x1c, 0xb5, 0xfd, 0x8d,
|
0xc1, 0xe8, 0xb3, 0xba, 0xde, 0x82, 0x6a, 0x84, 0xcc, 0x99, 0xf2, 0xe6, 0x34, 0x1a, 0x17, 0x50,
|
||||||
0xb6, 0x28, 0x85, 0x1d, 0xf9, 0x89, 0x51, 0x7f, 0x7e, 0xe7, 0x13, 0x57, 0x1f, 0x05, 0xb5, 0x2f,
|
0x2d, 0xc1, 0xbf, 0x36, 0x83, 0xd4, 0xf6, 0x37, 0xd2, 0x22, 0x17, 0x76, 0xe4, 0x11, 0xa3, 0x86,
|
||||||
0xef, 0x33, 0x47, 0xde, 0xc6, 0xb0, 0xb3, 0x41, 0xc0, 0xd1, 0xaf, 0x92, 0x2b, 0xb8, 0x57, 0xfe,
|
0xfd, 0xce, 0x11, 0x57, 0xa7, 0x84, 0xda, 0x97, 0xf7, 0x91, 0x23, 0x6d, 0x63, 0xd8, 0xd9, 0x90,
|
||||||
0x6b, 0xcf, 0x7f, 0x89, 0xb6, 0x8c, 0xb2, 0x41, 0xe9, 0x57, 0xa2, 0xdc, 0x7f, 0x4f, 0xac, 0x44,
|
0xc0, 0xd1, 0xaf, 0x92, 0x3b, 0xb8, 0x37, 0xfd, 0xd7, 0x9e, 0xff, 0x12, 0xdb, 0xd2, 0xca, 0x86,
|
||||||
0xf9, 0xdc, 0x85, 0xf1, 0x23, 0xa8, 0xeb, 0x4a, 0x80, 0xf4, 0xf5, 0xb9, 0x77, 0x55, 0xa9, 0xf6,
|
0x4c, 0xbf, 0x62, 0xe5, 0xfe, 0x3a, 0xb1, 0x62, 0xe5, 0x73, 0x05, 0xe3, 0x47, 0x50, 0xd7, 0x33,
|
||||||
0xf4, 0xb3, 0x9c, 0xc8, 0xb9, 0x09, 0xb0, 0xac, 0x27, 0xf4, 0x38, 0x31, 0xe5, 0x8e, 0x1e, 0xd4,
|
0x01, 0xd2, 0xd7, 0x65, 0xef, 0x66, 0xa5, 0xda, 0xd3, 0xcf, 0xf2, 0x44, 0xca, 0x4d, 0x80, 0x65,
|
||||||
0x0e, 0xee, 0xb1, 0x4a, 0x57, 0xa7, 0xbf, 0xfe, 0xe3, 0xf1, 0xb5, 0xc3, 0x26, 0xf3, 0xcb, 0xa3,
|
0x3c, 0xa1, 0xc7, 0x09, 0x91, 0x3b, 0xf9, 0xa0, 0x76, 0x70, 0x0f, 0x55, 0xaa, 0x3a, 0xfd, 0xf5,
|
||||||
0x91, 0x3f, 0x3d, 0x76, 0x79, 0x47, 0xef, 0x39, 0xde, 0xb5, 0x47, 0xd9, 0xcf, 0x7e, 0x70, 0x73,
|
0x1f, 0x8f, 0xaf, 0x1d, 0x36, 0x99, 0x5f, 0x1e, 0x8d, 0xfc, 0xe9, 0xb1, 0xcb, 0x5b, 0x7c, 0xcf,
|
||||||
0xec, 0x7a, 0xe3, 0x63, 0x51, 0x96, 0xc7, 0x0b, 0x2f, 0x97, 0x59, 0xf1, 0xff, 0xa7, 0xdf, 0xfc,
|
0xf1, 0xae, 0x3d, 0xca, 0x7e, 0xf6, 0x83, 0x9b, 0x63, 0xd7, 0x1b, 0x1f, 0x8b, 0xb0, 0x3c, 0x5e,
|
||||||
0x2f, 0x00, 0x00, 0xff, 0xff, 0x42, 0x87, 0x8c, 0xeb, 0xaf, 0x12, 0x00, 0x00,
|
0x68, 0xb9, 0xcc, 0x8a, 0x3f, 0xa4, 0x7e, 0xf3, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x10, 0xf6,
|
||||||
|
0xab, 0x74, 0xc0, 0x12, 0x00, 0x00,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reference imports to suppress errors if they are not otherwise used.
|
// Reference imports to suppress errors if they are not otherwise used.
|
||||||
|
@ -109,7 +109,9 @@ message SendPaymentRequest {
|
|||||||
/**
|
/**
|
||||||
Features assumed to be supported by the final node. All transitive feature
|
Features assumed to be supported by the final node. All transitive feature
|
||||||
depdencies must also be set properly. For a given feature bit pair, either
|
depdencies must also be set properly. For a given feature bit pair, either
|
||||||
optional or remote may be set, but not both.
|
optional or remote may be set, but not both. If this field is nil or empty,
|
||||||
|
the router will try to load destination features from the graph as a
|
||||||
|
fallback.
|
||||||
*/
|
*/
|
||||||
repeated lnrpc.FeatureBit dest_features = 16;
|
repeated lnrpc.FeatureBit dest_features = 16;
|
||||||
}
|
}
|
||||||
@ -254,6 +256,7 @@ message Failure {
|
|||||||
PERMANENT_NODE_FAILURE = 20;
|
PERMANENT_NODE_FAILURE = 20;
|
||||||
PERMANENT_CHANNEL_FAILURE = 21;
|
PERMANENT_CHANNEL_FAILURE = 21;
|
||||||
EXPIRY_TOO_FAR = 22;
|
EXPIRY_TOO_FAR = 22;
|
||||||
|
MPP_TIMEOUT = 23;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
The error source is known, but the failure itself couldn't be decoded.
|
The error source is known, but the failure itself couldn't be decoded.
|
||||||
|
@ -710,7 +710,15 @@ func unmarshallHopHint(rpcHint *lnrpc.HopHint) (zpay32.HopHint, error) {
|
|||||||
// UnmarshalFeatures converts a list of uint32's into a valid feature vector.
|
// UnmarshalFeatures converts a list of uint32's into a valid feature vector.
|
||||||
// This method checks that feature bit pairs aren't assigned toegether, and
|
// This method checks that feature bit pairs aren't assigned toegether, and
|
||||||
// validates transitive depdencies.
|
// validates transitive depdencies.
|
||||||
func UnmarshalFeatures(rpcFeatures []lnrpc.FeatureBit) (*lnwire.FeatureVector, error) {
|
func UnmarshalFeatures(
|
||||||
|
rpcFeatures []lnrpc.FeatureBit) (*lnwire.FeatureVector, error) {
|
||||||
|
|
||||||
|
// If no destination features are specified we'll return nil to signal
|
||||||
|
// that the router should try to use the graph as a fallback.
|
||||||
|
if rpcFeatures == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
raw := lnwire.NewRawFeatureVector()
|
raw := lnwire.NewRawFeatureVector()
|
||||||
for _, bit := range rpcFeatures {
|
for _, bit := range rpcFeatures {
|
||||||
err := raw.SafeSet(lnwire.FeatureBit(bit))
|
err := raw.SafeSet(lnwire.FeatureBit(bit))
|
||||||
|
@ -415,6 +415,9 @@ func marshallError(sendError error) (*Failure, error) {
|
|||||||
case *lnwire.FailPermanentChannelFailure:
|
case *lnwire.FailPermanentChannelFailure:
|
||||||
response.Code = Failure_PERMANENT_CHANNEL_FAILURE
|
response.Code = Failure_PERMANENT_CHANNEL_FAILURE
|
||||||
|
|
||||||
|
case *lnwire.FailMPPTimeout:
|
||||||
|
response.Code = Failure_MPP_TIMEOUT
|
||||||
|
|
||||||
case nil:
|
case nil:
|
||||||
response.Code = Failure_UNKNOWN_FAILURE
|
response.Code = Failure_UNKNOWN_FAILURE
|
||||||
|
|
||||||
|
@ -1229,7 +1229,9 @@ type SendRequest struct {
|
|||||||
//*
|
//*
|
||||||
//Features assumed to be supported by the final node. All transitive feature
|
//Features assumed to be supported by the final node. All transitive feature
|
||||||
//depdencies must also be set properly. For a given feature bit pair, either
|
//depdencies must also be set properly. For a given feature bit pair, either
|
||||||
//optional or remote may be set, but not both.
|
//optional or remote may be set, but not both. If this field is nil or empty,
|
||||||
|
//the router will try to load destination features from the graph as a
|
||||||
|
//fallback.
|
||||||
DestFeatures []FeatureBit `protobuf:"varint,15,rep,packed,name=dest_features,json=destFeatures,proto3,enum=lnrpc.FeatureBit" json:"dest_features,omitempty"`
|
DestFeatures []FeatureBit `protobuf:"varint,15,rep,packed,name=dest_features,json=destFeatures,proto3,enum=lnrpc.FeatureBit" json:"dest_features,omitempty"`
|
||||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
XXX_unrecognized []byte `json:"-"`
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
@ -971,7 +971,9 @@ message SendRequest {
|
|||||||
/**
|
/**
|
||||||
Features assumed to be supported by the final node. All transitive feature
|
Features assumed to be supported by the final node. All transitive feature
|
||||||
depdencies must also be set properly. For a given feature bit pair, either
|
depdencies must also be set properly. For a given feature bit pair, either
|
||||||
optional or remote may be set, but not both.
|
optional or remote may be set, but not both. If this field is nil or empty,
|
||||||
|
the router will try to load destination features from the graph as a
|
||||||
|
fallback.
|
||||||
*/
|
*/
|
||||||
repeated FeatureBit dest_features = 15;
|
repeated FeatureBit dest_features = 15;
|
||||||
}
|
}
|
||||||
|
@ -3874,7 +3874,7 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/lnrpcFeatureBit"
|
"$ref": "#/definitions/lnrpcFeatureBit"
|
||||||
},
|
},
|
||||||
"description": "*\nFeatures assumed to be supported by the final node. All transitive feature\ndepdencies must also be set properly. For a given feature bit pair, either\noptional or remote may be set, but not both."
|
"description": "*\nFeatures assumed to be supported by the final node. All transitive feature\ndepdencies must also be set properly. For a given feature bit pair, either\noptional or remote may be set, but not both. If this field is nil or empty,\nthe router will try to load destination features from the graph as a\nfallback."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -80,6 +80,7 @@ const (
|
|||||||
CodeFinalIncorrectHtlcAmount FailCode = 19
|
CodeFinalIncorrectHtlcAmount FailCode = 19
|
||||||
CodeExpiryTooFar FailCode = 21
|
CodeExpiryTooFar FailCode = 21
|
||||||
CodeInvalidOnionPayload = FlagPerm | 22
|
CodeInvalidOnionPayload = FlagPerm | 22
|
||||||
|
CodeMPPTimeout FailCode = 23
|
||||||
)
|
)
|
||||||
|
|
||||||
// String returns the string representation of the failure code.
|
// String returns the string representation of the failure code.
|
||||||
@ -154,6 +155,9 @@ func (c FailCode) String() string {
|
|||||||
case CodeInvalidOnionPayload:
|
case CodeInvalidOnionPayload:
|
||||||
return "InvalidOnionPayload"
|
return "InvalidOnionPayload"
|
||||||
|
|
||||||
|
case CodeMPPTimeout:
|
||||||
|
return "MPPTimeout"
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return "<unknown>"
|
return "<unknown>"
|
||||||
}
|
}
|
||||||
@ -1182,6 +1186,26 @@ func (f *InvalidOnionPayload) Encode(w io.Writer, pver uint32) error {
|
|||||||
return WriteElements(w, f.Offset)
|
return WriteElements(w, f.Offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FailMPPTimeout is returned if the complete amount for a multi part payment
|
||||||
|
// was not received within a reasonable time.
|
||||||
|
//
|
||||||
|
// NOTE: May only be returned by the final node in the path.
|
||||||
|
type FailMPPTimeout struct{}
|
||||||
|
|
||||||
|
// Code returns the failure unique code.
|
||||||
|
//
|
||||||
|
// NOTE: Part of the FailureMessage interface.
|
||||||
|
func (f *FailMPPTimeout) Code() FailCode {
|
||||||
|
return CodeMPPTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a human readable string describing the target FailureMessage.
|
||||||
|
//
|
||||||
|
// NOTE: Implements the error interface.
|
||||||
|
func (f *FailMPPTimeout) Error() string {
|
||||||
|
return f.Code().String()
|
||||||
|
}
|
||||||
|
|
||||||
// DecodeFailure decodes, validates, and parses the lnwire onion failure, for
|
// DecodeFailure decodes, validates, and parses the lnwire onion failure, for
|
||||||
// the provided protocol version.
|
// the provided protocol version.
|
||||||
func DecodeFailure(r io.Reader, pver uint32) (FailureMessage, error) {
|
func DecodeFailure(r io.Reader, pver uint32) (FailureMessage, error) {
|
||||||
@ -1366,6 +1390,9 @@ func makeEmptyOnionError(code FailCode) (FailureMessage, error) {
|
|||||||
case CodeInvalidOnionPayload:
|
case CodeInvalidOnionPayload:
|
||||||
return &InvalidOnionPayload{}, nil
|
return &InvalidOnionPayload{}, nil
|
||||||
|
|
||||||
|
case CodeMPPTimeout:
|
||||||
|
return &FailMPPTimeout{}, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, errors.Errorf("unknown error code: %v", code)
|
return nil, errors.Errorf("unknown error code: %v", code)
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ var onionFailures = []FailureMessage{
|
|||||||
&FailUnknownNextPeer{},
|
&FailUnknownNextPeer{},
|
||||||
&FailIncorrectPaymentAmount{},
|
&FailIncorrectPaymentAmount{},
|
||||||
&FailFinalExpiryTooSoon{},
|
&FailFinalExpiryTooSoon{},
|
||||||
|
&FailMPPTimeout{},
|
||||||
|
|
||||||
NewFailIncorrectDetails(99, 100),
|
NewFailIncorrectDetails(99, 100),
|
||||||
NewInvalidOnionVersion(testOnionHash),
|
NewInvalidOnionVersion(testOnionHash),
|
||||||
|
@ -98,6 +98,11 @@ func (r *MPP) Record() tlv.Record {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PayloadSize returns the size this record takes up in encoded form.
|
||||||
|
func (r *MPP) PayloadSize() uint64 {
|
||||||
|
return 32 + tlv.SizeTUint64(uint64(r.totalMsat))
|
||||||
|
}
|
||||||
|
|
||||||
// String returns a human-readable representation of the mpp payload field.
|
// String returns a human-readable representation of the mpp payload field.
|
||||||
func (r *MPP) String() string {
|
func (r *MPP) String() string {
|
||||||
return fmt.Sprintf("total=%v, addr=%x", r.totalMsat, r.paymentAddr)
|
return fmt.Sprintf("total=%v, addr=%x", r.totalMsat, r.paymentAddr)
|
||||||
|
@ -24,9 +24,10 @@ type nodeWithDist struct {
|
|||||||
// amount that includes also the fees for subsequent hops.
|
// amount that includes also the fees for subsequent hops.
|
||||||
amountToReceive lnwire.MilliSatoshi
|
amountToReceive lnwire.MilliSatoshi
|
||||||
|
|
||||||
// incomingCltv is the expected cltv value for the incoming htlc of this
|
// incomingCltv is the expected absolute expiry height for the incoming
|
||||||
// node. This value does not include the final cltv.
|
// htlc of this node. This value should already include the final cltv
|
||||||
incomingCltv uint32
|
// delta.
|
||||||
|
incomingCltv int32
|
||||||
|
|
||||||
// probability is the probability that from this node onward the route
|
// probability is the probability that from this node onward the route
|
||||||
// is successful.
|
// is successful.
|
||||||
@ -39,6 +40,10 @@ type nodeWithDist struct {
|
|||||||
|
|
||||||
// nextHop is the edge this route comes from.
|
// nextHop is the edge this route comes from.
|
||||||
nextHop *channeldb.ChannelEdgePolicy
|
nextHop *channeldb.ChannelEdgePolicy
|
||||||
|
|
||||||
|
// routingInfoSize is the total size requirement for the payloads field
|
||||||
|
// in the onion packet from this hop towards the final destination.
|
||||||
|
routingInfoSize uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// distanceHeap is a min-distance heap that's used within our path finding
|
// distanceHeap is a min-distance heap that's used within our path finding
|
||||||
|
@ -16,8 +16,12 @@ import (
|
|||||||
|
|
||||||
const testMaxRecords = 2
|
const testMaxRecords = 2
|
||||||
|
|
||||||
|
// TestMissionControlStore tests the recording of payment failure events
|
||||||
|
// in mission control. It tests encoding and decoding of differing lnwire
|
||||||
|
// failures (FailIncorrectDetails and FailMppTimeout), pruning of results
|
||||||
|
// and idempotent writes.
|
||||||
func TestMissionControlStore(t *testing.T) {
|
func TestMissionControlStore(t *testing.T) {
|
||||||
// Set time zone explictly to keep test deterministic.
|
// Set time zone explicitly to keep test deterministic.
|
||||||
time.Local = time.UTC
|
time.Local = time.UTC
|
||||||
|
|
||||||
file, err := ioutil.TempFile("", "*.db")
|
file, err := ioutil.TempFile("", "*.db")
|
||||||
@ -115,11 +119,12 @@ func TestMissionControlStore(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a newer result.
|
// Add a newer result which failed due to mpp timeout.
|
||||||
result3 := result1
|
result3 := result1
|
||||||
result3.timeReply = result1.timeReply.Add(2 * time.Hour)
|
result3.timeReply = result1.timeReply.Add(2 * time.Hour)
|
||||||
result3.timeFwd = result1.timeReply.Add(2 * time.Hour)
|
result3.timeFwd = result1.timeReply.Add(2 * time.Hour)
|
||||||
result3.id = 3
|
result3.id = 3
|
||||||
|
result3.failure = &lnwire.FailMPPTimeout{}
|
||||||
|
|
||||||
err = store.AddResult(&result3)
|
err = store.AddResult(&result3)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -7,8 +7,8 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec"
|
|
||||||
"github.com/coreos/bbolt"
|
"github.com/coreos/bbolt"
|
||||||
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/feature"
|
"github.com/lightningnetwork/lnd/feature"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
@ -17,12 +17,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// HopLimit is the maximum number hops that is permissible as a route.
|
|
||||||
// Any potential paths found that lie above this limit will be rejected
|
|
||||||
// with an error. This value is computed using the current fixed-size
|
|
||||||
// packet length of the Sphinx construction.
|
|
||||||
HopLimit = 20
|
|
||||||
|
|
||||||
// infinity is used as a starting distance in our shortest path search.
|
// infinity is used as a starting distance in our shortest path search.
|
||||||
infinity = math.MaxInt64
|
infinity = math.MaxInt64
|
||||||
|
|
||||||
@ -48,7 +42,8 @@ const (
|
|||||||
// pathFinder defines the interface of a path finding algorithm.
|
// pathFinder defines the interface of a path finding algorithm.
|
||||||
type pathFinder = func(g *graphParams, r *RestrictParams,
|
type pathFinder = func(g *graphParams, r *RestrictParams,
|
||||||
cfg *PathFindingConfig, source, target route.Vertex,
|
cfg *PathFindingConfig, source, target route.Vertex,
|
||||||
amt lnwire.MilliSatoshi) ([]*channeldb.ChannelEdgePolicy, error)
|
amt lnwire.MilliSatoshi, finalHtlcExpiry int32) (
|
||||||
|
[]*channeldb.ChannelEdgePolicy, error)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// DefaultPaymentAttemptPenalty is the virtual cost in path finding weight
|
// DefaultPaymentAttemptPenalty is the virtual cost in path finding weight
|
||||||
@ -79,10 +74,6 @@ var (
|
|||||||
// not exist in the graph.
|
// not exist in the graph.
|
||||||
errNoPathFound = errors.New("unable to find a path to destination")
|
errNoPathFound = errors.New("unable to find a path to destination")
|
||||||
|
|
||||||
// errMaxHopsExceeded is returned when a candidate path is found, but
|
|
||||||
// the length of that path exceeds HopLimit.
|
|
||||||
errMaxHopsExceeded = errors.New("potential path has too many hops")
|
|
||||||
|
|
||||||
// errInsufficientLocalBalance is returned when none of the local
|
// errInsufficientLocalBalance is returned when none of the local
|
||||||
// channels have enough balance for the payment.
|
// channels have enough balance for the payment.
|
||||||
errInsufficientBalance = errors.New("insufficient local balance")
|
errInsufficientBalance = errors.New("insufficient local balance")
|
||||||
@ -412,7 +403,7 @@ func getMaxOutgoingAmt(node route.Vertex, outgoingChan *uint64,
|
|||||||
// that need to be paid along the path and accurately check the amount
|
// that need to be paid along the path and accurately check the amount
|
||||||
// to forward at every node against the available bandwidth.
|
// to forward at every node against the available bandwidth.
|
||||||
func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
||||||
source, target route.Vertex, amt lnwire.MilliSatoshi) (
|
source, target route.Vertex, amt lnwire.MilliSatoshi, finalHtlcExpiry int32) (
|
||||||
[]*channeldb.ChannelEdgePolicy, error) {
|
[]*channeldb.ChannelEdgePolicy, error) {
|
||||||
|
|
||||||
// Pathfinding can be a significant portion of the total payment
|
// Pathfinding can be a significant portion of the total payment
|
||||||
@ -447,12 +438,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
|||||||
// we have for the target node from our graph.
|
// we have for the target node from our graph.
|
||||||
features := r.DestFeatures
|
features := r.DestFeatures
|
||||||
if features == nil {
|
if features == nil {
|
||||||
targetKey, err := btcec.ParsePubKey(target[:], btcec.S256())
|
targetNode, err := g.graph.FetchLightningNode(tx, target)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
targetNode, err := g.graph.FetchLightningNode(targetKey)
|
|
||||||
switch {
|
switch {
|
||||||
|
|
||||||
// If the node exists and has features, use them directly.
|
// If the node exists and has features, use them directly.
|
||||||
@ -534,6 +520,23 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build a preliminary destination hop structure to obtain the payload
|
||||||
|
// size.
|
||||||
|
var mpp *record.MPP
|
||||||
|
if r.PaymentAddr != nil {
|
||||||
|
mpp = record.NewMPP(amt, *r.PaymentAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
finalHop := route.Hop{
|
||||||
|
AmtToForward: amt,
|
||||||
|
OutgoingTimeLock: uint32(finalHtlcExpiry),
|
||||||
|
CustomRecords: r.DestCustomRecords,
|
||||||
|
LegacyPayload: !features.HasFeature(
|
||||||
|
lnwire.TLVOnionPayloadOptional,
|
||||||
|
),
|
||||||
|
MPP: mpp,
|
||||||
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
// The target node charges no fee. Distance is set to 0, because this is
|
// The target node charges no fee. Distance is set to 0, because this is
|
||||||
@ -548,13 +551,19 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
|||||||
weight: 0,
|
weight: 0,
|
||||||
node: target,
|
node: target,
|
||||||
amountToReceive: amt,
|
amountToReceive: amt,
|
||||||
incomingCltv: 0,
|
incomingCltv: finalHtlcExpiry,
|
||||||
probability: 1,
|
probability: 1,
|
||||||
|
routingInfoSize: finalHop.PayloadSize(0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate the absolute cltv limit. Use uint64 to prevent an overflow
|
||||||
|
// if the cltv limit is MaxUint32.
|
||||||
|
absoluteCltvLimit := uint64(r.CltvLimit) + uint64(finalHtlcExpiry)
|
||||||
|
|
||||||
// processEdge is a helper closure that will be used to make sure edges
|
// processEdge is a helper closure that will be used to make sure edges
|
||||||
// satisfy our specific requirements.
|
// satisfy our specific requirements.
|
||||||
processEdge := func(fromVertex route.Vertex,
|
processEdge := func(fromVertex route.Vertex,
|
||||||
|
fromFeatures *lnwire.FeatureVector,
|
||||||
edge *channeldb.ChannelEdgePolicy, toNodeDist *nodeWithDist) {
|
edge *channeldb.ChannelEdgePolicy, toNodeDist *nodeWithDist) {
|
||||||
|
|
||||||
edgesExpanded++
|
edgesExpanded++
|
||||||
@ -596,11 +605,10 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
|||||||
timeLockDelta = edge.TimeLockDelta
|
timeLockDelta = edge.TimeLockDelta
|
||||||
}
|
}
|
||||||
|
|
||||||
incomingCltv := toNodeDist.incomingCltv +
|
incomingCltv := toNodeDist.incomingCltv + int32(timeLockDelta)
|
||||||
uint32(timeLockDelta)
|
|
||||||
|
|
||||||
// Check that we are within our CLTV limit.
|
// Check that we are within our CLTV limit.
|
||||||
if incomingCltv > r.CltvLimit {
|
if uint64(incomingCltv) > absoluteCltvLimit {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -676,34 +684,32 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
|||||||
edge.ChannelID)
|
edge.ChannelID)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
// Calculate the total routing info size if this hop were to be
|
||||||
|
// included. If we are coming from the source hop, the payload
|
||||||
|
// size is zero, because the original htlc isn't in the onion
|
||||||
|
// blob.
|
||||||
|
var payloadSize uint64
|
||||||
|
if fromVertex != source {
|
||||||
|
supportsTlv := fromFeatures.HasFeature(
|
||||||
|
lnwire.TLVOnionPayloadOptional,
|
||||||
|
)
|
||||||
|
|
||||||
// If this edge takes us to the final hop, we'll set the node
|
hop := route.Hop{
|
||||||
// features to those determined above. These are either taken
|
AmtToForward: amountToSend,
|
||||||
// from the destination features, e.g. virtual or invoice
|
OutgoingTimeLock: uint32(
|
||||||
// features, or loaded as a fallback from the graph. The
|
toNodeDist.incomingCltv,
|
||||||
// transitive dependencies were already validated above, so no
|
),
|
||||||
// need to do so now.
|
LegacyPayload: !supportsTlv,
|
||||||
//
|
|
||||||
// NOTE: This may overwrite features loaded from the graph if
|
|
||||||
// destination features were provided. This is fine though,
|
|
||||||
// since our route construction does not care where the features
|
|
||||||
// are actually taken from. In the future we may wish to do
|
|
||||||
// route construction within findPath, and avoid using
|
|
||||||
// ChannelEdgePolicy altogether.
|
|
||||||
case edge.Node.PubKeyBytes == target:
|
|
||||||
edge.Node.Features = features
|
|
||||||
|
|
||||||
// Otherwise, this is some other intermediary node. Verify the
|
|
||||||
// transitive feature dependencies for this node, and skip the
|
|
||||||
// channel if they are invalid.
|
|
||||||
default:
|
|
||||||
err := feature.ValidateDeps(edge.Node.Features)
|
|
||||||
if err != nil {
|
|
||||||
log.Tracef("Node %x has invalid features",
|
|
||||||
edge.Node.PubKeyBytes)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
payloadSize = hop.PayloadSize(edge.ChannelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
routingInfoSize := toNodeDist.routingInfoSize + payloadSize
|
||||||
|
|
||||||
|
// Skip paths that would exceed the maximum routing info size.
|
||||||
|
if routingInfoSize > sphinx.MaxPayloadSize {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// All conditions are met and this new tentative distance is
|
// All conditions are met and this new tentative distance is
|
||||||
@ -718,6 +724,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
|||||||
incomingCltv: incomingCltv,
|
incomingCltv: incomingCltv,
|
||||||
probability: probability,
|
probability: probability,
|
||||||
nextHop: edge,
|
nextHop: edge,
|
||||||
|
routingInfoSize: routingInfoSize,
|
||||||
}
|
}
|
||||||
distance[fromVertex] = withDist
|
distance[fromVertex] = withDist
|
||||||
|
|
||||||
@ -730,6 +737,44 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
|||||||
// TODO(roasbeef): also add path caching
|
// TODO(roasbeef): also add path caching
|
||||||
// * similar to route caching, but doesn't factor in the amount
|
// * similar to route caching, but doesn't factor in the amount
|
||||||
|
|
||||||
|
// Cache features because we visit nodes multiple times.
|
||||||
|
featureCache := make(map[route.Vertex]*lnwire.FeatureVector)
|
||||||
|
|
||||||
|
// getGraphFeatures returns (cached) node features from the graph.
|
||||||
|
getGraphFeatures := func(node route.Vertex) (*lnwire.FeatureVector,
|
||||||
|
error) {
|
||||||
|
|
||||||
|
// Check cache for features of the fromNode.
|
||||||
|
fromFeatures, ok := featureCache[node]
|
||||||
|
if !ok {
|
||||||
|
targetNode, err := g.graph.FetchLightningNode(tx, node)
|
||||||
|
switch {
|
||||||
|
|
||||||
|
// If the node exists and has valid features, use them.
|
||||||
|
case err == nil:
|
||||||
|
err := feature.ValidateDeps(targetNode.Features)
|
||||||
|
if err == nil {
|
||||||
|
fromFeatures = targetNode.Features
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an error other than the node not existing is hit,
|
||||||
|
// abort.
|
||||||
|
case err != channeldb.ErrGraphNodeNotFound:
|
||||||
|
return nil, err
|
||||||
|
|
||||||
|
// Otherwise, we couldn't find a node announcement,
|
||||||
|
// populate a blank feature vector.
|
||||||
|
default:
|
||||||
|
fromFeatures = lnwire.EmptyFeatureVector()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cache.
|
||||||
|
featureCache[node] = fromFeatures
|
||||||
|
}
|
||||||
|
|
||||||
|
return fromFeatures, nil
|
||||||
|
}
|
||||||
|
|
||||||
routeToSelf := source == target
|
routeToSelf := source == target
|
||||||
for {
|
for {
|
||||||
nodesVisited++
|
nodesVisited++
|
||||||
@ -777,9 +822,20 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get feature vector for fromNode.
|
||||||
|
fromFeatures, err := getGraphFeatures(fromNode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no valid features, skip this node.
|
||||||
|
if fromFeatures == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this candidate node is better than what we
|
// Check if this candidate node is better than what we
|
||||||
// already have.
|
// already have.
|
||||||
processEdge(fromNode, policy, partialPath)
|
processEdge(fromNode, fromFeatures, policy, partialPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
if nodeHeap.Len() == 0 {
|
if nodeHeap.Len() == 0 {
|
||||||
@ -824,17 +880,21 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The route is invalid if it spans more than 20 hops. The current
|
// For the final hop, we'll set the node features to those determined
|
||||||
// Sphinx (onion routing) implementation can only encode up to 20 hops
|
// above. These are either taken from the destination features, e.g.
|
||||||
// as the entire packet is fixed size. If this route is more than 20
|
// virtual or invoice features, or loaded as a fallback from the graph.
|
||||||
// hops, then it's invalid.
|
// The transitive dependencies were already validated above, so no need
|
||||||
numEdges := len(pathEdges)
|
// to do so now.
|
||||||
if numEdges > HopLimit {
|
//
|
||||||
return nil, errMaxHopsExceeded
|
// NOTE: This may overwrite features loaded from the graph if
|
||||||
}
|
// destination features were provided. This is fine though, since our
|
||||||
|
// route construction does not care where the features are actually
|
||||||
|
// taken from. In the future we may wish to do route construction within
|
||||||
|
// findPath, and avoid using ChannelEdgePolicy altogether.
|
||||||
|
pathEdges[len(pathEdges)-1].Node.Features = features
|
||||||
|
|
||||||
log.Debugf("Found route: probability=%v, hops=%v, fee=%v\n",
|
log.Debugf("Found route: probability=%v, hops=%v, fee=%v",
|
||||||
distance[source].probability, numEdges,
|
distance[source].probability, len(pathEdges),
|
||||||
distance[source].amountToReceive-amt)
|
distance[source].amountToReceive-amt)
|
||||||
|
|
||||||
return pathEdges, nil
|
return pathEdges, nil
|
||||||
|
@ -823,6 +823,7 @@ func testBasicGraphPathFindingCase(t *testing.T, graphInstance *testGraphInstanc
|
|||||||
},
|
},
|
||||||
testPathFindingConfig,
|
testPathFindingConfig,
|
||||||
sourceNode.PubKeyBytes, target, paymentAmt,
|
sourceNode.PubKeyBytes, target, paymentAmt,
|
||||||
|
startingHeight+finalHopCLTV,
|
||||||
)
|
)
|
||||||
if test.expectFailureNoPath {
|
if test.expectFailureNoPath {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -1012,6 +1013,7 @@ func TestPathFindingWithAdditionalEdges(t *testing.T) {
|
|||||||
},
|
},
|
||||||
r, testPathFindingConfig,
|
r, testPathFindingConfig,
|
||||||
sourceNode.PubKeyBytes, doge.PubKeyBytes, paymentAmt,
|
sourceNode.PubKeyBytes, doge.PubKeyBytes, paymentAmt,
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1358,53 +1360,53 @@ func TestNewRoute(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNewRoutePathTooLong(t *testing.T) {
|
func TestNewRoutePathTooLong(t *testing.T) {
|
||||||
t.Skip()
|
t.Parallel()
|
||||||
|
|
||||||
// Ensure that potential paths which are over the maximum hop-limit are
|
var testChannels []*testChannel
|
||||||
// rejected.
|
|
||||||
graph, err := parseTestGraph(excessiveHopsGraphFilePath)
|
// Setup a linear network of 21 hops.
|
||||||
|
fromNode := "start"
|
||||||
|
for i := 0; i < 21; i++ {
|
||||||
|
toNode := fmt.Sprintf("node-%v", i+1)
|
||||||
|
c := symmetricTestChannel(fromNode, toNode, 100000, &testChannelPolicy{
|
||||||
|
Expiry: 144,
|
||||||
|
FeeRate: 400,
|
||||||
|
MinHTLC: 1,
|
||||||
|
MaxHTLC: 100000001,
|
||||||
|
})
|
||||||
|
testChannels = append(testChannels, c)
|
||||||
|
|
||||||
|
fromNode = toNode
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := newPathFindingTestContext(t, testChannels, "start")
|
||||||
|
defer ctx.cleanup()
|
||||||
|
|
||||||
|
// Assert that we can find 20 hop routes.
|
||||||
|
node20 := ctx.keyFromAlias("node-20")
|
||||||
|
payAmt := lnwire.MilliSatoshi(100001)
|
||||||
|
_, err := ctx.findPath(node20, payAmt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create graph: %v", err)
|
t.Fatalf("unexpected pathfinding failure: %v", err)
|
||||||
}
|
|
||||||
defer graph.cleanUp()
|
|
||||||
|
|
||||||
sourceNode, err := graph.graph.SourceNode()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to fetch source node: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
paymentAmt := lnwire.NewMSatFromSatoshis(100)
|
// Assert that finding a 21 hop route fails.
|
||||||
|
node21 := ctx.keyFromAlias("node-21")
|
||||||
// We start by confirming that routing a payment 20 hops away is
|
_, err = ctx.findPath(node21, payAmt)
|
||||||
// possible. Alice should be able to find a valid route to ursula.
|
if err != errNoPathFound {
|
||||||
target := graph.aliasMap["ursula"]
|
t.Fatalf("not route error expected, but got %v", err)
|
||||||
_, err = findPath(
|
|
||||||
&graphParams{
|
|
||||||
graph: graph.graph,
|
|
||||||
},
|
|
||||||
noRestrictions, testPathFindingConfig,
|
|
||||||
sourceNode.PubKeyBytes, target, paymentAmt,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("path should have been found")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vincent is 21 hops away from Alice, and thus no valid route should be
|
// Assert that we can't find a 20 hop route if custom records make it
|
||||||
// presented to Alice.
|
// exceed the maximum payload size.
|
||||||
target = graph.aliasMap["vincent"]
|
ctx.restrictParams.DestFeatures = tlvFeatures
|
||||||
path, err := findPath(
|
ctx.restrictParams.DestCustomRecords = map[uint64][]byte{
|
||||||
&graphParams{
|
100000: bytes.Repeat([]byte{1}, 100),
|
||||||
graph: graph.graph,
|
}
|
||||||
},
|
_, err = ctx.findPath(node20, payAmt)
|
||||||
noRestrictions, testPathFindingConfig,
|
if err != errNoPathFound {
|
||||||
sourceNode.PubKeyBytes, target, paymentAmt,
|
t.Fatalf("not route error expected, but got %v", err)
|
||||||
)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("should not have been able to find path, supposed to be "+
|
|
||||||
"greater than 20 hops, found route with %v hops",
|
|
||||||
len(path))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPathNotAvailable(t *testing.T) {
|
func TestPathNotAvailable(t *testing.T) {
|
||||||
@ -1437,7 +1439,7 @@ func TestPathNotAvailable(t *testing.T) {
|
|||||||
graph: graph.graph,
|
graph: graph.graph,
|
||||||
},
|
},
|
||||||
noRestrictions, testPathFindingConfig,
|
noRestrictions, testPathFindingConfig,
|
||||||
sourceNode.PubKeyBytes, unknownNode, 100,
|
sourceNode.PubKeyBytes, unknownNode, 100, 0,
|
||||||
)
|
)
|
||||||
if err != errNoPathFound {
|
if err != errNoPathFound {
|
||||||
t.Fatalf("path shouldn't have been found: %v", err)
|
t.Fatalf("path shouldn't have been found: %v", err)
|
||||||
@ -1495,7 +1497,7 @@ func TestDestTLVGraphFallback(t *testing.T) {
|
|||||||
graph: ctx.graphParams.graph,
|
graph: ctx.graphParams.graph,
|
||||||
},
|
},
|
||||||
r, testPathFindingConfig,
|
r, testPathFindingConfig,
|
||||||
sourceNode.PubKeyBytes, target, 100,
|
sourceNode.PubKeyBytes, target, 100, 0,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1604,7 +1606,7 @@ func TestMissingFeatureDep(t *testing.T) {
|
|||||||
graph: ctx.graphParams.graph,
|
graph: ctx.graphParams.graph,
|
||||||
},
|
},
|
||||||
r, testPathFindingConfig,
|
r, testPathFindingConfig,
|
||||||
sourceNode.PubKeyBytes, target, 100,
|
sourceNode.PubKeyBytes, target, 100, 0,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1682,7 +1684,7 @@ func TestDestPaymentAddr(t *testing.T) {
|
|||||||
graph: ctx.graphParams.graph,
|
graph: ctx.graphParams.graph,
|
||||||
},
|
},
|
||||||
r, testPathFindingConfig,
|
r, testPathFindingConfig,
|
||||||
sourceNode.PubKeyBytes, target, 100,
|
sourceNode.PubKeyBytes, target, 100, 0,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1743,7 +1745,7 @@ func TestPathInsufficientCapacity(t *testing.T) {
|
|||||||
graph: graph.graph,
|
graph: graph.graph,
|
||||||
},
|
},
|
||||||
noRestrictions, testPathFindingConfig,
|
noRestrictions, testPathFindingConfig,
|
||||||
sourceNode.PubKeyBytes, target, payAmt,
|
sourceNode.PubKeyBytes, target, payAmt, 0,
|
||||||
)
|
)
|
||||||
if err != errNoPathFound {
|
if err != errNoPathFound {
|
||||||
t.Fatalf("graph shouldn't be able to support payment: %v", err)
|
t.Fatalf("graph shouldn't be able to support payment: %v", err)
|
||||||
@ -1776,7 +1778,7 @@ func TestRouteFailMinHTLC(t *testing.T) {
|
|||||||
graph: graph.graph,
|
graph: graph.graph,
|
||||||
},
|
},
|
||||||
noRestrictions, testPathFindingConfig,
|
noRestrictions, testPathFindingConfig,
|
||||||
sourceNode.PubKeyBytes, target, payAmt,
|
sourceNode.PubKeyBytes, target, payAmt, 0,
|
||||||
)
|
)
|
||||||
if err != errNoPathFound {
|
if err != errNoPathFound {
|
||||||
t.Fatalf("graph shouldn't be able to support payment: %v", err)
|
t.Fatalf("graph shouldn't be able to support payment: %v", err)
|
||||||
@ -1875,7 +1877,7 @@ func TestRouteFailDisabledEdge(t *testing.T) {
|
|||||||
graph: graph.graph,
|
graph: graph.graph,
|
||||||
},
|
},
|
||||||
noRestrictions, testPathFindingConfig,
|
noRestrictions, testPathFindingConfig,
|
||||||
sourceNode.PubKeyBytes, target, payAmt,
|
sourceNode.PubKeyBytes, target, payAmt, 0,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to find path: %v", err)
|
t.Fatalf("unable to find path: %v", err)
|
||||||
@ -1903,7 +1905,7 @@ func TestRouteFailDisabledEdge(t *testing.T) {
|
|||||||
graph: graph.graph,
|
graph: graph.graph,
|
||||||
},
|
},
|
||||||
noRestrictions, testPathFindingConfig,
|
noRestrictions, testPathFindingConfig,
|
||||||
sourceNode.PubKeyBytes, target, payAmt,
|
sourceNode.PubKeyBytes, target, payAmt, 0,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to find path: %v", err)
|
t.Fatalf("unable to find path: %v", err)
|
||||||
@ -1928,7 +1930,7 @@ func TestRouteFailDisabledEdge(t *testing.T) {
|
|||||||
graph: graph.graph,
|
graph: graph.graph,
|
||||||
},
|
},
|
||||||
noRestrictions, testPathFindingConfig,
|
noRestrictions, testPathFindingConfig,
|
||||||
sourceNode.PubKeyBytes, target, payAmt,
|
sourceNode.PubKeyBytes, target, payAmt, 0,
|
||||||
)
|
)
|
||||||
if err != errNoPathFound {
|
if err != errNoPathFound {
|
||||||
t.Fatalf("graph shouldn't be able to support payment: %v", err)
|
t.Fatalf("graph shouldn't be able to support payment: %v", err)
|
||||||
@ -1962,7 +1964,7 @@ func TestPathSourceEdgesBandwidth(t *testing.T) {
|
|||||||
graph: graph.graph,
|
graph: graph.graph,
|
||||||
},
|
},
|
||||||
noRestrictions, testPathFindingConfig,
|
noRestrictions, testPathFindingConfig,
|
||||||
sourceNode.PubKeyBytes, target, payAmt,
|
sourceNode.PubKeyBytes, target, payAmt, 0,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to find path: %v", err)
|
t.Fatalf("unable to find path: %v", err)
|
||||||
@ -1986,7 +1988,7 @@ func TestPathSourceEdgesBandwidth(t *testing.T) {
|
|||||||
bandwidthHints: bandwidths,
|
bandwidthHints: bandwidths,
|
||||||
},
|
},
|
||||||
noRestrictions, testPathFindingConfig,
|
noRestrictions, testPathFindingConfig,
|
||||||
sourceNode.PubKeyBytes, target, payAmt,
|
sourceNode.PubKeyBytes, target, payAmt, 0,
|
||||||
)
|
)
|
||||||
if err != errNoPathFound {
|
if err != errNoPathFound {
|
||||||
t.Fatalf("graph shouldn't be able to support payment: %v", err)
|
t.Fatalf("graph shouldn't be able to support payment: %v", err)
|
||||||
@ -2004,7 +2006,7 @@ func TestPathSourceEdgesBandwidth(t *testing.T) {
|
|||||||
bandwidthHints: bandwidths,
|
bandwidthHints: bandwidths,
|
||||||
},
|
},
|
||||||
noRestrictions, testPathFindingConfig,
|
noRestrictions, testPathFindingConfig,
|
||||||
sourceNode.PubKeyBytes, target, payAmt,
|
sourceNode.PubKeyBytes, target, payAmt, 0,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to find path: %v", err)
|
t.Fatalf("unable to find path: %v", err)
|
||||||
@ -2035,7 +2037,7 @@ func TestPathSourceEdgesBandwidth(t *testing.T) {
|
|||||||
bandwidthHints: bandwidths,
|
bandwidthHints: bandwidths,
|
||||||
},
|
},
|
||||||
noRestrictions, testPathFindingConfig,
|
noRestrictions, testPathFindingConfig,
|
||||||
sourceNode.PubKeyBytes, target, payAmt,
|
sourceNode.PubKeyBytes, target, payAmt, 0,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to find path: %v", err)
|
t.Fatalf("unable to find path: %v", err)
|
||||||
@ -2070,11 +2072,7 @@ func TestPathFindSpecExample(t *testing.T) {
|
|||||||
// Carol, so we set "B" as the source node so path finding starts from
|
// Carol, so we set "B" as the source node so path finding starts from
|
||||||
// Bob.
|
// Bob.
|
||||||
bob := ctx.aliases["B"]
|
bob := ctx.aliases["B"]
|
||||||
bobKey, err := btcec.ParsePubKey(bob[:], btcec.S256())
|
bobNode, err := ctx.graph.FetchLightningNode(nil, bob)
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
bobNode, err := ctx.graph.FetchLightningNode(bobKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to find bob: %v", err)
|
t.Fatalf("unable to find bob: %v", err)
|
||||||
}
|
}
|
||||||
@ -2123,11 +2121,7 @@ func TestPathFindSpecExample(t *testing.T) {
|
|||||||
// Next, we'll set A as the source node so we can assert that we create
|
// Next, we'll set A as the source node so we can assert that we create
|
||||||
// the proper route for any queries starting with Alice.
|
// the proper route for any queries starting with Alice.
|
||||||
alice := ctx.aliases["A"]
|
alice := ctx.aliases["A"]
|
||||||
aliceKey, err := btcec.ParsePubKey(alice[:], btcec.S256())
|
aliceNode, err := ctx.graph.FetchLightningNode(nil, alice)
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
aliceNode, err := ctx.graph.FetchLightningNode(aliceKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to find alice: %v", err)
|
t.Fatalf("unable to find alice: %v", err)
|
||||||
}
|
}
|
||||||
@ -2880,7 +2874,7 @@ func (c *pathFindingTestContext) findPath(target route.Vertex,
|
|||||||
|
|
||||||
return findPath(
|
return findPath(
|
||||||
&c.graphParams, &c.restrictParams, &c.pathFindingConfig,
|
&c.graphParams, &c.restrictParams, &c.pathFindingConfig,
|
||||||
c.source, target, amt,
|
c.source, target, amt, 0,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,7 +196,6 @@ func errorToPaymentFailure(err error) channeldb.FailureReason {
|
|||||||
errNoTlvPayload,
|
errNoTlvPayload,
|
||||||
errNoPaymentAddr,
|
errNoPaymentAddr,
|
||||||
errNoPathFound,
|
errNoPathFound,
|
||||||
errMaxHopsExceeded,
|
|
||||||
errPrebuiltRouteTried:
|
errPrebuiltRouteTried:
|
||||||
|
|
||||||
return channeldb.FailureReasonNoRoute
|
return channeldb.FailureReasonNoRoute
|
||||||
|
@ -113,6 +113,8 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finalHtlcExpiry := int32(height) + int32(finalCltvDelta)
|
||||||
|
|
||||||
path, err := p.pathFinder(
|
path, err := p.pathFinder(
|
||||||
&graphParams{
|
&graphParams{
|
||||||
graph: ss.Graph,
|
graph: ss.Graph,
|
||||||
@ -121,7 +123,7 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment,
|
|||||||
},
|
},
|
||||||
restrictions, &ss.PathFindingConfig,
|
restrictions, &ss.PathFindingConfig,
|
||||||
ss.SelfNode.PubKeyBytes, payment.Target,
|
ss.SelfNode.PubKeyBytes, payment.Target,
|
||||||
payment.Amount,
|
payment.Amount, finalHtlcExpiry,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -15,8 +15,8 @@ func TestRequestRoute(t *testing.T) {
|
|||||||
|
|
||||||
findPath := func(g *graphParams, r *RestrictParams,
|
findPath := func(g *graphParams, r *RestrictParams,
|
||||||
cfg *PathFindingConfig, source, target route.Vertex,
|
cfg *PathFindingConfig, source, target route.Vertex,
|
||||||
amt lnwire.MilliSatoshi) ([]*channeldb.ChannelEdgePolicy,
|
amt lnwire.MilliSatoshi, finalHtlcExpiry int32) (
|
||||||
error) {
|
[]*channeldb.ChannelEdgePolicy, error) {
|
||||||
|
|
||||||
// We expect find path to receive a cltv limit excluding the
|
// We expect find path to receive a cltv limit excluding the
|
||||||
// final cltv delta (including the block padding).
|
// final cltv delta (including the block padding).
|
||||||
|
@ -191,10 +191,8 @@ func (i *interpretedResult) processPaymentOutcomeFinal(
|
|||||||
i.failPair(route, n-1)
|
i.failPair(route, n-1)
|
||||||
|
|
||||||
// The other hops relayed corectly, so assign those pairs a
|
// The other hops relayed corectly, so assign those pairs a
|
||||||
// success result.
|
// success result. At this point, n >= 2.
|
||||||
if n > 2 {
|
|
||||||
i.successPairRange(route, 0, n-2)
|
i.successPairRange(route, 0, n-2)
|
||||||
}
|
|
||||||
|
|
||||||
// We are using wrong payment hash or amount, fail the payment.
|
// We are using wrong payment hash or amount, fail the payment.
|
||||||
case *lnwire.FailIncorrectPaymentAmount,
|
case *lnwire.FailIncorrectPaymentAmount,
|
||||||
@ -216,6 +214,22 @@ func (i *interpretedResult) processPaymentOutcomeFinal(
|
|||||||
// deliberately. What to penalize?
|
// deliberately. What to penalize?
|
||||||
i.finalFailureReason = &reasonIncorrectDetails
|
i.finalFailureReason = &reasonIncorrectDetails
|
||||||
|
|
||||||
|
case *lnwire.FailMPPTimeout:
|
||||||
|
// TODO(carla): decide how to penalize mpp timeout. In the
|
||||||
|
// meantime, attribute success to the hops along the route and
|
||||||
|
// do not penalize the final node.
|
||||||
|
|
||||||
|
i.finalFailureReason = &reasonError
|
||||||
|
|
||||||
|
// If this is a direct payment, take no action.
|
||||||
|
if n == 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign all pairs a success result except the final hop, as
|
||||||
|
// the payment reached the destination correctly.
|
||||||
|
i.successPairRange(route, 0, n-2)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// All other errors are considered terminal if coming from the
|
// All other errors are considered terminal if coming from the
|
||||||
// final hop. They indicate that something is wrong at the
|
// final hop. They indicate that something is wrong at the
|
||||||
@ -223,7 +237,7 @@ func (i *interpretedResult) processPaymentOutcomeFinal(
|
|||||||
i.failNode(route, n)
|
i.failNode(route, n)
|
||||||
|
|
||||||
// Other channels in the route forwarded correctly.
|
// Other channels in the route forwarded correctly.
|
||||||
if n > 2 {
|
if n >= 2 {
|
||||||
i.successPairRange(route, 0, n-2)
|
i.successPairRange(route, 0, n-2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,6 +32,16 @@ var (
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
routeThreeHop = route.Route{
|
||||||
|
SourcePubKey: hops[0],
|
||||||
|
TotalAmount: 100,
|
||||||
|
Hops: []*route.Hop{
|
||||||
|
{PubKeyBytes: hops[1], AmtToForward: 99},
|
||||||
|
{PubKeyBytes: hops[2], AmtToForward: 97},
|
||||||
|
{PubKeyBytes: hops[3], AmtToForward: 94},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
routeFourHop = route.Route{
|
routeFourHop = route.Route{
|
||||||
SourcePubKey: hops[0],
|
SourcePubKey: hops[0],
|
||||||
TotalAmount: 100,
|
TotalAmount: 100,
|
||||||
@ -203,7 +213,7 @@ var resultTestCases = []resultTestCase{
|
|||||||
// be failed while the proceeding hops are reproed as successes. The
|
// be failed while the proceeding hops are reproed as successes. The
|
||||||
// failure is terminal since the receiver can't process our onion.
|
// failure is terminal since the receiver can't process our onion.
|
||||||
{
|
{
|
||||||
name: "fail invalid onion payload final hop",
|
name: "fail invalid onion payload final hop four",
|
||||||
route: &routeFourHop,
|
route: &routeFourHop,
|
||||||
failureSrcIdx: 4,
|
failureSrcIdx: 4,
|
||||||
failure: lnwire.NewInvalidOnionPayload(0, 0),
|
failure: lnwire.NewInvalidOnionPayload(0, 0),
|
||||||
@ -229,6 +239,30 @@ var resultTestCases = []resultTestCase{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Tests an invalid onion payload from a final hop on a three hop route.
|
||||||
|
{
|
||||||
|
name: "fail invalid onion payload final hop three",
|
||||||
|
route: &routeThreeHop,
|
||||||
|
failureSrcIdx: 3,
|
||||||
|
failure: lnwire.NewInvalidOnionPayload(0, 0),
|
||||||
|
|
||||||
|
expectedResult: &interpretedResult{
|
||||||
|
pairResults: map[DirectedNodePair]pairResult{
|
||||||
|
getTestPair(0, 1): {
|
||||||
|
success: true,
|
||||||
|
amt: 100,
|
||||||
|
},
|
||||||
|
getTestPair(1, 2): {
|
||||||
|
success: true,
|
||||||
|
amt: 99,
|
||||||
|
},
|
||||||
|
getTestPair(3, 2): {},
|
||||||
|
},
|
||||||
|
finalFailureReason: &reasonError,
|
||||||
|
nodeFailure: &hops[3],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// Tests an invalid onion payload from an intermediate hop. Only the
|
// Tests an invalid onion payload from an intermediate hop. Only the
|
||||||
// reporting node should be failed. The failure is non-terminal since we
|
// reporting node should be failed. The failure is non-terminal since we
|
||||||
// can still try other paths.
|
// can still try other paths.
|
||||||
@ -272,6 +306,39 @@ var resultTestCases = []resultTestCase{
|
|||||||
nodeFailure: &hops[1],
|
nodeFailure: &hops[1],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Tests a single hop mpp timeout. Test that final node is not
|
||||||
|
// penalized. This is a temporary measure while we decide how to
|
||||||
|
// penalize mpp timeouts.
|
||||||
|
{
|
||||||
|
name: "one hop mpp timeout",
|
||||||
|
route: &routeOneHop,
|
||||||
|
failureSrcIdx: 1,
|
||||||
|
failure: &lnwire.FailMPPTimeout{},
|
||||||
|
|
||||||
|
expectedResult: &interpretedResult{
|
||||||
|
finalFailureReason: &reasonError,
|
||||||
|
nodeFailure: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tests a two hop mpp timeout. Test that final node is not penalized
|
||||||
|
// and the intermediate hop is attributed the success. This is a
|
||||||
|
// temporary measure while we decide how to penalize mpp timeouts.
|
||||||
|
{
|
||||||
|
name: "two hop mpp timeout",
|
||||||
|
route: &routeTwoHop,
|
||||||
|
failureSrcIdx: 2,
|
||||||
|
failure: &lnwire.FailMPPTimeout{},
|
||||||
|
|
||||||
|
expectedResult: &interpretedResult{
|
||||||
|
pairResults: map[DirectedNodePair]pairResult{
|
||||||
|
getTestPair(0, 1): successPairResult(100),
|
||||||
|
},
|
||||||
|
finalFailureReason: &reasonError,
|
||||||
|
nodeFailure: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestResultInterpretation executes a list of test cases that test the result
|
// TestResultInterpretation executes a list of test cases that test the result
|
||||||
|
@ -184,6 +184,53 @@ func (h *Hop) PackHopPayload(w io.Writer, nextChanID uint64) error {
|
|||||||
return tlvStream.Encode(w)
|
return tlvStream.Encode(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Size returns the total size this hop's payload would take up in the onion
|
||||||
|
// packet.
|
||||||
|
func (h *Hop) PayloadSize(nextChanID uint64) uint64 {
|
||||||
|
if h.LegacyPayload {
|
||||||
|
return sphinx.LegacyHopDataSize
|
||||||
|
}
|
||||||
|
|
||||||
|
var payloadSize uint64
|
||||||
|
|
||||||
|
addRecord := func(tlvType tlv.Type, length uint64) {
|
||||||
|
payloadSize += tlv.VarIntSize(uint64(tlvType)) +
|
||||||
|
tlv.VarIntSize(length) + length
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add amount size.
|
||||||
|
addRecord(record.AmtOnionType, tlv.SizeTUint64(uint64(h.AmtToForward)))
|
||||||
|
|
||||||
|
// Add lock time size.
|
||||||
|
addRecord(
|
||||||
|
record.LockTimeOnionType,
|
||||||
|
tlv.SizeTUint64(uint64(h.OutgoingTimeLock)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add next hop if present.
|
||||||
|
if nextChanID != 0 {
|
||||||
|
addRecord(record.NextHopOnionType, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add mpp if present.
|
||||||
|
if h.MPP != nil {
|
||||||
|
addRecord(record.MPPOnionType, h.MPP.PayloadSize())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom records.
|
||||||
|
for k, v := range h.CustomRecords {
|
||||||
|
addRecord(tlv.Type(k), uint64(len(v)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the size required to encode the payload length.
|
||||||
|
payloadSize += tlv.VarIntSize(payloadSize)
|
||||||
|
|
||||||
|
// Add HMAC.
|
||||||
|
payloadSize += sphinx.HMACSize
|
||||||
|
|
||||||
|
return payloadSize
|
||||||
|
}
|
||||||
|
|
||||||
// Route represents a path through the channel graph which runs over one or
|
// Route represents a path through the channel graph which runs over one or
|
||||||
// more channels in succession. This struct carries all the information
|
// more channels in succession. This struct carries all the information
|
||||||
// required to craft the Sphinx onion packet, and send the payment along the
|
// required to craft the Sphinx onion packet, and send the payment along the
|
||||||
|
@ -2,12 +2,20 @@ package route
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/record"
|
"github.com/lightningnetwork/lnd/record"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testPrivKeyBytes, _ = hex.DecodeString("e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734")
|
||||||
|
_, testPubKey = btcec.PrivKeyFromBytes(btcec.S256(), testPrivKeyBytes)
|
||||||
|
testPubKeyBytes, _ = NewVertexFromBytes(testPubKey.SerializeCompressed())
|
||||||
|
)
|
||||||
|
|
||||||
// TestRouteTotalFees checks that a route reports the expected total fee.
|
// TestRouteTotalFees checks that a route reports the expected total fee.
|
||||||
func TestRouteTotalFees(t *testing.T) {
|
func TestRouteTotalFees(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@ -56,7 +64,6 @@ func TestRouteTotalFees(t *testing.T) {
|
|||||||
if r.TotalFees() != fee {
|
if r.TotalFees() != fee {
|
||||||
t.Fatalf("expected %v fees, got %v", fee, r.TotalFees())
|
t.Fatalf("expected %v fees, got %v", fee, r.TotalFees())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -93,3 +100,57 @@ func TestMPPHop(t *testing.T) {
|
|||||||
t.Fatalf("expected err: %v, got: %v", nil, err)
|
t.Fatalf("expected err: %v, got: %v", nil, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPayloadSize tests the payload size calculation that is provided by Hop
|
||||||
|
// structs.
|
||||||
|
func TestPayloadSize(t *testing.T) {
|
||||||
|
hops := []*Hop{
|
||||||
|
{
|
||||||
|
PubKeyBytes: testPubKeyBytes,
|
||||||
|
AmtToForward: 1000,
|
||||||
|
OutgoingTimeLock: 600000,
|
||||||
|
ChannelID: 3432483437438,
|
||||||
|
LegacyPayload: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
PubKeyBytes: testPubKeyBytes,
|
||||||
|
AmtToForward: 1200,
|
||||||
|
OutgoingTimeLock: 700000,
|
||||||
|
ChannelID: 63584534844,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
PubKeyBytes: testPubKeyBytes,
|
||||||
|
AmtToForward: 1200,
|
||||||
|
OutgoingTimeLock: 700000,
|
||||||
|
MPP: record.NewMPP(500, [32]byte{}),
|
||||||
|
CustomRecords: map[uint64][]byte{
|
||||||
|
100000: {1, 2, 3},
|
||||||
|
1000000: {4, 5},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rt := Route{
|
||||||
|
Hops: hops,
|
||||||
|
}
|
||||||
|
path, err := rt.ToSphinxPath()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, onionHop := range path[:path.TrueRouteLength()] {
|
||||||
|
hop := hops[i]
|
||||||
|
var nextChan uint64
|
||||||
|
if i < len(hops)-1 {
|
||||||
|
nextChan = hops[i+1].ChannelID
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := uint64(onionHop.HopPayload.NumBytes())
|
||||||
|
actual := hop.PayloadSize(nextChan)
|
||||||
|
if expected != actual {
|
||||||
|
t.Fatalf("unexpected payload size at hop %v: "+
|
||||||
|
"expected %v, got %v",
|
||||||
|
i, expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1431,27 +1431,29 @@ func (r *ChannelRouter) FindRoute(source, target route.Vertex,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We'll fetch the current block height so we can properly calculate the
|
||||||
|
// required HTLC time locks within the route.
|
||||||
|
_, currentHeight, err := r.cfg.Chain.GetBestBlock()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Now that we know the destination is reachable within the graph, we'll
|
// Now that we know the destination is reachable within the graph, we'll
|
||||||
// execute our path finding algorithm.
|
// execute our path finding algorithm.
|
||||||
|
finalHtlcExpiry := currentHeight + int32(finalCLTVDelta)
|
||||||
|
|
||||||
path, err := findPath(
|
path, err := findPath(
|
||||||
&graphParams{
|
&graphParams{
|
||||||
graph: r.cfg.Graph,
|
graph: r.cfg.Graph,
|
||||||
bandwidthHints: bandwidthHints,
|
bandwidthHints: bandwidthHints,
|
||||||
},
|
},
|
||||||
restrictions, &r.cfg.PathFindingConfig,
|
restrictions, &r.cfg.PathFindingConfig,
|
||||||
source, target, amt,
|
source, target, amt, finalHtlcExpiry,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// We'll fetch the current block height so we can properly calculate the
|
|
||||||
// required HTLC time locks within the route.
|
|
||||||
_, currentHeight, err := r.cfg.Chain.GetBestBlock()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the route with absolute time lock values.
|
// Create the route with absolute time lock values.
|
||||||
route, err := newRoute(
|
route, err := newRoute(
|
||||||
source, path, uint32(currentHeight),
|
source, path, uint32(currentHeight),
|
||||||
@ -2089,11 +2091,7 @@ func (r *ChannelRouter) GetChannelByID(chanID lnwire.ShortChannelID) (
|
|||||||
//
|
//
|
||||||
// NOTE: This method is part of the ChannelGraphSource interface.
|
// NOTE: This method is part of the ChannelGraphSource interface.
|
||||||
func (r *ChannelRouter) FetchLightningNode(node route.Vertex) (*channeldb.LightningNode, error) {
|
func (r *ChannelRouter) FetchLightningNode(node route.Vertex) (*channeldb.LightningNode, error) {
|
||||||
pubKey, err := btcec.ParsePubKey(node[:], btcec.S256())
|
return r.cfg.Graph.FetchLightningNode(nil, node)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to parse raw public key: %v", err)
|
|
||||||
}
|
|
||||||
return r.cfg.Graph.FetchLightningNode(pubKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForEachNode is used to iterate over every node in router topology.
|
// ForEachNode is used to iterate over every node in router topology.
|
||||||
|
@ -1319,7 +1319,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
|
|||||||
t.Fatalf("unable to find any routes: %v", err)
|
t.Fatalf("unable to find any routes: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
copy1, err := ctx.graph.FetchLightningNode(priv1.PubKey())
|
copy1, err := ctx.graph.FetchLightningNode(nil, pub1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to fetch node: %v", err)
|
t.Fatalf("unable to fetch node: %v", err)
|
||||||
}
|
}
|
||||||
@ -1328,7 +1328,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
|
|||||||
t.Fatalf("fetched node not equal to original")
|
t.Fatalf("fetched node not equal to original")
|
||||||
}
|
}
|
||||||
|
|
||||||
copy2, err := ctx.graph.FetchLightningNode(priv2.PubKey())
|
copy2, err := ctx.graph.FetchLightningNode(nil, pub2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to fetch node: %v", err)
|
t.Fatalf("unable to fetch node: %v", err)
|
||||||
}
|
}
|
||||||
@ -2174,7 +2174,7 @@ func TestFindPathFeeWeighting(t *testing.T) {
|
|||||||
},
|
},
|
||||||
noRestrictions,
|
noRestrictions,
|
||||||
testPathFindingConfig,
|
testPathFindingConfig,
|
||||||
sourceNode.PubKeyBytes, target, amt,
|
sourceNode.PubKeyBytes, target, amt, 0,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to find path: %v", err)
|
t.Fatalf("unable to find path: %v", err)
|
||||||
|
410
routing/testdata/excessive_hops.json
vendored
410
routing/testdata/excessive_hops.json
vendored
@ -1,410 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"source": true,
|
|
||||||
"pubkey": "021b96642e723592ee0b095983fe3a26c8b40b8926968d8b7510e51c9429d4562c",
|
|
||||||
"alias": "alice"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "022096b2b0ac083e708074a5ab57288bc821b6bef7b964185b307e073772c3748f",
|
|
||||||
"alias": "bob"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "022a190ce901ab2b6f349483f18b28a1d72c64a7bccb8057291f25784c0899840f",
|
|
||||||
"alias": "carol"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "022d855d09971dd047b7ecf929b23c6f147b568d4668af67fb2226eb8c15c4660d",
|
|
||||||
"alias": "dave"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "024ca436834b0d38d9dc7ee4d95aa21db321c45598dc5921a4a52304a8e0dd2952",
|
|
||||||
"alias": "eve"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "025234a0c44cbf1b20c18e2c397107ad731376831e1c43ddb360b41dbb98c10266",
|
|
||||||
"alias": "fez"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "0253e9d03030f2ff08d3a7f1d824ad6d8c0dae422f324e72d5bb313e3f2a2d45a8",
|
|
||||||
"alias": "gabby"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "0263d4f2baca258ff3bd5bce86c2754e95daaea27f268ae1a048c1253ff20de56e",
|
|
||||||
"alias": "harold"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "02650db8e44302f75e265e9427264bc0d7e2337831d6b9ceb7c58ed1e725d4576a",
|
|
||||||
"alias": "inez"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "02727bfd298aa055a6419404931dfc1ccb4f0eb7c9660a7df346b93d0025df3ba1",
|
|
||||||
"alias": "jake"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "0280c83b3eded413dcec12f7952410e2738f079bd9cbc9a7c462e32ed4d74bd5b7",
|
|
||||||
"alias": "karen"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "0290bf454f4b95baf9227801301b331e35d477c6b6e7f36a599983ae58747b3828",
|
|
||||||
"alias": "liam"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "0297c8de635d17e3dd5775edfa2797be0874c53b0026f69009787cecd2fa577de8",
|
|
||||||
"alias": "maggie"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "02a27227113c71eab0c8609ac0cdc7e76791fc3163c16e643cb4658d1080c7e336",
|
|
||||||
"alias": "nick"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "02f5f6bb6373fc60528118003f803557b916fbecd90c3a0c5df4c86c6a6e962fd1",
|
|
||||||
"alias": "ophelia"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "02fd7a5f04d550cf0ba8af6053a20e0080d956f41b1221357a35fab3a363e5f78e",
|
|
||||||
"alias": "patrick"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "030da942ed7cfc7d3096811b3264e15115778e692eaacb2b7a76fb27a58cbb5359",
|
|
||||||
"alias": "quinn"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "0319d6b038e26ac89802e856d7e78f293e9d109c414614f98e3fa5c626f20934be",
|
|
||||||
"alias": "rick"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "03384439e78e87d168fecabe8d88218dfd5983c5e14fd8fa6dc89caeb3cc0fb171",
|
|
||||||
"alias": "sarah"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "0362002b8fbc1a799c839c8bcea43fce38a147467a00bc450414bbeab5c7a19efe",
|
|
||||||
"alias": "tim"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "0369bca64993fce966745d32c09b882f668958d9bd7aabb60ba35ef1884013be1d",
|
|
||||||
"alias": "ursula"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"source": false,
|
|
||||||
"pubkey": "0367cec75158a4129177bfb8b269cb586efe93d751b43800d456485e81c2620ca6",
|
|
||||||
"alias": "vincent"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"edges": [
|
|
||||||
{
|
|
||||||
"node_1": "021b96642e723592ee0b095983fe3a26c8b40b8926968d8b7510e51c9429d4562c",
|
|
||||||
"node_2": "022096b2b0ac083e708074a5ab57288bc821b6bef7b964185b307e073772c3748f",
|
|
||||||
"channel_id": 12345,
|
|
||||||
"channel_point": "99dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "022096b2b0ac083e708074a5ab57288bc821b6bef7b964185b307e073772c3748f",
|
|
||||||
"node_2": "022a190ce901ab2b6f349483f18b28a1d72c64a7bccb8057291f25784c0899840f",
|
|
||||||
"channel_id": 12346,
|
|
||||||
"channel_point": "79dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "022a190ce901ab2b6f349483f18b28a1d72c64a7bccb8057291f25784c0899840f",
|
|
||||||
"node_2": "022d855d09971dd047b7ecf929b23c6f147b568d4668af67fb2226eb8c15c4660d",
|
|
||||||
"channel_id": 12347,
|
|
||||||
"channel_point": "69dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "022d855d09971dd047b7ecf929b23c6f147b568d4668af67fb2226eb8c15c4660d",
|
|
||||||
"node_2": "024ca436834b0d38d9dc7ee4d95aa21db321c45598dc5921a4a52304a8e0dd2952",
|
|
||||||
"channel_id": 12348,
|
|
||||||
"channel_point": "59dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "024ca436834b0d38d9dc7ee4d95aa21db321c45598dc5921a4a52304a8e0dd2952",
|
|
||||||
"node_2": "025234a0c44cbf1b20c18e2c397107ad731376831e1c43ddb360b41dbb98c10266",
|
|
||||||
"channel_id": 12349,
|
|
||||||
"channel_point": "49dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "025234a0c44cbf1b20c18e2c397107ad731376831e1c43ddb360b41dbb98c10266",
|
|
||||||
"node_2": "0253e9d03030f2ff08d3a7f1d824ad6d8c0dae422f324e72d5bb313e3f2a2d45a8",
|
|
||||||
"channel_id": 12340,
|
|
||||||
"channel_point": "39dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "0253e9d03030f2ff08d3a7f1d824ad6d8c0dae422f324e72d5bb313e3f2a2d45a8",
|
|
||||||
"node_2": "0263d4f2baca258ff3bd5bce86c2754e95daaea27f268ae1a048c1253ff20de56e",
|
|
||||||
"channel_id": 12344,
|
|
||||||
"channel_point": "29dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "0263d4f2baca258ff3bd5bce86c2754e95daaea27f268ae1a048c1253ff20de56e",
|
|
||||||
"node_2": "02650db8e44302f75e265e9427264bc0d7e2337831d6b9ceb7c58ed1e725d4576a",
|
|
||||||
"channel_id": 12343,
|
|
||||||
"channel_point": "19dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "02650db8e44302f75e265e9427264bc0d7e2337831d6b9ceb7c58ed1e725d4576a",
|
|
||||||
"node_2": "02727bfd298aa055a6419404931dfc1ccb4f0eb7c9660a7df346b93d0025df3ba1",
|
|
||||||
"channel_id": 12342,
|
|
||||||
"channel_point": "88dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "02727bfd298aa055a6419404931dfc1ccb4f0eb7c9660a7df346b93d0025df3ba1",
|
|
||||||
"node_2": "0280c83b3eded413dcec12f7952410e2738f079bd9cbc9a7c462e32ed4d74bd5b7",
|
|
||||||
"channel_id": 12341,
|
|
||||||
"channel_point": "87dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "0280c83b3eded413dcec12f7952410e2738f079bd9cbc9a7c462e32ed4d74bd5b7",
|
|
||||||
"node_2": "0290bf454f4b95baf9227801301b331e35d477c6b6e7f36a599983ae58747b3828",
|
|
||||||
"channel_id": 12355,
|
|
||||||
"channel_point": "86dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "0290bf454f4b95baf9227801301b331e35d477c6b6e7f36a599983ae58747b3828",
|
|
||||||
"node_2": "0297c8de635d17e3dd5775edfa2797be0874c53b0026f69009787cecd2fa577de8",
|
|
||||||
"channel_id": 12365,
|
|
||||||
"channel_point": "85dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "0297c8de635d17e3dd5775edfa2797be0874c53b0026f69009787cecd2fa577de8",
|
|
||||||
"node_2": "02a27227113c71eab0c8609ac0cdc7e76791fc3163c16e643cb4658d1080c7e336",
|
|
||||||
"channel_id": 12375,
|
|
||||||
"channel_point": "84dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "02a27227113c71eab0c8609ac0cdc7e76791fc3163c16e643cb4658d1080c7e336",
|
|
||||||
"node_2": "02f5f6bb6373fc60528118003f803557b916fbecd90c3a0c5df4c86c6a6e962fd1",
|
|
||||||
"channel_id": 12385,
|
|
||||||
"channel_point": "83dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "02f5f6bb6373fc60528118003f803557b916fbecd90c3a0c5df4c86c6a6e962fd1",
|
|
||||||
"node_2": "02fd7a5f04d550cf0ba8af6053a20e0080d956f41b1221357a35fab3a363e5f78e",
|
|
||||||
"channel_id": 12395,
|
|
||||||
"channel_point": "82dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "02fd7a5f04d550cf0ba8af6053a20e0080d956f41b1221357a35fab3a363e5f78e",
|
|
||||||
"node_2": "030da942ed7cfc7d3096811b3264e15115778e692eaacb2b7a76fb27a58cbb5359",
|
|
||||||
"channel_id": 12305,
|
|
||||||
"channel_point": "81dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "030da942ed7cfc7d3096811b3264e15115778e692eaacb2b7a76fb27a58cbb5359",
|
|
||||||
"node_2": "0319d6b038e26ac89802e856d7e78f293e9d109c414614f98e3fa5c626f20934be",
|
|
||||||
"channel_id": 12335,
|
|
||||||
"channel_point": "80dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "0319d6b038e26ac89802e856d7e78f293e9d109c414614f98e3fa5c626f20934be",
|
|
||||||
"node_2": "03384439e78e87d168fecabe8d88218dfd5983c5e14fd8fa6dc89caeb3cc0fb171",
|
|
||||||
"channel_id": 12325,
|
|
||||||
"channel_point": "89ec56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "03384439e78e87d168fecabe8d88218dfd5983c5e14fd8fa6dc89caeb3cc0fb171",
|
|
||||||
"node_2": "0362002b8fbc1a799c839c8bcea43fce38a147467a00bc450414bbeab5c7a19efe",
|
|
||||||
"channel_id": 12315,
|
|
||||||
"channel_point": "89fc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "0362002b8fbc1a799c839c8bcea43fce38a147467a00bc450414bbeab5c7a19efe",
|
|
||||||
"node_2": "0369bca64993fce966745d32c09b882f668958d9bd7aabb60ba35ef1884013be1d",
|
|
||||||
"channel_id": 12445,
|
|
||||||
"channel_point": "89cc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"node_1": "0369bca64993fce966745d32c09b882f668958d9bd7aabb60ba35ef1884013be1d",
|
|
||||||
"node_2": "0367cec75158a4129177bfb8b269cb586efe93d751b43800d456485e81c2620ca6",
|
|
||||||
"channel_id": 12545,
|
|
||||||
"channel_point": "89bc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
|
||||||
"channel_flags": 0,
|
|
||||||
"message_flags": 1,
|
|
||||||
"expiry": 1,
|
|
||||||
"min_htlc": 1,
|
|
||||||
"max_htlc": 100000000,
|
|
||||||
"fee_base_msat": 10,
|
|
||||||
"fee_rate": 0.001,
|
|
||||||
"capacity": 100000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -4231,11 +4231,7 @@ func (r *rpcServer) GetNodeInfo(ctx context.Context,
|
|||||||
|
|
||||||
// First, parse the hex-encoded public key into a full in-memory public
|
// First, parse the hex-encoded public key into a full in-memory public
|
||||||
// key object we can work with for querying.
|
// key object we can work with for querying.
|
||||||
pubKeyBytes, err := hex.DecodeString(in.PubKey)
|
pubKey, err := route.NewVertexFromStr(in.PubKey)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
pubKey, err := btcec.ParsePubKey(pubKeyBytes, btcec.S256())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -4243,7 +4239,7 @@ func (r *rpcServer) GetNodeInfo(ctx context.Context,
|
|||||||
// With the public key decoded, attempt to fetch the node corresponding
|
// With the public key decoded, attempt to fetch the node corresponding
|
||||||
// to this public key. If the node cannot be found, then an error will
|
// to this public key. If the node cannot be found, then an error will
|
||||||
// be returned.
|
// be returned.
|
||||||
node, err := graph.FetchLightningNode(pubKey)
|
node, err := graph.FetchLightningNode(nil, pubKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -3369,7 +3369,12 @@ func computeNextBackoff(currBackoff time.Duration) time.Duration {
|
|||||||
|
|
||||||
// fetchNodeAdvertisedAddr attempts to fetch an advertised address of a node.
|
// fetchNodeAdvertisedAddr attempts to fetch an advertised address of a node.
|
||||||
func (s *server) fetchNodeAdvertisedAddr(pub *btcec.PublicKey) (net.Addr, error) {
|
func (s *server) fetchNodeAdvertisedAddr(pub *btcec.PublicKey) (net.Addr, error) {
|
||||||
node, err := s.chanDB.ChannelGraph().FetchLightningNode(pub)
|
vertex, err := route.NewVertexFromBytes(pub.SerializeCompressed())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
node, err := s.chanDB.ChannelGraph().FetchLightningNode(nil, vertex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrVarIntNotCanonical signals that the decoded varint was not minimally encoded.
|
// ErrVarIntNotCanonical signals that the decoded varint was not minimally encoded.
|
||||||
@ -107,3 +109,8 @@ func WriteVarInt(w io.Writer, val uint64, buf *[8]byte) error {
|
|||||||
_, err := w.Write(buf[:length])
|
_, err := w.Write(buf[:length])
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VarIntSize returns the required number of bytes to encode a var int.
|
||||||
|
func VarIntSize(val uint64) uint64 {
|
||||||
|
return uint64(wire.VarIntSerializeSize(val))
|
||||||
|
}
|
||||||
|
@ -1095,6 +1095,14 @@ func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if invoice.PaymentAddr != nil {
|
||||||
|
err := writeBytes32(
|
||||||
|
bufferBase32, fieldTypeS, *invoice.PaymentAddr,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if invoice.Features.SerializeSize32() > 0 {
|
if invoice.Features.SerializeSize32() > 0 {
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
err := invoice.Features.RawFeatureVector.EncodeBase32(&b)
|
err := invoice.Features.RawFeatureVector.EncodeBase32(&b)
|
||||||
@ -1107,14 +1115,6 @@ func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if invoice.PaymentAddr != nil {
|
|
||||||
err := writeBytes32(
|
|
||||||
bufferBase32, fieldTypeS, *invoice.PaymentAddr,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,13 @@ var (
|
|||||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
specPaymentAddr = [32]byte{
|
||||||
|
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
|
||||||
|
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
|
||||||
|
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
|
||||||
|
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
|
||||||
|
}
|
||||||
|
|
||||||
testEmptyString = ""
|
testEmptyString = ""
|
||||||
testCupOfCoffee = "1 cup coffee"
|
testCupOfCoffee = "1 cup coffee"
|
||||||
testCoffeeBeans = "coffee beans"
|
testCoffeeBeans = "coffee beans"
|
||||||
@ -499,8 +506,8 @@ func TestDecodeEncode(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// On mainnet, please send $30 coffee beans supporting
|
// On mainnet, please send $30 coffee beans supporting
|
||||||
// features 1 and 9.
|
// features 9, 15 and 99, using secret 0x11...
|
||||||
encodedInvoice: "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9qzsze992adudgku8p05pstl6zh7av6rx2f297pv89gu5q93a0hf3g7lynl3xq56t23dpvah6u7y9qey9lccrdml3gaqwc6nxsl5ktzm464sq73t7cl",
|
encodedInvoice: "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q5sqqqqqqqqqqqqqqqpqsq67gye39hfg3zd8rgc80k32tvy9xk2xunwm5lzexnvpx6fd77en8qaq424dxgt56cag2dpt359k3ssyhetktkpqh24jqnjyw6uqd08sgptq44qu",
|
||||||
valid: true,
|
valid: true,
|
||||||
decodedInvoice: func() *Invoice {
|
decodedInvoice: func() *Invoice {
|
||||||
return &Invoice{
|
return &Invoice{
|
||||||
@ -508,10 +515,11 @@ func TestDecodeEncode(t *testing.T) {
|
|||||||
MilliSat: &testMillisat25mBTC,
|
MilliSat: &testMillisat25mBTC,
|
||||||
Timestamp: time.Unix(1496314658, 0),
|
Timestamp: time.Unix(1496314658, 0),
|
||||||
PaymentHash: &testPaymentHash,
|
PaymentHash: &testPaymentHash,
|
||||||
|
PaymentAddr: &specPaymentAddr,
|
||||||
Description: &testCoffeeBeans,
|
Description: &testCoffeeBeans,
|
||||||
Destination: testPubKey,
|
Destination: testPubKey,
|
||||||
Features: lnwire.NewFeatureVector(
|
Features: lnwire.NewFeatureVector(
|
||||||
lnwire.NewRawFeatureVector(1, 9),
|
lnwire.NewRawFeatureVector(9, 15, 99),
|
||||||
lnwire.Features,
|
lnwire.Features,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@ -525,8 +533,8 @@ func TestDecodeEncode(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// On mainnet, please send $30 coffee beans supporting
|
// On mainnet, please send $30 coffee beans supporting
|
||||||
// features 1, 9, and 100.
|
// features 9, 15, 99, and 100, using secret 0x11...
|
||||||
encodedInvoice: "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9q4pqqqqqqqqqqqqqqqqqqszk3ed62snp73037h4py4gry05eltlp0uezm2w9ajnerhmxzhzhsu40g9mgyx5v3ad4aqwkmvyftzk4k9zenz90mhjcy9hcevc7r3lx2sphzfxz7",
|
encodedInvoice: "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdeessp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9q4psqqqqqqqqqqqqqqqpqqqqu7fz6pjqczdm3jp3qps7xntj2w2mm70e0ckhw3c5xk9p36pvk3sewn7ncaex6uzfq0vtqzy28se6pcwn790vxex7xystzumhg55p6qq9wq7td",
|
||||||
valid: false,
|
valid: false,
|
||||||
skipEncoding: true,
|
skipEncoding: true,
|
||||||
decodedInvoice: func() *Invoice {
|
decodedInvoice: func() *Invoice {
|
||||||
@ -535,10 +543,11 @@ func TestDecodeEncode(t *testing.T) {
|
|||||||
MilliSat: &testMillisat25mBTC,
|
MilliSat: &testMillisat25mBTC,
|
||||||
Timestamp: time.Unix(1496314658, 0),
|
Timestamp: time.Unix(1496314658, 0),
|
||||||
PaymentHash: &testPaymentHash,
|
PaymentHash: &testPaymentHash,
|
||||||
|
PaymentAddr: &specPaymentAddr,
|
||||||
Description: &testCoffeeBeans,
|
Description: &testCoffeeBeans,
|
||||||
Destination: testPubKey,
|
Destination: testPubKey,
|
||||||
Features: lnwire.NewFeatureVector(
|
Features: lnwire.NewFeatureVector(
|
||||||
lnwire.NewRawFeatureVector(1, 9, 100),
|
lnwire.NewRawFeatureVector(9, 15, 99, 100),
|
||||||
lnwire.Features,
|
lnwire.Features,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user