Merge pull request #9692 from lightningnetwork/elle-graph

[graph-work-side-branch]: side branch for graph work
This commit is contained in:
Oliver Gugger authored and GitHub committed 2025-05-23 10:29:09 +02:00
commit cbdd1c22b7
75 files changed
+3106 -1492

No files matched your search

+2
View File
@@ -222,6 +222,8 @@ jobs:
- unit tags="kvdb_etcd"
- unit tags="kvdb_postgres"
- unit tags="kvdb_sqlite"
- unit tags="test_db_sqlite"
- unit tags="test_db_postgres"
- unit-race
- unit-module
+23 -11
View File
@@ -2,6 +2,7 @@ package autopilot
import (
"bytes"
"context"
"fmt"
"math/rand"
"net"
@@ -11,6 +12,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/lnwire"
)
@@ -166,8 +168,9 @@ type Agent struct {
pendingOpens map[NodeID]LocalChannel
pendingMtx sync.Mutex
quit chan struct{}
wg sync.WaitGroup
quit chan struct{}
wg sync.WaitGroup
cancel fn.Option[context.CancelFunc]
}
// New creates a new instance of the Agent instantiated using the passed
@@ -202,17 +205,20 @@ func New(cfg Config, initialState []LocalChannel) (*Agent, error) {
func (a *Agent) Start() error {
var err error
a.started.Do(func() {
err = a.start()
ctx, cancel := context.WithCancel(context.Background())
a.cancel = fn.Some(cancel)
err = a.start(ctx)
})
return err
}
func (a *Agent) start() error {
func (a *Agent) start(ctx context.Context) error {
rand.Seed(time.Now().Unix())
log.Infof("Autopilot Agent starting")
a.wg.Add(1)
go a.controller()
go a.controller(ctx)
return nil
}
@@ -230,6 +236,7 @@ func (a *Agent) Stop() error {
func (a *Agent) stop() error {
log.Infof("Autopilot Agent stopping")
a.cancel.WhenSome(func(fn context.CancelFunc) { fn() })
close(a.quit)
a.wg.Wait()
@@ -401,7 +408,7 @@ func mergeChanState(pendingChans map[NodeID]LocalChannel,
// and external state changes as a result of decisions it makes w.r.t channel
// allocation, or attributes affecting its control loop being updated by the
// backing Lightning Node.
func (a *Agent) controller() {
func (a *Agent) controller(ctx context.Context) {
defer a.wg.Done()
// We'll start off by assigning our starting balance, and injecting
@@ -502,6 +509,9 @@ func (a *Agent) controller() {
// immediately.
case <-a.quit:
return
case <-ctx.Done():
return
}
a.pendingMtx.Lock()
@@ -539,7 +549,7 @@ func (a *Agent) controller() {
log.Infof("Triggering attachment directive dispatch, "+
"total_funds=%v", a.totalBalance)
err := a.openChans(availableFunds, numChans, totalChans)
err := a.openChans(ctx, availableFunds, numChans, totalChans)
if err != nil {
log.Errorf("Unable to open channels: %v", err)
}
@@ -548,8 +558,8 @@ func (a *Agent) controller() {
// openChans queries the agent's heuristic for a set of channel candidates, and
// attempts to open channels to them.
func (a *Agent) openChans(availableFunds btcutil.Amount, numChans uint32,
totalChans []LocalChannel) error {
func (a *Agent) openChans(ctx context.Context, availableFunds btcutil.Amount,
numChans uint32, totalChans []LocalChannel) error {
// As channel size we'll use the maximum channel size available.
chanSize := a.cfg.Constraints.MaxChanSize()
@@ -598,7 +608,9 @@ func (a *Agent) openChans(availableFunds btcutil.Amount, numChans uint32,
selfPubBytes := a.cfg.Self.SerializeCompressed()
nodes := make(map[NodeID]struct{})
addresses := make(map[NodeID][]net.Addr)
if err := a.cfg.Graph.ForEachNode(func(node Node) error {
if err := a.cfg.Graph.ForEachNode(ctx, func(_ context.Context,
node Node) error {
nID := NodeID(node.PubKey())
// If we come across ourselves, them we'll continue in
@@ -636,7 +648,7 @@ func (a *Agent) openChans(availableFunds btcutil.Amount, numChans uint32,
// graph.
log.Debugf("Scoring %d nodes for chan_size=%v", len(nodes), chanSize)
scores, err := a.cfg.Heuristic.NodeScores(
a.cfg.Graph, totalChans, chanSize, nodes,
ctx, a.cfg.Graph, totalChans, chanSize, nodes,
)
if err != nil {
return fmt.Errorf("unable to calculate node scores : %w", err)
+4 -3
View File
@@ -1,6 +1,7 @@
package autopilot
import (
"context"
"errors"
"fmt"
"net"
@@ -85,9 +86,9 @@ func (m *mockHeuristic) Name() string {
return "mock"
}
func (m *mockHeuristic) NodeScores(g ChannelGraph, chans []LocalChannel,
chanSize btcutil.Amount, nodes map[NodeID]struct{}) (
map[NodeID]*NodeScore, error) {
func (m *mockHeuristic) NodeScores(_ context.Context, g ChannelGraph,
chans []LocalChannel, chanSize btcutil.Amount,
nodes map[NodeID]struct{}) (map[NodeID]*NodeScore, error) {
if m.nodeScoresArgs != nil {
directive := directiveArg{
+5 -2
View File
@@ -1,6 +1,7 @@
package autopilot
import (
"context"
"fmt"
"sync"
)
@@ -168,8 +169,10 @@ func betweennessCentrality(g *SimpleGraph, s int, centrality []float64) {
}
// Refresh recalculates and stores centrality values.
func (bc *BetweennessCentrality) Refresh(graph ChannelGraph) error {
cache, err := NewSimpleGraph(graph)
func (bc *BetweennessCentrality) Refresh(ctx context.Context,
graph ChannelGraph) error {
cache, err := NewSimpleGraph(ctx, graph)
if err != nil {
return err
}
+9 -2
View File
@@ -1,6 +1,7 @@
package autopilot
import (
"context"
"fmt"
"testing"
@@ -30,6 +31,9 @@ func TestBetweennessCentralityMetricConstruction(t *testing.T) {
// Tests that empty graph results in empty centrality result.
func TestBetweennessCentralityEmptyGraph(t *testing.T) {
t.Parallel()
ctx := context.Background()
centralityMetric, err := NewBetweennessCentralityMetric(1)
require.NoError(
t, err,
@@ -42,7 +46,7 @@ func TestBetweennessCentralityEmptyGraph(t *testing.T) {
require.NoError(t, err, "unable to create graph")
success := t.Run(chanGraph.name, func(t1 *testing.T) {
err = centralityMetric.Refresh(graph)
err = centralityMetric.Refresh(ctx, graph)
require.NoError(t1, err)
centrality := centralityMetric.GetMetric(false)
@@ -59,6 +63,9 @@ func TestBetweennessCentralityEmptyGraph(t *testing.T) {
// Test betweenness centrality calculating using an example graph.
func TestBetweennessCentralityWithNonEmptyGraph(t *testing.T) {
t.Parallel()
ctx := context.Background()
workers := []int{1, 3, 9, 100}
tests := []struct {
@@ -100,7 +107,7 @@ func TestBetweennessCentralityWithNonEmptyGraph(t *testing.T) {
t1, graph, centralityTestGraph,
)
err = metric.Refresh(graph)
err = metric.Refresh(ctx, graph)
require.NoError(t1, err)
for _, expected := range tests {
+5 -4
View File
@@ -1,6 +1,7 @@
package autopilot
import (
"context"
"fmt"
"github.com/btcsuite/btcd/btcutil"
@@ -70,9 +71,9 @@ func (c *WeightedCombAttachment) Name() string {
// is the maximum possible improvement in connectivity.
//
// NOTE: This is a part of the AttachmentHeuristic interface.
func (c *WeightedCombAttachment) NodeScores(g ChannelGraph, chans []LocalChannel,
chanSize btcutil.Amount, nodes map[NodeID]struct{}) (
map[NodeID]*NodeScore, error) {
func (c *WeightedCombAttachment) NodeScores(ctx context.Context, g ChannelGraph,
chans []LocalChannel, chanSize btcutil.Amount,
nodes map[NodeID]struct{}) (map[NodeID]*NodeScore, error) {
// We now query each heuristic to determine the score they give to the
// nodes for the given channel size.
@@ -81,7 +82,7 @@ func (c *WeightedCombAttachment) NodeScores(g ChannelGraph, chans []LocalChannel
log.Tracef("Getting scores from sub heuristic %v", h.Name())
s, err := h.NodeScores(
g, chans, chanSize, nodes,
ctx, g, chans, chanSize, nodes,
)
if err != nil {
return nil, fmt.Errorf("unable to get sub score: %w",
+4 -3
View File
@@ -1,6 +1,7 @@
package autopilot
import (
"context"
"fmt"
"sync"
@@ -80,9 +81,9 @@ func (s *ExternalScoreAttachment) SetNodeScores(targetHeuristic string,
// not known will get a score of 0.
//
// NOTE: This is a part of the AttachmentHeuristic interface.
func (s *ExternalScoreAttachment) NodeScores(g ChannelGraph, chans []LocalChannel,
chanSize btcutil.Amount, nodes map[NodeID]struct{}) (
map[NodeID]*NodeScore, error) {
func (s *ExternalScoreAttachment) NodeScores(_ context.Context, g ChannelGraph,
chans []LocalChannel, chanSize btcutil.Amount,
nodes map[NodeID]struct{}) (map[NodeID]*NodeScore, error) {
existingPeers := make(map[NodeID]struct{})
for _, c := range chans {
+3 -1
View File
@@ -1,6 +1,7 @@
package autopilot_test
import (
"context"
"testing"
"github.com/btcsuite/btcd/btcec/v2"
@@ -22,6 +23,7 @@ func randKey() (*btcec.PublicKey, error) {
// ExternalScoreAttachment correctly reflects the scores we set last.
func TestSetNodeScores(t *testing.T) {
t.Parallel()
ctx := context.Background()
const name = "externalscore"
@@ -62,7 +64,7 @@ func TestSetNodeScores(t *testing.T) {
q[nID] = struct{}{}
}
resp, err := h.NodeScores(
nil, nil, btcutil.Amount(btcutil.SatoshiPerBitcoin), q,
ctx, nil, nil, btcutil.Amount(btcutil.SatoshiPerBitcoin), q,
)
if err != nil {
t.Fatal(err)
+23 -10
View File
@@ -1,6 +1,7 @@
package autopilot
import (
"context"
"encoding/hex"
"net"
"sort"
@@ -80,7 +81,9 @@ func (d *dbNode) Addrs() []net.Addr {
// describes the active channel.
//
// NOTE: Part of the autopilot.Node interface.
func (d *dbNode) ForEachChannel(cb func(ChannelEdge) error) error {
func (d *dbNode) ForEachChannel(ctx context.Context,
cb func(context.Context, ChannelEdge) error) error {
return d.tx.ForEachChannel(func(ei *models.ChannelEdgeInfo, ep,
_ *models.ChannelEdgePolicy) error {
@@ -108,7 +111,7 @@ func (d *dbNode) ForEachChannel(cb func(ChannelEdge) error) error {
},
}
return cb(edge)
return cb(ctx, edge)
})
}
@@ -117,7 +120,9 @@ func (d *dbNode) ForEachChannel(cb func(ChannelEdge) error) error {
// error, then execution should be terminated.
//
// NOTE: Part of the autopilot.ChannelGraph interface.
func (d *databaseChannelGraph) ForEachNode(cb func(Node) error) error {
func (d *databaseChannelGraph) ForEachNode(ctx context.Context,
cb func(context.Context, Node) error) error {
return d.db.ForEachNode(func(nodeTx graphdb.NodeRTx) error {
// We'll skip over any node that doesn't have any advertised
// addresses. As we won't be able to reach them to actually
@@ -129,7 +134,8 @@ func (d *databaseChannelGraph) ForEachNode(cb func(Node) error) error {
node := &dbNode{
tx: nodeTx,
}
return cb(node)
return cb(ctx, node)
})
}
@@ -185,7 +191,9 @@ func (nc dbNodeCached) Addrs() []net.Addr {
// describes the active channel.
//
// NOTE: Part of the autopilot.Node interface.
func (nc dbNodeCached) ForEachChannel(cb func(ChannelEdge) error) error {
func (nc dbNodeCached) ForEachChannel(ctx context.Context,
cb func(context.Context, ChannelEdge) error) error {
for cid, channel := range nc.channels {
edge := ChannelEdge{
ChanID: lnwire.NewShortChanIDFromInt(cid),
@@ -195,7 +203,7 @@ func (nc dbNodeCached) ForEachChannel(cb func(ChannelEdge) error) error {
},
}
if err := cb(edge); err != nil {
if err := cb(ctx, edge); err != nil {
return err
}
}
@@ -208,7 +216,9 @@ func (nc dbNodeCached) ForEachChannel(cb func(ChannelEdge) error) error {
// error, then execution should be terminated.
//
// NOTE: Part of the autopilot.ChannelGraph interface.
func (dc *databaseChannelGraphCached) ForEachNode(cb func(Node) error) error {
func (dc *databaseChannelGraphCached) ForEachNode(ctx context.Context,
cb func(context.Context, Node) error) error {
return dc.db.ForEachNodeCached(func(n route.Vertex,
channels map[uint64]*graphdb.DirectedChannel) error {
@@ -217,7 +227,8 @@ func (dc *databaseChannelGraphCached) ForEachNode(cb func(Node) error) error {
node: n,
channels: channels,
}
return cb(node)
return cb(ctx, node)
}
return nil
})
@@ -262,9 +273,11 @@ func (m memNode) Addrs() []net.Addr {
// describes the active channel.
//
// NOTE: Part of the autopilot.Node interface.
func (m memNode) ForEachChannel(cb func(ChannelEdge) error) error {
func (m memNode) ForEachChannel(ctx context.Context,
cb func(context.Context, ChannelEdge) error) error {
for _, channel := range m.chans {
if err := cb(channel); err != nil {
if err := cb(ctx, channel); err != nil {
return err
}
}
+6 -4
View File
@@ -1,6 +1,7 @@
package autopilot
import (
"context"
"net"
"github.com/btcsuite/btcd/btcec/v2"
@@ -35,7 +36,8 @@ type Node interface {
// iterate through all edges emanating from/to the target node. For
// each active channel, this function should be called with the
// populated ChannelEdge that describes the active channel.
ForEachChannel(func(ChannelEdge) error) error
ForEachChannel(context.Context, func(context.Context,
ChannelEdge) error) error
}
// LocalChannel is a simple struct which contains relevant details of a
@@ -83,7 +85,7 @@ type ChannelGraph interface {
// ForEachNode is a higher-order function that should be called once
// for each connected node within the channel graph. If the passed
// callback returns an error, then execution should be terminated.
ForEachNode(func(Node) error) error
ForEachNode(context.Context, func(context.Context, Node) error) error
}
// NodeScore is a tuple mapping a NodeID to a score indicating the preference
@@ -142,7 +144,7 @@ type AttachmentHeuristic interface {
//
// NOTE: A NodeID not found in the returned map is implicitly given a
// score of 0.
NodeScores(g ChannelGraph, chans []LocalChannel,
NodeScores(ctx context.Context, g ChannelGraph, chans []LocalChannel,
chanSize btcutil.Amount, nodes map[NodeID]struct{}) (
map[NodeID]*NodeScore, error)
}
@@ -155,7 +157,7 @@ type NodeMetric interface {
Name() string
// Refresh refreshes the metric values based on the current graph.
Refresh(graph ChannelGraph) error
Refresh(ctx context.Context, graph ChannelGraph) error
// GetMetric returns the latest value of this metric. Values in the
// map are per node and can be in arbitrary domain. If normalize is
+8 -6
View File
@@ -1,6 +1,7 @@
package autopilot
import (
"context"
"fmt"
"sync"
@@ -266,8 +267,8 @@ func (m *Manager) StopAgent() error {
}
// QueryHeuristics queries the available autopilot heuristics for node scores.
func (m *Manager) QueryHeuristics(nodes []NodeID, localState bool) (
HeuristicScores, error) {
func (m *Manager) QueryHeuristics(ctx context.Context, nodes []NodeID,
localState bool) (HeuristicScores, error) {
m.Lock()
defer m.Unlock()
@@ -278,7 +279,8 @@ func (m *Manager) QueryHeuristics(nodes []NodeID, localState bool) (
}
log.Debugf("Querying heuristics for %d nodes", len(n))
return m.queryHeuristics(n, localState)
return m.queryHeuristics(ctx, n, localState)
}
// HeuristicScores is an alias for a map that maps heuristic names to a map of
@@ -289,8 +291,8 @@ type HeuristicScores map[string]map[NodeID]float64
// the agent's current active heuristic.
//
// NOTE: Must be called with the manager's lock.
func (m *Manager) queryHeuristics(nodes map[NodeID]struct{}, localState bool) (
HeuristicScores, error) {
func (m *Manager) queryHeuristics(ctx context.Context,
nodes map[NodeID]struct{}, localState bool) (HeuristicScores, error) {
// If we want to take the local state into action when querying the
// heuristics, we fetch it. If not we'll just pass an empty slice to
@@ -338,7 +340,7 @@ func (m *Manager) queryHeuristics(nodes map[NodeID]struct{}, localState bool) (
}
s, err := h.NodeScores(
m.cfg.PilotCfg.Graph, totalChans, chanSize, nodes,
ctx, m.cfg.PilotCfg.Graph, totalChans, chanSize, nodes,
)
if err != nil {
return nil, fmt.Errorf("unable to get sub score: %w",
+15 -8
View File
@@ -1,6 +1,7 @@
package autopilot
import (
"context"
prand "math/rand"
"time"
@@ -78,9 +79,9 @@ func (p *PrefAttachment) Name() string {
// given to nodes already having high connectivity in the graph.
//
// NOTE: This is a part of the AttachmentHeuristic interface.
func (p *PrefAttachment) NodeScores(g ChannelGraph, chans []LocalChannel,
chanSize btcutil.Amount, nodes map[NodeID]struct{}) (
map[NodeID]*NodeScore, error) {
func (p *PrefAttachment) NodeScores(ctx context.Context, g ChannelGraph,
chans []LocalChannel, chanSize btcutil.Amount,
nodes map[NodeID]struct{}) (map[NodeID]*NodeScore, error) {
// We first run though the graph once in order to find the median
// channel size.
@@ -88,8 +89,10 @@ func (p *PrefAttachment) NodeScores(g ChannelGraph, chans []LocalChannel,
allChans []btcutil.Amount
seenChans = make(map[uint64]struct{})
)
if err := g.ForEachNode(func(n Node) error {
err := n.ForEachChannel(func(e ChannelEdge) error {
if err := g.ForEachNode(ctx, func(ctx context.Context, n Node) error {
err := n.ForEachChannel(ctx, func(_ context.Context,
e ChannelEdge) error {
if _, ok := seenChans[e.ChanID.ToUint64()]; ok {
return nil
}
@@ -114,15 +117,19 @@ func (p *PrefAttachment) NodeScores(g ChannelGraph, chans []LocalChannel,
// the graph.
var maxChans int
nodeChanNum := make(map[NodeID]int)
if err := g.ForEachNode(func(n Node) error {
if err := g.ForEachNode(ctx, func(ctx context.Context, n Node) error {
var nodeChans int
err := n.ForEachChannel(func(e ChannelEdge) error {
err := n.ForEachChannel(ctx, func(_ context.Context,
e ChannelEdge) error {
// Since connecting to nodes with a lot of small
// channels actually worsens our connectivity in the
// graph (we will potentially waste time trying to use
// these useless channels in path finding), we decrease
// the counter for such channels.
if e.Capacity < medianChanSize/minMedianChanSizeFraction {
if e.Capacity <
medianChanSize/minMedianChanSizeFraction {
nodeChans--
return nil
}
+48 -45
View File
@@ -2,6 +2,7 @@ package autopilot
import (
"bytes"
"context"
"errors"
prand "math/rand"
"net"
@@ -13,7 +14,6 @@ import (
"github.com/btcsuite/btcd/btcutil"
graphdb "github.com/lightningnetwork/lnd/graph/db"
"github.com/lightningnetwork/lnd/graph/db/models"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
@@ -36,19 +36,7 @@ type testDBGraph struct {
}
func newDiskChanGraph(t *testing.T) (testGraph, error) {
backend, err := kvdb.GetBoltBackend(&kvdb.BoltBackendConfig{
DBPath: t.TempDir(),
DBFileName: "graph.db",
NoFreelistSync: true,
AutoCompact: false,
AutoCompactMinAge: kvdb.DefaultBoltAutoCompactMinAge,
DBTimeout: kvdb.DefaultDBTimeout,
})
require.NoError(t, err)
graphDB, err := graphdb.NewChannelGraph(&graphdb.Config{KVDB: backend})
require.NoError(t, err)
graphDB := graphdb.MakeTestGraph(t)
require.NoError(t, graphDB.Start())
t.Cleanup(func() {
require.NoError(t, graphDB.Stop())
@@ -87,6 +75,8 @@ var chanGraphs = []struct {
// TestPrefAttachmentSelectEmptyGraph ensures that when passed an
// empty graph, the NodeSores function always returns a score of 0.
func TestPrefAttachmentSelectEmptyGraph(t *testing.T) {
t.Parallel()
ctx := context.Background()
prefAttach := NewPrefAttachment()
// Create a random public key, which we will query to get a score for.
@@ -107,7 +97,7 @@ func TestPrefAttachmentSelectEmptyGraph(t *testing.T) {
// attempt to get the score for this one node.
const walletFunds = btcutil.SatoshiPerBitcoin
scores, err := prefAttach.NodeScores(
graph, nil, walletFunds, nodes,
ctx, graph, nil, walletFunds, nodes,
)
require.NoError(t1, err)
@@ -126,6 +116,7 @@ func TestPrefAttachmentSelectEmptyGraph(t *testing.T) {
// and the funds are appropriately allocated across each peer.
func TestPrefAttachmentSelectTwoVertexes(t *testing.T) {
t.Parallel()
ctx := context.Background()
prand.Seed(time.Now().Unix())
@@ -156,10 +147,12 @@ func TestPrefAttachmentSelectTwoVertexes(t *testing.T) {
// Get the score for all nodes found in the graph at
// this point.
nodes := make(map[NodeID]struct{})
err = graph.ForEachNode(func(n Node) error {
nodes[n.PubKey()] = struct{}{}
return nil
})
err = graph.ForEachNode(ctx,
func(_ context.Context, n Node) error {
nodes[n.PubKey()] = struct{}{}
return nil
},
)
require.NoError(t1, err)
require.Len(t1, nodes, 3)
@@ -168,7 +161,7 @@ func TestPrefAttachmentSelectTwoVertexes(t *testing.T) {
// attempt to get our candidates channel score given
// the current state of the graph.
candidates, err := prefAttach.NodeScores(
graph, nil, maxChanSize, nodes,
ctx, graph, nil, maxChanSize, nodes,
)
require.NoError(t1, err)
@@ -207,6 +200,7 @@ func TestPrefAttachmentSelectTwoVertexes(t *testing.T) {
// allocate all funds to each vertex (up to the max channel size).
func TestPrefAttachmentSelectGreedyAllocation(t *testing.T) {
t.Parallel()
ctx := context.Background()
prand.Seed(time.Now().Unix())
@@ -245,22 +239,25 @@ func TestPrefAttachmentSelectGreedyAllocation(t *testing.T) {
numNodes := 0
twoChans := false
nodes := make(map[NodeID]struct{})
err = graph.ForEachNode(func(n Node) error {
numNodes++
nodes[n.PubKey()] = struct{}{}
numChans := 0
err := n.ForEachChannel(func(c ChannelEdge) error {
numChans++
err = graph.ForEachNode(
ctx, func(ctx context.Context, n Node) error {
numNodes++
nodes[n.PubKey()] = struct{}{}
numChans := 0
err := n.ForEachChannel(ctx,
func(_ context.Context, c ChannelEdge) error { //nolint:ll
numChans++
return nil
},
)
if err != nil {
return err
}
twoChans = twoChans || (numChans == 2)
return nil
})
if err != nil {
return err
}
twoChans = twoChans || (numChans == 2)
return nil
})
require.NoError(t1, err)
require.EqualValues(t1, 3, numNodes)
@@ -272,7 +269,7 @@ func TestPrefAttachmentSelectGreedyAllocation(t *testing.T) {
// result, the heuristic should try to greedily
// allocate funds to channels.
scores, err := prefAttach.NodeScores(
graph, nil, maxChanSize, nodes,
ctx, graph, nil, maxChanSize, nodes,
)
require.NoError(t1, err)
@@ -290,7 +287,7 @@ func TestPrefAttachmentSelectGreedyAllocation(t *testing.T) {
// candidates of that size.
const remBalance = btcutil.SatoshiPerBitcoin * 0.5
scores, err = prefAttach.NodeScores(
graph, nil, remBalance, nodes,
ctx, graph, nil, remBalance, nodes,
)
require.NoError(t1, err)
@@ -313,6 +310,7 @@ func TestPrefAttachmentSelectGreedyAllocation(t *testing.T) {
// of zero during scoring.
func TestPrefAttachmentSelectSkipNodes(t *testing.T) {
t.Parallel()
ctx := context.Background()
prand.Seed(time.Now().Unix())
@@ -335,10 +333,13 @@ func TestPrefAttachmentSelectSkipNodes(t *testing.T) {
require.NoError(t1, err)
nodes := make(map[NodeID]struct{})
err = graph.ForEachNode(func(n Node) error {
nodes[n.PubKey()] = struct{}{}
return nil
})
err = graph.ForEachNode(
ctx, func(_ context.Context, n Node) error {
nodes[n.PubKey()] = struct{}{}
return nil
},
)
require.NoError(t1, err)
require.Len(t1, nodes, 2)
@@ -346,7 +347,7 @@ func TestPrefAttachmentSelectSkipNodes(t *testing.T) {
// With our graph created, we'll now get the scores for
// all nodes in the graph.
scores, err := prefAttach.NodeScores(
graph, nil, maxChanSize, nodes,
ctx, graph, nil, maxChanSize, nodes,
)
require.NoError(t1, err)
@@ -374,7 +375,7 @@ func TestPrefAttachmentSelectSkipNodes(t *testing.T) {
// then all nodes should have a score of zero, since we
// already got channels to them.
scores, err = prefAttach.NodeScores(
graph, chans, maxChanSize, nodes,
ctx, graph, chans, maxChanSize, nodes,
)
require.NoError(t1, err)
@@ -583,9 +584,11 @@ func newMemChannelGraph() *memChannelGraph {
// error, then execution should be terminated.
//
// NOTE: Part of the autopilot.ChannelGraph interface.
func (m *memChannelGraph) ForEachNode(cb func(Node) error) error {
func (m *memChannelGraph) ForEachNode(ctx context.Context,
cb func(context.Context, Node) error) error {
for _, node := range m.graph {
if err := cb(node); err != nil {
if err := cb(ctx, node); err != nil {
return err
}
}
@@ -720,7 +723,7 @@ func (t *testNodeTx) Node() *models.LightningNode {
func (t *testNodeTx) ForEachChannel(f func(*models.ChannelEdgeInfo,
*models.ChannelEdgePolicy, *models.ChannelEdgePolicy) error) error {
return t.db.db.ForEachNodeChannel(t.node.PubKeyBytes, func(_ kvdb.RTx,
return t.db.db.ForEachNodeChannel(t.node.PubKeyBytes, func(
edge *models.ChannelEdgeInfo, policy1,
policy2 *models.ChannelEdgePolicy) error {
+11
View File
@@ -0,0 +1,11 @@
package autopilot
import (
"testing"
"github.com/lightningnetwork/lnd/kvdb"
)
func TestMain(m *testing.M) {
kvdb.RunTests(m)
}
+16 -9
View File
@@ -1,5 +1,7 @@
package autopilot
import "context"
// diameterCutoff is used to discard nodes in the diameter calculation.
// It is the multiplier for the eccentricity of the highest-degree node,
// serving as a cutoff to discard all nodes with a smaller hop distance. This
@@ -20,7 +22,7 @@ type SimpleGraph struct {
// NewSimpleGraph creates a simplified graph from the current channel graph.
// Returns an error if the channel graph iteration fails due to underlying
// failure.
func NewSimpleGraph(g ChannelGraph) (*SimpleGraph, error) {
func NewSimpleGraph(ctx context.Context, g ChannelGraph) (*SimpleGraph, error) {
nodes := make(map[NodeID]int)
adj := make(map[int][]int)
nextIndex := 0
@@ -42,17 +44,22 @@ func NewSimpleGraph(g ChannelGraph) (*SimpleGraph, error) {
return nodeIndex
}
// Iterate over each node and each channel and update the adj and the node
// index.
err := g.ForEachNode(func(node Node) error {
// Iterate over each node and each channel and update the adj and the
// node index.
err := g.ForEachNode(ctx, func(ctx context.Context, node Node) error {
u := getNodeIndex(node)
return node.ForEachChannel(func(edge ChannelEdge) error {
v := getNodeIndex(edge.Peer)
return node.ForEachChannel(
ctx, func(_ context.Context,
edge ChannelEdge) error {
adj[u] = append(adj[u], v)
return nil
})
v := getNodeIndex(edge.Peer)
adj[u] = append(adj[u], v)
return nil
},
)
})
if err != nil {
return nil, err
+5 -4
View File
@@ -1,6 +1,7 @@
package autopilot
import (
"context"
"runtime"
"github.com/btcsuite/btcd/btcutil"
@@ -50,12 +51,12 @@ func (g *TopCentrality) Name() string {
// As our current implementation of betweenness centrality is non-incremental,
// NodeScores will recalculate the centrality values on every call, which is
// slow for large graphs.
func (g *TopCentrality) NodeScores(graph ChannelGraph, chans []LocalChannel,
chanSize btcutil.Amount, nodes map[NodeID]struct{}) (
map[NodeID]*NodeScore, error) {
func (g *TopCentrality) NodeScores(ctx context.Context, graph ChannelGraph,
chans []LocalChannel, chanSize btcutil.Amount,
nodes map[NodeID]struct{}) (map[NodeID]*NodeScore, error) {
// Calculate betweenness centrality for the whole graph.
if err := g.centralityMetric.Refresh(graph); err != nil {
if err := g.centralityMetric.Refresh(ctx, graph); err != nil {
return nil, err
}
+2 -1
View File
@@ -1,6 +1,7 @@
package autopilot
import (
"context"
"testing"
"github.com/btcsuite/btcd/btcec/v2"
@@ -58,7 +59,7 @@ func testTopCentrality(t *testing.T, graph testGraph,
// Attempt to get centrality scores and expect
// that the result equals with the expected set.
scores, err := topCentrality.NodeScores(
graph, channels, chanSize, nodes,
context.Background(), graph, channels, chanSize, nodes,
)
require.NoError(t, err)
+28 -12
View File
@@ -1,10 +1,10 @@
package batch
import (
"context"
"errors"
"sync"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/sqldb"
)
@@ -14,28 +14,44 @@ var errSolo = errors.New(
"batch function returned an error and should be re-run solo",
)
type request struct {
*Request
// txOpts implements the sqldb.TxOptions interface. It is used to indicate that
// the transaction can be read-only or not transaction.
type txOpts struct {
readOnly bool
}
// ReadOnly returns true if the transaction should be read only.
//
// NOTE: This is part of the sqldb.TxOptions interface.
func (t *txOpts) ReadOnly() bool {
return t.readOnly
}
type request[Q any] struct {
*Request[Q]
errChan chan error
}
type batch struct {
db kvdb.Backend
type batch[Q any] struct {
db sqldb.BatchedTx[Q]
start sync.Once
reqs []*request
clear func(b *batch)
reqs []*request[Q]
clear func(b *batch[Q])
locker sync.Locker
txOpts txOpts
}
// trigger is the entry point for the batch and ensures that run is started at
// most once.
func (b *batch) trigger() {
b.start.Do(b.run)
func (b *batch[Q]) trigger(ctx context.Context) {
b.start.Do(func() {
b.run(ctx)
})
}
// run executes the current batch of requests. If any individual requests fail
// alongside others they will be retried by the caller.
func (b *batch) run() {
func (b *batch[Q]) run(ctx context.Context) {
// Clear the batch from its scheduler, ensuring that no new requests are
// added to this batch.
b.clear(b)
@@ -52,9 +68,9 @@ func (b *batch) run() {
// that fail will be retried individually.
for len(b.reqs) > 0 {
var failIdx = -1
err := kvdb.Update(b.db, func(tx kvdb.RwTx) error {
err := b.db.ExecTx(ctx, &b.txOpts, func(tx Q) error {
for i, req := range b.reqs {
err := req.Update(tx)
err := req.Do(tx)
if err != nil {
// If we get a serialization error, we
// want the underlying SQL retry
+628 -7
View File
@@ -1,7 +1,11 @@
package batch
import (
"context"
"database/sql"
"encoding/binary"
"errors"
"fmt"
"path/filepath"
"sync"
"testing"
@@ -9,10 +13,26 @@ import (
"github.com/btcsuite/btcwallet/walletdb"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/sqldb"
"github.com/lightningnetwork/lnd/sqldb/sqlc"
"github.com/stretchr/testify/require"
)
// batchTestIntervals is a list of batch commit intervals to use for
// benchmarking tests.
var batchTestIntervals = []time.Duration{
time.Millisecond * 0,
time.Millisecond * 50,
time.Millisecond * 100,
time.Millisecond * 200,
time.Millisecond * 500,
}
// TestRetry tests the retry logic of the batch scheduler.
func TestRetry(t *testing.T) {
t.Parallel()
ctx := context.Background()
dbDir := t.TempDir()
dbName := filepath.Join(dbDir, "weks.db")
@@ -30,19 +50,21 @@ func TestRetry(t *testing.T) {
mu sync.Mutex
called int
)
sched := NewTimeScheduler(db, &mu, time.Second)
sched := NewTimeScheduler[kvdb.RwTx](
NewBoltBackend[kvdb.RwTx](db), &mu, time.Second,
)
// First, we construct a request that should retry individually and
// execute it non-lazily. It should still return the error the second
// time.
req := &Request{
Update: func(tx kvdb.RwTx) error {
req := &Request[kvdb.RwTx]{
Do: func(tx kvdb.RwTx) error {
called++
return errors.New("test")
},
}
err = sched.Execute(req)
err = sched.Execute(ctx, req)
// Check and reset the called counter.
mu.Lock()
@@ -56,14 +78,14 @@ func TestRetry(t *testing.T) {
// a serialization error, which should cause the underlying postgres
// transaction to retry. Since we aren't using postgres, this will
// cause the transaction to not be retried at all.
req = &Request{
Update: func(tx kvdb.RwTx) error {
req = &Request[kvdb.RwTx]{
Do: func(tx kvdb.RwTx) error {
called++
return errors.New("could not serialize access")
},
}
err = sched.Execute(req)
err = sched.Execute(ctx, req)
// Check the called counter.
mu.Lock()
@@ -72,3 +94,602 @@ func TestRetry(t *testing.T) {
require.ErrorContains(t, err, "could not serialize access")
}
// TestReadOnly just ensures that nothing breaks if we specify a read-only tx
// and then continue to add a write transaction to the same batch.
func TestReadOnly(t *testing.T) {
t.Parallel()
ctx := context.Background()
t.Run("bbolt-ReadWrite", func(t *testing.T) {
db, err := walletdb.Create(
"bdb", filepath.Join(t.TempDir(), "weks.db"), true,
kvdb.DefaultDBTimeout, false,
)
require.NoError(t, err)
if err != nil {
t.Fatalf("unable to create walletdb: %v", err)
}
t.Cleanup(func() {
require.NoError(t, db.Close())
})
// Create a bbolt read-write scheduler.
rwSche := NewTimeScheduler[kvdb.RwTx](
NewBoltBackend[kvdb.RwTx](db), nil, time.Second,
)
// Call it without a read-only option.
var called bool
req := &Request[kvdb.RwTx]{
Do: func(tx kvdb.RwTx) error {
called = true
return nil
},
}
require.NoError(t, rwSche.Execute(ctx, req))
require.True(t, called)
// Call it with a read-only option.
called = false
req = &Request[kvdb.RwTx]{
Opts: NewSchedulerOptions(ReadOnly()),
Do: func(tx kvdb.RwTx) error {
called = true
return nil
},
}
require.NoError(t, rwSche.Execute(ctx, req))
require.True(t, called)
// Now, spin off a bunch of reads and writes at the same time
// so that we can simulate the upgrade from read-only to
// read-write.
var (
wg sync.WaitGroup
reads = 0
readsMu sync.Mutex
writes = 0
writesMu sync.Mutex
)
for i := 0; i < 100; i++ {
// Spin off the reads.
wg.Add(1)
go func() {
defer wg.Done()
req := &Request[kvdb.RwTx]{
Opts: NewSchedulerOptions(ReadOnly()),
Do: func(tx kvdb.RwTx) error {
readsMu.Lock()
reads++
readsMu.Unlock()
return nil
},
}
require.NoError(t, rwSche.Execute(ctx, req))
}()
// Spin off the writes.
wg.Add(1)
go func() {
defer wg.Done()
req := &Request[kvdb.RwTx]{
Do: func(tx kvdb.RwTx) error {
writesMu.Lock()
writes++
writesMu.Unlock()
return nil
},
}
require.NoError(t, rwSche.Execute(ctx, req))
}()
}
wg.Wait()
require.Equal(t, 100, reads)
require.Equal(t, 100, writes)
})
// Note that if the scheduler is initialized with a read-only bbolt tx,
// then the ReadOnly option does nothing as it will be read-only
// regardless.
t.Run("bbolt-ReadOnly", func(t *testing.T) {
db, err := walletdb.Create(
"bdb", filepath.Join(t.TempDir(), "weks.db"), true,
kvdb.DefaultDBTimeout, false,
)
require.NoError(t, err)
if err != nil {
t.Fatalf("unable to create walletdb: %v", err)
}
t.Cleanup(func() {
require.NoError(t, db.Close())
})
// Create a bbolt read only scheduler.
rwSche := NewTimeScheduler[kvdb.RTx](
NewBoltBackend[kvdb.RTx](db), nil, time.Second,
)
// Call it without a read-only option.
var called bool
req := &Request[kvdb.RTx]{
Do: func(tx kvdb.RTx) error {
called = true
return nil
},
}
require.NoError(t, rwSche.Execute(ctx, req))
require.True(t, called)
// Call it with a read-only option.
called = false
req = &Request[kvdb.RTx]{
Opts: NewSchedulerOptions(ReadOnly()),
Do: func(tx kvdb.RTx) error {
called = true
return nil
},
}
require.NoError(t, rwSche.Execute(ctx, req))
require.True(t, called)
})
t.Run("sql", func(t *testing.T) {
base := sqldb.NewTestSqliteDB(t).BaseDB
db := sqldb.NewTransactionExecutor(
base, func(tx *sql.Tx) *sqlc.Queries {
return base.WithTx(tx)
},
)
// Create a SQL scheduler with a long batch interval.
scheduler := NewTimeScheduler[*sqlc.Queries](
db, nil, time.Second,
)
// writeRecord is a helper that adds a single new invoice to the
// database. It uses the 'i' argument to create a unique hash
// for the invoice.
writeRecord := func(t *testing.T, tx *sqlc.Queries, i int64) {
var hash [8]byte
binary.BigEndian.PutUint64(hash[:], uint64(i))
_, err := tx.InsertInvoice(
ctx, sqlc.InsertInvoiceParams{
Hash: hash[:],
PaymentAddr: hash[:],
PaymentRequestHash: hash[:],
Expiry: -123,
},
)
require.NoError(t, err)
}
// readRecord is a helper that reads a single invoice from the
// database. It uses the 'i' argument to create a unique hash
// for the invoice.
readRecord := func(t *testing.T, tx *sqlc.Queries,
i int) error {
var hash [8]byte
binary.BigEndian.PutUint64(hash[:], uint64(i))
_, err := tx.GetInvoiceByHash(ctx, hash[:])
return err
}
// Execute a bunch of read-only requests in parallel. These
// should be batched together and kept as read only.
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
req := &Request[*sqlc.Queries]{
Opts: NewSchedulerOptions(ReadOnly()),
Do: func(tx *sqlc.Queries) error {
err := readRecord(t, tx, i)
require.ErrorIs(
t, err, sql.ErrNoRows,
)
return nil
},
}
require.NoError(t, scheduler.Execute(ctx, req))
}(i)
}
wg.Wait()
// Now, execute reads and writes in parallel. These should be
// batched together and the tx should be updated to read-write.
// We just simulate this scenario. Write transactions succeeding
// are how we know that the tx was upgraded to read-write.
for i := 0; i < 100; i++ {
// Spin off the writes.
wg.Add(1)
go func(i int) {
defer wg.Done()
req := &Request[*sqlc.Queries]{
Do: func(tx *sqlc.Queries) error {
writeRecord(t, tx, int64(i))
return nil
},
}
require.NoError(t, scheduler.Execute(ctx, req))
}(i)
// Spin off the reads.
wg.Add(1)
go func(i int) {
defer wg.Done()
errExpected := func(err error) {
noRows := errors.Is(err, sql.ErrNoRows)
require.True(t, err == nil || noRows)
}
req := &Request[*sqlc.Queries]{
Opts: NewSchedulerOptions(ReadOnly()),
Do: func(tx *sqlc.Queries) error {
err := readRecord(t, tx, i)
errExpected(err)
return nil
},
}
require.NoError(t, scheduler.Execute(ctx, req))
}(i)
}
wg.Wait()
})
}
// BenchmarkBoltBatching benchmarks the performance of the batch scheduler
// against the bolt backend.
func BenchmarkBoltBatching(b *testing.B) {
setUpDB := func(b *testing.B) kvdb.Backend {
// Create a new database backend for the test.
backend, backendCleanup, err := kvdb.GetTestBackend(
b.TempDir(), "db",
)
require.NoError(b, err)
b.Cleanup(func() { backendCleanup() })
return backend
}
// writeRecord is a helper that writes a simple record to the
// database. It creates a top-level bucket and a sub-bucket, then
// writes a record with a sequence number as the key.
writeRecord := func(b *testing.B, tx kvdb.RwTx) {
bucket, err := tx.CreateTopLevelBucket([]byte("top-level"))
require.NoError(b, err)
subBucket, err := bucket.CreateBucketIfNotExists(
[]byte("sub-bucket"),
)
require.NoError(b, err)
seq, err := subBucket.NextSequence()
require.NoError(b, err)
var key [8]byte
binary.BigEndian.PutUint64(key[:], seq)
err = subBucket.Put(key[:], []byte("value"))
require.NoError(b, err)
}
// verifyRecordsWritten is a helper that verifies that the writeRecord
// helper was called the expected number of times.
verifyRecordsWritten := func(b *testing.B, db kvdb.Backend, N int) {
err := db.View(func(tx kvdb.RTx) error {
bucket := tx.ReadBucket([]byte("top-level"))
require.NotNil(b, bucket)
subBucket := bucket.NestedReadBucket(
[]byte("sub-bucket"),
)
require.NotNil(b, subBucket)
require.EqualValues(b, subBucket.Sequence(), N)
return nil
}, func() {})
require.NoError(b, err)
}
// This test benchmarks the performance when using N new transactions
// for N write queries. This does not use the scheduler.
b.Run("N txs for N write queries", func(b *testing.B) {
db := setUpDB(b)
b.ResetTimer()
var wg sync.WaitGroup
for i := 0; i < b.N; i++ {
wg.Add(1)
go func() {
defer wg.Done()
err := db.Update(func(tx kvdb.RwTx) error {
writeRecord(b, tx)
return nil
}, func() {})
require.NoError(b, err)
}()
}
wg.Wait()
b.StopTimer()
verifyRecordsWritten(b, db, b.N)
})
// This test benchmarks the performance when using a single transaction
// for N write queries. This does not use the scheduler.
b.Run("1 txs for N write queries", func(b *testing.B) {
db := setUpDB(b)
b.ResetTimer()
err := db.Update(func(tx kvdb.RwTx) error {
for i := 0; i < b.N; i++ {
writeRecord(b, tx)
}
return nil
}, func() {})
require.NoError(b, err)
b.StopTimer()
verifyRecordsWritten(b, db, b.N)
})
// batchTest benches the performance of the batch scheduler configured
// with/without the LazyAdd option and with the given commit interval.
batchTest := func(b *testing.B, lazy bool, interval time.Duration) {
ctx := context.Background()
db := setUpDB(b)
scheduler := NewTimeScheduler(
NewBoltBackend[kvdb.RwTx](db), nil, interval,
)
var opts []SchedulerOption
if lazy {
opts = append(opts, LazyAdd())
}
b.ResetTimer()
var wg sync.WaitGroup
for i := 0; i < b.N; i++ {
wg.Add(1)
go func() {
defer wg.Done()
r := &Request[kvdb.RwTx]{
Opts: NewSchedulerOptions(
opts...,
),
Do: func(tx kvdb.RwTx) error {
writeRecord(b, tx)
return nil
},
}
err := scheduler.Execute(ctx, r)
require.NoError(b, err)
}()
}
wg.Wait()
b.StopTimer()
verifyRecordsWritten(b, db, b.N)
}
for _, lazy := range []bool{true, false} {
for _, interval := range batchTestIntervals {
name := fmt.Sprintf(
"batched queries %s lazy: %v", interval, lazy,
)
b.Run(name, func(b *testing.B) {
batchTest(b, lazy, interval)
})
}
}
}
// BenchmarkSQLBatching benchmarks the performance of the batch scheduler
// against the sqlite and postgres backends.
func BenchmarkSQLBatching(b *testing.B) {
b.Run("sqlite", func(b *testing.B) {
benchmarkSQLBatching(b, true)
})
b.Run("postgres", func(b *testing.B) {
benchmarkSQLBatching(b, false)
})
}
// benchmarkSQLBatching benchmarks the performance of the batch scheduler
// against an SQL backend. It uses the AddInvoice query as the operation to
// benchmark.
func benchmarkSQLBatching(b *testing.B, sqlite bool) {
// First create a shared Postgres instance so we don't spawn a new
// docker container for each test.
pgFixture := sqldb.NewTestPgFixture(
b, sqldb.DefaultPostgresFixtureLifetime,
)
b.Cleanup(func() {
pgFixture.TearDown(b)
})
setUpDB := func(b *testing.B) sqldb.BatchedTx[*sqlc.Queries] {
var db *sqldb.BaseDB
if sqlite {
db = sqldb.NewTestSqliteDB(b).BaseDB
} else {
db = sqldb.NewTestPostgresDB(b, pgFixture).BaseDB
}
return sqldb.NewTransactionExecutor(
db, func(tx *sql.Tx) *sqlc.Queries {
return db.WithTx(tx)
},
)
}
ctx := context.Background()
var opts txOpts
// writeRecord is a helper that adds a single new invoice to the
// database. It uses the 'i' argument to create a unique hash for the
// invoice.
writeRecord := func(b *testing.B, tx *sqlc.Queries, i int64) {
var hash [8]byte
binary.BigEndian.PutUint64(hash[:], uint64(i))
_, err := tx.InsertInvoice(ctx, sqlc.InsertInvoiceParams{
Hash: hash[:],
PaymentAddr: hash[:],
PaymentRequestHash: hash[:],
Expiry: -123,
})
require.NoError(b, err)
}
// verifyRecordsWritten is a helper that verifies that the writeRecord
// helper was called the expected number of times. We know that N was
// used to derive the hash for each invoice persisted to the DB, so we
// can use it to verify that the last expected invoice was written.
verifyRecordsWritten := func(b *testing.B,
tx sqldb.BatchedTx[*sqlc.Queries], N int) {
var hash [8]byte
binary.BigEndian.PutUint64(hash[:], uint64(N-1))
err := tx.ExecTx(
ctx, &txOpts{}, func(queries *sqlc.Queries) error {
_, err := queries.GetInvoiceByHash(ctx, hash[:])
require.NoError(b, err)
return nil
}, func() {},
)
require.NoError(b, err)
}
// This test benchmarks the performance when using N new transactions
// for N write queries. This does not use the scheduler.
b.Run("N txs for N write queries", func(b *testing.B) {
db := setUpDB(b)
b.ResetTimer()
var wg sync.WaitGroup
for i := 0; i < b.N; i++ {
wg.Add(1)
go func(j int) {
defer wg.Done()
err := db.ExecTx(
ctx, &opts,
func(tx *sqlc.Queries) error {
writeRecord(b, tx, int64(j))
return nil
}, func() {},
)
require.NoError(b, err)
}(i)
}
wg.Wait()
b.StopTimer()
verifyRecordsWritten(b, db, b.N)
})
// This test benchmarks the performance when using a single transaction
// for N write queries. This does not use the scheduler.
b.Run("1 txs for N write queries", func(b *testing.B) {
db := setUpDB(b)
b.ResetTimer()
err := db.ExecTx(
ctx, &opts,
func(tx *sqlc.Queries) error {
for i := 0; i < b.N; i++ {
writeRecord(b, tx, int64(i))
}
return nil
}, func() {},
)
require.NoError(b, err)
b.StopTimer()
verifyRecordsWritten(b, db, b.N)
})
// batchTest benches the performance of the batch scheduler configured
// with/without the LazyAdd option and with the given commit interval.
batchTest := func(b *testing.B, lazy bool, interval time.Duration) {
db := setUpDB(b)
scheduler := NewTimeScheduler[*sqlc.Queries](
db, nil, interval,
)
var opts []SchedulerOption
if lazy {
opts = append(opts, LazyAdd())
}
b.ResetTimer()
var wg sync.WaitGroup
for i := 0; i < b.N; i++ {
wg.Add(1)
go func(j int) {
defer wg.Done()
r := &Request[*sqlc.Queries]{
Opts: NewSchedulerOptions(
opts...,
),
Do: func(tx *sqlc.Queries) error {
writeRecord(b, tx, int64(j))
return nil
},
}
err := scheduler.Execute(ctx, r)
require.NoError(b, err)
}(i)
}
wg.Wait()
b.StopTimer()
verifyRecordsWritten(b, db, b.N)
}
for _, lazy := range []bool{true, false} {
for _, interval := range batchTestIntervals {
name := fmt.Sprintf(
"batched queries %s lazy: %v", interval, lazy,
)
b.Run(name, func(b *testing.B) {
batchTest(b, lazy, interval)
})
}
}
}
+50 -11
View File
@@ -1,10 +1,13 @@
package batch
import "github.com/lightningnetwork/lnd/kvdb"
import "context"
// Request defines an operation that can be batched into a single bbolt
// transaction.
type Request struct {
type Request[Q any] struct {
// Opts holds various configuration options for a scheduled request.
Opts *SchedulerOptions
// Reset is called before each invocation of Update and is used to clear
// any possible modifications to local state as a result of previous
// calls to Update that were not committed due to a concurrent batch
@@ -13,10 +16,10 @@ type Request struct {
// NOTE: This field is optional.
Reset func()
// Update is applied alongside other operations in the batch.
// Do is applied alongside other operations in the batch.
//
// NOTE: This method MUST NOT acquire any mutexes.
Update func(tx kvdb.RwTx) error
Do func(tx Q) error
// OnCommit is called if the batch or a subset of the batch including
// this request all succeeded without failure. The passed error should
@@ -25,31 +28,67 @@ type Request struct {
//
// NOTE: This field is optional.
OnCommit func(commitErr error) error
}
// lazy should be true if we don't have to immediately execute this
// SchedulerOptions holds various configuration options for a scheduled request.
type SchedulerOptions struct {
// Lazy should be true if we don't have to immediately execute this
// request when it comes in. This means that it can be scheduled later,
// allowing larger batches.
lazy bool
Lazy bool
// ReadOnly should be true if the request is read-only. By default,
// this is false.
ReadOnly bool
}
// NewDefaultSchedulerOpts returns a new SchedulerOptions with default values.
func NewDefaultSchedulerOpts() *SchedulerOptions {
return &SchedulerOptions{
Lazy: false,
ReadOnly: false,
}
}
// NewSchedulerOptions returns a new SchedulerOptions with the given options
// applied on top of the default options.
func NewSchedulerOptions(options ...SchedulerOption) *SchedulerOptions {
opts := NewDefaultSchedulerOpts()
for _, o := range options {
o(opts)
}
return opts
}
// SchedulerOption is a type that can be used to supply options to a scheduled
// request.
type SchedulerOption func(r *Request)
type SchedulerOption func(*SchedulerOptions)
// LazyAdd will make the request be executed lazily, added to the next batch to
// reduce db contention.
func LazyAdd() SchedulerOption {
return func(r *Request) {
r.lazy = true
return func(opts *SchedulerOptions) {
opts.Lazy = true
}
}
// ReadOnly will mark the request as read-only. This means that the
// transaction will be executed in read-only mode, and no changes will be
// made to the database. If any requests in the same batch are not read-only,
// then the entire batch will be executed in read-write mode.
func ReadOnly() SchedulerOption {
return func(opts *SchedulerOptions) {
opts.ReadOnly = true
}
}
// Scheduler abstracts a generic batching engine that accumulates an incoming
// set of Requests, executes them, and returns the error from the operation.
type Scheduler interface {
type Scheduler[Q any] interface {
// Execute schedules a Request for execution with the next available
// batch. This method blocks until the underlying closure has been
// run against the database. The resulting error is returned to the
// caller.
Execute(req *Request) error
Execute(ctx context.Context, req *Request[Q]) error
}
+50
View File
@@ -0,0 +1,50 @@
package batch
import (
"context"
"fmt"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/sqldb"
)
// BoltBatcher is a bbolt implementation of the sqldb.BatchedTx interface.
type BoltBatcher[Q any] struct {
db kvdb.Backend
}
// NewBoltBackend creates a new BoltBackend instance.
func NewBoltBackend[Q any](db kvdb.Backend) *BoltBatcher[Q] {
return &BoltBatcher[Q]{db: db}
}
// ExecTx will execute the passed txBody, operating upon generic
// parameter Q (usually a storage interface) in a single transaction.
//
// NOTE: This is part of the sqldb.BatchedTx interface.
func (t *BoltBatcher[Q]) ExecTx(_ context.Context, opts sqldb.TxOptions,
txBody func(Q) error, reset func()) error {
if opts.ReadOnly() {
return kvdb.View(t.db, func(tx kvdb.RTx) error {
q, ok := any(tx).(Q)
if !ok {
return fmt.Errorf("unable to cast tx(%T) "+
"into the type expected by the "+
"BoltBatcher(%T)", tx, t)
}
return txBody(q)
}, reset)
}
return kvdb.Update(t.db, func(tx kvdb.RwTx) error {
q, ok := any(tx).(Q)
if !ok {
return fmt.Errorf("unable to cast tx(%T) into the "+
"type expected by the BoltBatcher(%T)", tx, t)
}
return txBody(q)
}, reset)
}
+44 -16
View File
@@ -1,10 +1,11 @@
package batch
import (
"context"
"sync"
"time"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/sqldb"
)
// TimeScheduler is a batching engine that executes requests within a fixed
@@ -12,13 +13,13 @@ import (
// configurable duration for other concurrent requests to join the batch. Once
// this time has elapsed, the batch is closed and executed. Subsequent requests
// are then added to a new batch which undergoes the same process.
type TimeScheduler struct {
db kvdb.Backend
type TimeScheduler[Q any] struct {
db sqldb.BatchedTx[Q]
locker sync.Locker
duration time.Duration
mu sync.Mutex
b *batch
b *batch[Q]
}
// NewTimeScheduler initializes a new TimeScheduler with a fixed duration at
@@ -26,10 +27,10 @@ type TimeScheduler struct {
// cache, the cache's lock should be provided to so that external consistency
// can be maintained, as successful db operations will cause a request's
// OnCommit method to be executed while holding this lock.
func NewTimeScheduler(db kvdb.Backend, locker sync.Locker,
duration time.Duration) *TimeScheduler {
func NewTimeScheduler[Q any](db sqldb.BatchedTx[Q], locker sync.Locker,
duration time.Duration) *TimeScheduler[Q] {
return &TimeScheduler{
return &TimeScheduler[Q]{
db: db,
locker: locker,
duration: duration,
@@ -42,8 +43,12 @@ func NewTimeScheduler(db kvdb.Backend, locker sync.Locker,
// underlying operation is returned to the caller.
//
// NOTE: Part of the Scheduler interface.
func (s *TimeScheduler) Execute(r *Request) error {
req := request{
func (s *TimeScheduler[Q]) Execute(ctx context.Context, r *Request[Q]) error {
if r.Opts == nil {
r.Opts = NewDefaultSchedulerOpts()
}
req := request[Q]{
Request: r,
errChan: make(chan error, 1),
}
@@ -52,20 +57,41 @@ func (s *TimeScheduler) Execute(r *Request) error {
// or no batch exists, create a new one.
s.mu.Lock()
if s.b == nil {
s.b = &batch{
s.b = &batch[Q]{
db: s.db,
clear: s.clear,
locker: s.locker,
// By default, we assume that the batch is read-only,
// and we only upgrade it to read-write if a request
// is added that is not read-only.
txOpts: txOpts{
readOnly: true,
},
}
time.AfterFunc(s.duration, s.b.trigger)
trigger := s.b.trigger
time.AfterFunc(s.duration, func() {
trigger(ctx)
})
}
s.b.reqs = append(s.b.reqs, &req)
// If this is a non-lazy request, we'll execute the batch immediately.
if !r.lazy {
go s.b.trigger()
// We only upgrade the batch to read-write if the new request is not
// read-only. If it is already read-write, we don't need to do anything.
if s.b.txOpts.readOnly && !r.Opts.ReadOnly {
s.b.txOpts.readOnly = false
}
// If this is a non-lazy request, we'll execute the batch immediately.
if !r.Opts.Lazy {
go s.b.trigger(ctx)
}
// We need to grab a reference to the batch's txOpts so that we can
// pass it before we unlock the scheduler's mutex since the batch may
// be set to nil before we access the txOpts below.
txOpts := s.b.txOpts
s.mu.Unlock()
// Wait for the batch to process the request. If the batch didn't
@@ -83,7 +109,9 @@ func (s *TimeScheduler) Execute(r *Request) error {
}
// Otherwise, run the request on its own.
commitErr := kvdb.Update(s.db, req.Update, func() {
commitErr := s.db.ExecTx(ctx, &txOpts, func(tx Q) error {
return req.Do(tx)
}, func() {
if req.Reset != nil {
req.Reset()
}
@@ -100,7 +128,7 @@ func (s *TimeScheduler) Execute(r *Request) error {
// clear resets the scheduler's batch to nil so that no more requests can be
// added.
func (s *TimeScheduler) clear(b *batch) {
func (s *TimeScheduler[Q]) clear(b *batch[Q]) {
s.mu.Lock()
if s.b == b {
s.b = nil
+11 -11
View File
@@ -14,7 +14,6 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
graphdb "github.com/lightningnetwork/lnd/graph/db"
"github.com/lightningnetwork/lnd/graph/db/models"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/kvdb"
@@ -185,21 +184,22 @@ func TestMultiSourceAddrsForNode(t *testing.T) {
fullDB, err := MakeTestDB(t)
require.NoError(t, err, "unable to make test database")
graph, err := graphdb.MakeTestGraph(t)
require.NoError(t, err)
graph := newMockAddrSource(t)
t.Cleanup(func() {
graph.AssertExpectations(t)
})
// We'll make a test vertex to insert into the database, as the source
// node, but this node will only have half the number of addresses it
// usually does.
// We'll make a test vertex, but this node will only have half the
// number of addresses it usually does.
testNode := createTestVertex(t)
require.NoError(t, err, "unable to create test node")
testNode.Addresses = []net.Addr{testAddr}
require.NoError(t, graph.SetSourceNode(testNode))
nodePub, err := testNode.PubKey()
require.NoError(t, err)
graph.On("AddrsForNode", nodePub).Return(
true, []net.Addr{testAddr}, nil,
).Once()
// Next, we'll make a link node with the same pubkey, but with an
// additional address.
nodePub, err := testNode.PubKey()
require.NoError(t, err, "unable to recv node pub")
linkNode := NewLinkNode(
fullDB.channelStateDB.linkNodeDB, wire.MainNet, nodePub,
anotherAddr,
-8
View File
@@ -6,7 +6,6 @@ import (
"github.com/btcsuite/btcwallet/walletdb"
"github.com/go-errors/errors"
graphdb "github.com/lightningnetwork/lnd/graph/db"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/stretchr/testify/require"
)
@@ -22,13 +21,6 @@ func applyMigration(t *testing.T, beforeMigration, afterMigration func(d *DB),
}
cdb.dryRun = dryRun
// Create a test node that will be our source node.
testNode := createTestVertex(t)
graph, err := graphdb.MakeTestGraph(t)
require.NoError(t, err)
require.NoError(t, graph.SetSourceNode(testNode))
// beforeMigration usually used for populating the database
// with test data.
beforeMigration(cdb)
+8 -4
View File
@@ -1046,10 +1046,14 @@ func (d *DefaultDatabaseBuilder) BuildDatabase(
)
}
dbs.GraphDB, err = graphdb.NewChannelGraph(&graphdb.Config{
KVDB: databaseBackends.GraphDB,
KVStoreOpts: graphDBOptions,
}, chanGraphOpts...)
graphStore, err := graphdb.NewKVStore(
databaseBackends.GraphDB, graphDBOptions...,
)
if err != nil {
return nil, nil, err
}
dbs.GraphDB, err = graphdb.NewChannelGraph(graphStore, chanGraphOpts...)
if err != nil {
cleanUp()
+18 -7
View File
@@ -2,6 +2,7 @@ package discovery
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"errors"
@@ -36,8 +37,9 @@ type NetworkPeerBootstrapper interface {
// denotes how many valid peer addresses to return. The passed set of
// node nodes allows the caller to ignore a set of nodes perhaps
// because they already have connections established.
SampleNodeAddrs(numAddrs uint32,
ignore map[autopilot.NodeID]struct{}) ([]*lnwire.NetAddress, error)
SampleNodeAddrs(ctx context.Context, numAddrs uint32,
ignore map[autopilot.NodeID]struct{}) ([]*lnwire.NetAddress,
error)
// Name returns a human readable string which names the concrete
// implementation of the NetworkPeerBootstrapper.
@@ -50,7 +52,8 @@ type NetworkPeerBootstrapper interface {
// bootstrapper will be queried successively until the target amount is met. If
// the ignore map is populated, then the bootstrappers will be instructed to
// skip those nodes.
func MultiSourceBootstrap(ignore map[autopilot.NodeID]struct{}, numAddrs uint32,
func MultiSourceBootstrap(ctx context.Context,
ignore map[autopilot.NodeID]struct{}, numAddrs uint32,
bootstrappers ...NetworkPeerBootstrapper) ([]*lnwire.NetAddress, error) {
// We'll randomly shuffle our bootstrappers before querying them in
@@ -73,7 +76,9 @@ func MultiSourceBootstrap(ignore map[autopilot.NodeID]struct{}, numAddrs uint32,
// the number of address remaining that we need to fetch.
numAddrsLeft := numAddrs - uint32(len(addrs))
log.Tracef("Querying for %v addresses", numAddrsLeft)
netAddrs, err := bootstrapper.SampleNodeAddrs(numAddrsLeft, ignore)
netAddrs, err := bootstrapper.SampleNodeAddrs(
ctx, numAddrsLeft, ignore,
)
if err != nil {
// If we encounter an error with a bootstrapper, then
// we'll continue on to the next available
@@ -152,9 +157,12 @@ func NewGraphBootstrapper(cg autopilot.ChannelGraph) (NetworkPeerBootstrapper, e
// many valid peer addresses to return.
//
// NOTE: Part of the NetworkPeerBootstrapper interface.
func (c *ChannelGraphBootstrapper) SampleNodeAddrs(numAddrs uint32,
func (c *ChannelGraphBootstrapper) SampleNodeAddrs(_ context.Context,
numAddrs uint32,
ignore map[autopilot.NodeID]struct{}) ([]*lnwire.NetAddress, error) {
ctx := context.TODO()
// We'll merge the ignore map with our currently selected map in order
// to ensure we don't return any duplicate nodes.
for n := range ignore {
@@ -177,7 +185,9 @@ func (c *ChannelGraphBootstrapper) SampleNodeAddrs(numAddrs uint32,
errFound = fmt.Errorf("found node")
)
err := c.chanGraph.ForEachNode(func(node autopilot.Node) error {
err := c.chanGraph.ForEachNode(ctx, func(_ context.Context,
node autopilot.Node) error {
nID := autopilot.NodeID(node.PubKey())
if _, ok := c.tried[nID]; ok {
return nil
@@ -382,7 +392,8 @@ func (d *DNSSeedBootstrapper) fallBackSRVLookup(soaShim string,
// network peer bootstrapper source. The num addrs field passed in denotes how
// many valid peer addresses to return. The set of DNS seeds are used
// successively to retrieve eligible target nodes.
func (d *DNSSeedBootstrapper) SampleNodeAddrs(numAddrs uint32,
func (d *DNSSeedBootstrapper) SampleNodeAddrs(_ context.Context,
numAddrs uint32,
ignore map[autopilot.NodeID]struct{}) ([]*lnwire.NetAddress, error) {
var netAddrs []*lnwire.NetAddress
+82 -58
View File
@@ -2,6 +2,7 @@ package discovery
import (
"bytes"
"context"
"errors"
"fmt"
"strings"
@@ -475,9 +476,6 @@ type AuthenticatedGossiper struct {
// held.
bestHeight uint32
quit chan struct{}
wg sync.WaitGroup
// cfg is a copy of the configuration struct that the gossiper service
// was initialized with.
cfg *Config
@@ -555,6 +553,10 @@ type AuthenticatedGossiper struct {
vb *ValidationBarrier
sync.Mutex
cancel fn.Option[context.CancelFunc]
quit chan struct{}
wg sync.WaitGroup
}
// New creates a new AuthenticatedGossiper instance, initialized with the
@@ -644,13 +646,16 @@ func (d *AuthenticatedGossiper) PropagateChanPolicyUpdate(
func (d *AuthenticatedGossiper) Start() error {
var err error
d.started.Do(func() {
ctx, cancel := context.WithCancel(context.Background())
d.cancel = fn.Some(cancel)
log.Info("Authenticated Gossiper starting")
err = d.start()
err = d.start(ctx)
})
return err
}
func (d *AuthenticatedGossiper) start() error {
func (d *AuthenticatedGossiper) start(ctx context.Context) error {
// First we register for new notifications of newly discovered blocks.
// We do this immediately so we'll later be able to consume any/all
// blocks which were discovered.
@@ -680,7 +685,7 @@ func (d *AuthenticatedGossiper) start() error {
// Start receiving blocks in its dedicated goroutine.
d.wg.Add(2)
go d.syncBlockHeight()
go d.networkHandler()
go d.networkHandler(ctx)
return nil
}
@@ -835,6 +840,7 @@ func (d *AuthenticatedGossiper) stop() {
d.banman.stop()
d.cancel.WhenSome(func(fn context.CancelFunc) { fn() })
close(d.quit)
d.wg.Wait()
@@ -851,8 +857,8 @@ func (d *AuthenticatedGossiper) stop() {
// then added to a queue for batched trickled announcement to all connected
// peers. Remote channel announcements should contain the announcement proof
// and be fully validated.
func (d *AuthenticatedGossiper) ProcessRemoteAnnouncement(msg lnwire.Message,
peer lnpeer.Peer) chan error {
func (d *AuthenticatedGossiper) ProcessRemoteAnnouncement(ctx context.Context,
msg lnwire.Message, peer lnpeer.Peer) chan error {
log.Debugf("Processing remote msg %T from peer=%x", msg, peer.PubKey())
@@ -901,7 +907,7 @@ func (d *AuthenticatedGossiper) ProcessRemoteAnnouncement(msg lnwire.Message,
// If we've found the message target, then we'll dispatch the
// message directly to it.
if err := syncer.ApplyGossipFilter(m); err != nil {
if err := syncer.ApplyGossipFilter(ctx, m); err != nil {
log.Warnf("Unable to apply gossip filter for peer=%x: "+
"%v", peer.PubKey(), err)
@@ -942,8 +948,12 @@ func (d *AuthenticatedGossiper) ProcessRemoteAnnouncement(msg lnwire.Message,
// If the peer that sent us this error is quitting, then we don't need
// to send back an error and can return immediately.
// TODO(elle): the peer should now just rely on canceling the passed
// context.
case <-peer.QuitSignal():
return nil
case <-ctx.Done():
return nil
case <-d.quit:
nMsg.err <- ErrGossiperShuttingDown
}
@@ -1337,7 +1347,7 @@ func (d *AuthenticatedGossiper) splitAnnouncementBatches(
// split size, and then sends out all items to the set of target peers. Locally
// generated announcements are always sent before remotely generated
// announcements.
func (d *AuthenticatedGossiper) splitAndSendAnnBatch(
func (d *AuthenticatedGossiper) splitAndSendAnnBatch(ctx context.Context,
annBatch msgsToBroadcast) {
// delayNextBatch is a helper closure that blocks for `SubBatchDelay`
@@ -1374,7 +1384,7 @@ func (d *AuthenticatedGossiper) splitAndSendAnnBatch(
// Now send the remote announcements.
for _, annBatch := range remoteBatches {
d.sendRemoteBatch(annBatch)
d.sendRemoteBatch(ctx, annBatch)
delayNextBatch()
}
}()
@@ -1398,14 +1408,16 @@ func (d *AuthenticatedGossiper) sendLocalBatch(annBatch []msgWithSenders) {
// sendRemoteBatch broadcasts a list of remotely generated announcements to our
// peers.
func (d *AuthenticatedGossiper) sendRemoteBatch(annBatch []msgWithSenders) {
func (d *AuthenticatedGossiper) sendRemoteBatch(ctx context.Context,
annBatch []msgWithSenders) {
syncerPeers := d.syncMgr.GossipSyncers()
// We'll first attempt to filter out this new message for all peers
// that have active gossip syncers active.
for pub, syncer := range syncerPeers {
log.Tracef("Sending messages batch to GossipSyncer(%s)", pub)
syncer.FilterGossipMsgs(annBatch...)
syncer.FilterGossipMsgs(ctx, annBatch...)
}
for _, msgChunk := range annBatch {
@@ -1431,7 +1443,7 @@ func (d *AuthenticatedGossiper) sendRemoteBatch(annBatch []msgWithSenders) {
// broadcasting our latest topology state to all connected peers.
//
// NOTE: This MUST be run as a goroutine.
func (d *AuthenticatedGossiper) networkHandler() {
func (d *AuthenticatedGossiper) networkHandler(ctx context.Context) {
defer d.wg.Done()
// Initialize empty deDupedAnnouncements to store announcement batch.
@@ -1446,7 +1458,7 @@ func (d *AuthenticatedGossiper) networkHandler() {
// To start, we'll first check to see if there are any stale channel or
// node announcements that we need to re-transmit.
if err := d.retransmitStaleAnns(time.Now()); err != nil {
if err := d.retransmitStaleAnns(ctx, time.Now()); err != nil {
log.Errorf("Unable to rebroadcast stale announcements: %v", err)
}
@@ -1463,7 +1475,7 @@ func (d *AuthenticatedGossiper) networkHandler() {
// the affected channels and also update the underlying
// graph with the new state.
newChanUpdates, err := d.processChanPolicyUpdate(
policyUpdate.edgesToUpdate,
ctx, policyUpdate.edgesToUpdate,
)
policyUpdate.errChan <- err
if err != nil {
@@ -1488,7 +1500,7 @@ func (d *AuthenticatedGossiper) networkHandler() {
// messages that we'll process serially.
case *lnwire.AnnounceSignatures1:
emittedAnnouncements, _ := d.processNetworkAnnouncement(
announcement,
ctx, announcement,
)
log.Debugf("Processed network message %s, "+
"returned len(announcements)=%v",
@@ -1528,7 +1540,7 @@ func (d *AuthenticatedGossiper) networkHandler() {
d.wg.Add(1)
go d.handleNetworkMessages(
announcement, &announcements, annJobID,
ctx, announcement, &announcements, annJobID,
)
// The trickle timer has ticked, which indicates we should
@@ -1551,7 +1563,7 @@ func (d *AuthenticatedGossiper) networkHandler() {
// announcements, we'll blast them out w/o regard for
// our peer's policies so we ensure they propagate
// properly.
d.splitAndSendAnnBatch(announcementBatch)
d.splitAndSendAnnBatch(ctx, announcementBatch)
// The retransmission timer has ticked which indicates that we
// should check if we need to prune or re-broadcast any of our
@@ -1560,7 +1572,7 @@ func (d *AuthenticatedGossiper) networkHandler() {
// have been dropped, or not properly propagated through the
// network.
case tick := <-d.cfg.RetransmitTicker.Ticks():
if err := d.retransmitStaleAnns(tick); err != nil {
if err := d.retransmitStaleAnns(ctx, tick); err != nil {
log.Errorf("unable to rebroadcast stale "+
"announcements: %v", err)
}
@@ -1578,8 +1590,8 @@ func (d *AuthenticatedGossiper) networkHandler() {
// signal its dependants and add the new announcements to the announce batch.
//
// NOTE: must be run as a goroutine.
func (d *AuthenticatedGossiper) handleNetworkMessages(nMsg *networkMsg,
deDuped *deDupedAnnouncements, jobID JobID) {
func (d *AuthenticatedGossiper) handleNetworkMessages(ctx context.Context,
nMsg *networkMsg, deDuped *deDupedAnnouncements, jobID JobID) {
defer d.wg.Done()
defer d.vb.CompleteJob()
@@ -1607,7 +1619,7 @@ func (d *AuthenticatedGossiper) handleNetworkMessages(nMsg *networkMsg,
// Process the network announcement to determine if this is either a
// new announcement from our PoV or an edges to a prior vertex/edge we
// previously proceeded.
newAnns, allow := d.processNetworkAnnouncement(nMsg)
newAnns, allow := d.processNetworkAnnouncement(ctx, nMsg)
log.Tracef("Processed network message %s, returned "+
"len(announcements)=%v, allowDependents=%v",
@@ -1681,7 +1693,9 @@ func (d *AuthenticatedGossiper) isRecentlyRejectedMsg(msg lnwire.Message,
// stale iff, the last timestamp of its rebroadcast is older than the
// RebroadcastInterval. We also check if a refreshed node announcement should
// be resent.
func (d *AuthenticatedGossiper) retransmitStaleAnns(now time.Time) error {
func (d *AuthenticatedGossiper) retransmitStaleAnns(ctx context.Context,
now time.Time) error {
// Iterate over all of our channels and check if any of them fall
// within the prune interval or re-broadcast interval.
type updateTuple struct {
@@ -1753,7 +1767,7 @@ func (d *AuthenticatedGossiper) retransmitStaleAnns(now time.Time) error {
// Re-sign and update the channel on disk and retrieve our
// ChannelUpdate to broadcast.
chanAnn, chanUpdate, err := d.updateChannel(
chanToUpdate.info, chanToUpdate.edge,
ctx, chanToUpdate.info, chanToUpdate.edge,
)
if err != nil {
return fmt.Errorf("unable to update channel: %w", err)
@@ -1794,7 +1808,7 @@ func (d *AuthenticatedGossiper) retransmitStaleAnns(now time.Time) error {
// Before broadcasting the refreshed node announcement, add it
// to our own graph.
if err := d.addNode(&newNodeAnn); err != nil {
if err := d.addNode(ctx, &newNodeAnn); err != nil {
log.Errorf("Unable to add refreshed node announcement "+
"to graph: %v", err)
}
@@ -1820,7 +1834,7 @@ func (d *AuthenticatedGossiper) retransmitStaleAnns(now time.Time) error {
// processChanPolicyUpdate generates a new set of channel updates for the
// provided list of edges and updates the backing ChannelGraphSource.
func (d *AuthenticatedGossiper) processChanPolicyUpdate(
func (d *AuthenticatedGossiper) processChanPolicyUpdate(ctx context.Context,
edgesToUpdate []EdgeWithInfo) ([]networkMsg, error) {
var chanUpdates []networkMsg
@@ -1829,7 +1843,7 @@ func (d *AuthenticatedGossiper) processChanPolicyUpdate(
// we'll re-sign and update the backing ChannelGraphSource, and
// retrieve our ChannelUpdate to broadcast.
_, chanUpdate, err := d.updateChannel(
edgeInfo.Info, edgeInfo.Edge,
ctx, edgeInfo.Info, edgeInfo.Edge,
)
if err != nil {
return nil, err
@@ -1875,7 +1889,7 @@ func (d *AuthenticatedGossiper) processChanPolicyUpdate(
edgeInfo.Info, chanUpdate.ChannelFlags,
)
err := d.reliableSender.sendMessage(
chanUpdate, remotePubKey,
ctx, chanUpdate, remotePubKey,
)
if err != nil {
log.Errorf("Unable to reliably send %v for "+
@@ -1922,7 +1936,7 @@ func remotePubFromChanInfo(chanInfo *models.ChannelEdgeInfo,
// situation in the case where we create a channel, but for some reason fail
// to receive the remote peer's proof, while the remote peer is able to fully
// assemble the proof and craft the ChannelAnnouncement.
func (d *AuthenticatedGossiper) processRejectedEdge(
func (d *AuthenticatedGossiper) processRejectedEdge(_ context.Context,
chanAnnMsg *lnwire.ChannelAnnouncement1,
proof *models.ChannelAuthProof) ([]networkMsg, error) {
@@ -2010,8 +2024,8 @@ func (d *AuthenticatedGossiper) fetchPKScript(chanID *lnwire.ShortChannelID) (
// addNode processes the given node announcement, and adds it to our channel
// graph.
func (d *AuthenticatedGossiper) addNode(msg *lnwire.NodeAnnouncement,
op ...batch.SchedulerOption) error {
func (d *AuthenticatedGossiper) addNode(_ context.Context,
msg *lnwire.NodeAnnouncement, op ...batch.SchedulerOption) error {
if err := netann.ValidateNodeAnn(msg); err != nil {
return fmt.Errorf("unable to validate node announcement: %w",
@@ -2086,7 +2100,7 @@ func (d *AuthenticatedGossiper) isPremature(chanID lnwire.ShortChannelID,
// be returned which should be broadcasted to the rest of the network. The
// boolean returned indicates whether any dependents of the announcement should
// attempt to be processed as well.
func (d *AuthenticatedGossiper) processNetworkAnnouncement(
func (d *AuthenticatedGossiper) processNetworkAnnouncement(ctx context.Context,
nMsg *networkMsg) ([]networkMsg, bool) {
// If this is a remote update, we set the scheduler option to lazily
@@ -2101,26 +2115,26 @@ func (d *AuthenticatedGossiper) processNetworkAnnouncement(
// information about a node in one of the channels we know about, or a
// updating previously advertised information.
case *lnwire.NodeAnnouncement:
return d.handleNodeAnnouncement(nMsg, msg, schedulerOp)
return d.handleNodeAnnouncement(ctx, nMsg, msg, schedulerOp)
// A new channel announcement has arrived, this indicates the
// *creation* of a new channel within the network. This only advertises
// the existence of a channel and not yet the routing policies in
// either direction of the channel.
case *lnwire.ChannelAnnouncement1:
return d.handleChanAnnouncement(nMsg, msg, schedulerOp...)
return d.handleChanAnnouncement(ctx, nMsg, msg, schedulerOp...)
// A new authenticated channel edge update has arrived. This indicates
// that the directional information for an already known channel has
// been updated.
case *lnwire.ChannelUpdate1:
return d.handleChanUpdate(nMsg, msg, schedulerOp)
return d.handleChanUpdate(ctx, nMsg, msg, schedulerOp)
// A new signature announcement has been received. This indicates
// willingness of nodes involved in the funding of a channel to
// announce this new channel to the rest of the world.
case *lnwire.AnnounceSignatures1:
return d.handleAnnSig(nMsg, msg)
return d.handleAnnSig(ctx, nMsg, msg)
default:
err := errors.New("wrong type of the announcement")
@@ -2134,7 +2148,7 @@ func (d *AuthenticatedGossiper) processNetworkAnnouncement(
//
// NOTE: only the NodeKey1Bytes and NodeKey2Bytes members of the ChannelEdgeInfo
// should be inspected.
func (d *AuthenticatedGossiper) processZombieUpdate(
func (d *AuthenticatedGossiper) processZombieUpdate(_ context.Context,
chanInfo *models.ChannelEdgeInfo, scid lnwire.ShortChannelID,
msg *lnwire.ChannelUpdate1) error {
@@ -2192,7 +2206,7 @@ func (d *AuthenticatedGossiper) processZombieUpdate(
// fetchNodeAnn fetches the latest signed node announcement from our point of
// view for the node with the given public key.
func (d *AuthenticatedGossiper) fetchNodeAnn(
func (d *AuthenticatedGossiper) fetchNodeAnn(_ context.Context,
pubKey [33]byte) (*lnwire.NodeAnnouncement, error) {
node, err := d.cfg.Graph.FetchLightningNode(pubKey)
@@ -2205,7 +2219,9 @@ func (d *AuthenticatedGossiper) fetchNodeAnn(
// isMsgStale determines whether a message retrieved from the backing
// MessageStore is seen as stale by the current graph.
func (d *AuthenticatedGossiper) isMsgStale(msg lnwire.Message) bool {
func (d *AuthenticatedGossiper) isMsgStale(_ context.Context,
msg lnwire.Message) bool {
switch msg := msg.(type) {
case *lnwire.AnnounceSignatures1:
chanInfo, _, _, err := d.cfg.Graph.GetChannelByID(
@@ -2272,7 +2288,8 @@ func (d *AuthenticatedGossiper) isMsgStale(msg lnwire.Message) bool {
// updateChannel creates a new fully signed update for the channel, and updates
// the underlying graph with the new state.
func (d *AuthenticatedGossiper) updateChannel(info *models.ChannelEdgeInfo,
func (d *AuthenticatedGossiper) updateChannel(_ context.Context,
info *models.ChannelEdgeInfo,
edge *models.ChannelEdgePolicy) (*lnwire.ChannelAnnouncement1,
*lnwire.ChannelUpdate1, error) {
@@ -2414,8 +2431,8 @@ func (d *AuthenticatedGossiper) latestHeight() uint32 {
}
// handleNodeAnnouncement processes a new node announcement.
func (d *AuthenticatedGossiper) handleNodeAnnouncement(nMsg *networkMsg,
nodeAnn *lnwire.NodeAnnouncement,
func (d *AuthenticatedGossiper) handleNodeAnnouncement(ctx context.Context,
nMsg *networkMsg, nodeAnn *lnwire.NodeAnnouncement,
ops []batch.SchedulerOption) ([]networkMsg, bool) {
timestamp := time.Unix(int64(nodeAnn.Timestamp), 0)
@@ -2432,7 +2449,7 @@ func (d *AuthenticatedGossiper) handleNodeAnnouncement(nMsg *networkMsg,
return nil, true
}
if err := d.addNode(nodeAnn, ops...); err != nil {
if err := d.addNode(ctx, nodeAnn, ops...); err != nil {
log.Debugf("Adding node: %x got error: %v", nodeAnn.NodeID,
err)
@@ -2487,8 +2504,10 @@ func (d *AuthenticatedGossiper) handleNodeAnnouncement(nMsg *networkMsg,
}
// handleChanAnnouncement processes a new channel announcement.
func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
ann *lnwire.ChannelAnnouncement1,
//
//nolint:funlen
func (d *AuthenticatedGossiper) handleChanAnnouncement(ctx context.Context,
nMsg *networkMsg, ann *lnwire.ChannelAnnouncement1,
ops ...batch.SchedulerOption) ([]networkMsg, bool) {
scid := ann.ShortChannelID
@@ -2680,7 +2699,7 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
// add an alias ChannelAnnouncement from the gossiper.
if !(d.cfg.AssumeChannelValid || d.cfg.IsAlias(scid)) { //nolint:nestif
op, capacity, script, err := d.validateFundingTransaction(
ann, tapscriptRoot,
ctx, ann, tapscriptRoot,
)
if err != nil {
defer d.channelMtx.Unlock(scid.ToUint64())
@@ -2802,7 +2821,7 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
if graph.IsError(err, graph.ErrIgnored) {
// Attempt to process the rejected message to see if we
// get any new announcements.
anns, rErr := d.processRejectedEdge(ann, proof)
anns, rErr := d.processRejectedEdge(ctx, ann, proof)
if rErr != nil {
key := newRejectCacheKey(
scid.ToUint64(),
@@ -2945,8 +2964,10 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
}
// handleChanUpdate processes a new channel update.
func (d *AuthenticatedGossiper) handleChanUpdate(nMsg *networkMsg,
upd *lnwire.ChannelUpdate1,
//
//nolint:funlen
func (d *AuthenticatedGossiper) handleChanUpdate(ctx context.Context,
nMsg *networkMsg, upd *lnwire.ChannelUpdate1,
ops []batch.SchedulerOption) ([]networkMsg, bool) {
log.Debugf("Processing ChannelUpdate: peer=%v, short_chan_id=%v, ",
@@ -3052,7 +3073,7 @@ func (d *AuthenticatedGossiper) handleChanUpdate(nMsg *networkMsg,
break
case errors.Is(err, graphdb.ErrZombieEdge):
err = d.processZombieUpdate(chanInfo, graphScid, upd)
err = d.processZombieUpdate(ctx, chanInfo, graphScid, upd)
if err != nil {
log.Debug(err)
nMsg.err <- err
@@ -3312,7 +3333,7 @@ func (d *AuthenticatedGossiper) handleChanUpdate(nMsg *networkMsg,
// Now we'll attempt to send the channel update message
// reliably to the remote peer in the background, so that we
// don't block if the peer happens to be offline at the moment.
err := d.reliableSender.sendMessage(upd, remotePubKey)
err := d.reliableSender.sendMessage(ctx, upd, remotePubKey)
if err != nil {
err := fmt.Errorf("unable to reliably send %v for "+
"channel=%v to peer=%x: %v", upd.MsgType(),
@@ -3346,8 +3367,11 @@ func (d *AuthenticatedGossiper) handleChanUpdate(nMsg *networkMsg,
}
// handleAnnSig processes a new announcement signatures message.
func (d *AuthenticatedGossiper) handleAnnSig(nMsg *networkMsg,
ann *lnwire.AnnounceSignatures1) ([]networkMsg, bool) {
//
//nolint:funlen
func (d *AuthenticatedGossiper) handleAnnSig(ctx context.Context,
nMsg *networkMsg, ann *lnwire.AnnounceSignatures1) ([]networkMsg,
bool) {
needBlockHeight := ann.ShortChannelID.BlockHeight +
d.cfg.ProofMatureDelta
@@ -3446,7 +3470,7 @@ func (d *AuthenticatedGossiper) handleAnnSig(nMsg *networkMsg,
// Since the remote peer might not be online we'll call a
// method that will attempt to deliver the proof when it comes
// online.
err := d.reliableSender.sendMessage(ann, remotePubKey)
err := d.reliableSender.sendMessage(ctx, ann, remotePubKey)
if err != nil {
err := fmt.Errorf("unable to reliably send %v for "+
"channel=%v to peer=%x: %v", ann.MsgType(),
@@ -3631,7 +3655,7 @@ func (d *AuthenticatedGossiper) handleAnnSig(nMsg *networkMsg,
// it since the source gets skipped. This isn't necessary for channel
// updates and announcement signatures since we send those directly to
// our channel counterparty through the gossiper's reliable sender.
node1Ann, err := d.fetchNodeAnn(chanInfo.NodeKey1Bytes)
node1Ann, err := d.fetchNodeAnn(ctx, chanInfo.NodeKey1Bytes)
if err != nil {
log.Debugf("Unable to fetch node announcement for %x: %v",
chanInfo.NodeKey1Bytes, err)
@@ -3645,7 +3669,7 @@ func (d *AuthenticatedGossiper) handleAnnSig(nMsg *networkMsg,
}
}
node2Ann, err := d.fetchNodeAnn(chanInfo.NodeKey2Bytes)
node2Ann, err := d.fetchNodeAnn(ctx, chanInfo.NodeKey2Bytes)
if err != nil {
log.Debugf("Unable to fetch node announcement for %x: %v",
chanInfo.NodeKey2Bytes, err)
@@ -3700,7 +3724,7 @@ func (d *AuthenticatedGossiper) ShouldDisconnect(pubkey *btcec.PublicKey) (
// transaction from chain to ensure that it exists, is not spent and matches
// the channel announcement proof. The transaction's outpoint and value are
// returned if we can glean them from the work done in this method.
func (d *AuthenticatedGossiper) validateFundingTransaction(
func (d *AuthenticatedGossiper) validateFundingTransaction(_ context.Context,
ann *lnwire.ChannelAnnouncement1,
tapscriptRoot fn.Option[chainhash.Hash]) (wire.OutPoint, btcutil.Amount,
[]byte, error) {
+423 -348
View File
File diff suppressed because it is too large. Load diff
+23 -12
View File
@@ -1,8 +1,10 @@
package discovery
import (
"context"
"sync"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/lnpeer"
"github.com/lightningnetwork/lnd/lnwire"
)
@@ -28,7 +30,7 @@ type reliableSenderCfg struct {
// IsMsgStale determines whether a message retrieved from the backing
// MessageStore is seen as stale by the current graph.
IsMsgStale func(lnwire.Message) bool
IsMsgStale func(context.Context, lnwire.Message) bool
}
// peerManager contains the set of channels required for the peerHandler to
@@ -59,8 +61,9 @@ type reliableSender struct {
activePeers map[[33]byte]peerManager
activePeersMtx sync.Mutex
wg sync.WaitGroup
quit chan struct{}
wg sync.WaitGroup
quit chan struct{}
cancel fn.Option[context.CancelFunc]
}
// newReliableSender returns a new reliableSender backed by the given config.
@@ -76,7 +79,10 @@ func newReliableSender(cfg *reliableSenderCfg) *reliableSender {
func (s *reliableSender) Start() error {
var err error
s.start.Do(func() {
err = s.resendPendingMsgs()
ctx, cancel := context.WithCancel(context.Background())
s.cancel = fn.Some(cancel)
err = s.resendPendingMsgs(ctx)
})
return err
}
@@ -87,6 +93,7 @@ func (s *reliableSender) Stop() {
log.Debugf("reliableSender is stopping")
defer log.Debugf("reliableSender stopped")
s.cancel.WhenSome(func(fn context.CancelFunc) { fn() })
close(s.quit)
s.wg.Wait()
})
@@ -96,7 +103,9 @@ func (s *reliableSender) Stop() {
// event that the peer is currently offline, this will only write the message to
// disk. Once the peer reconnects, this message, along with any others pending,
// will be sent to the peer.
func (s *reliableSender) sendMessage(msg lnwire.Message, peerPubKey [33]byte) error {
func (s *reliableSender) sendMessage(ctx context.Context, msg lnwire.Message,
peerPubKey [33]byte) error {
// We'll start by persisting the message to disk. This allows us to
// resend the message upon restarts and peer reconnections.
if err := s.cfg.MessageStore.AddMessage(msg, peerPubKey); err != nil {
@@ -106,7 +115,7 @@ func (s *reliableSender) sendMessage(msg lnwire.Message, peerPubKey [33]byte) er
// Then, we'll spawn a peerHandler for this peer to handle resending its
// pending messages while taking into account its connection lifecycle.
spawnHandler:
msgHandler, ok := s.spawnPeerHandler(peerPubKey)
msgHandler, ok := s.spawnPeerHandler(ctx, peerPubKey)
// If the handler wasn't previously active, we can exit now as we know
// that the message will be sent once the peer online notification is
@@ -134,7 +143,7 @@ spawnHandler:
// spawnPeerMsgHandler spawns a peerHandler for the given peer if there isn't
// one already active. The boolean returned signals whether there was already
// one active or not.
func (s *reliableSender) spawnPeerHandler(
func (s *reliableSender) spawnPeerHandler(ctx context.Context,
peerPubKey [33]byte) (peerManager, bool) {
s.activePeersMtx.Lock()
@@ -152,7 +161,7 @@ func (s *reliableSender) spawnPeerHandler(
// peerHandler.
if !ok {
s.wg.Add(1)
go s.peerHandler(msgHandler, peerPubKey)
go s.peerHandler(ctx, msgHandler, peerPubKey)
}
return msgHandler, ok
@@ -164,7 +173,9 @@ func (s *reliableSender) spawnPeerHandler(
// offline will be queued and sent once the peer reconnects.
//
// NOTE: This must be run as a goroutine.
func (s *reliableSender) peerHandler(peerMgr peerManager, peerPubKey [33]byte) {
func (s *reliableSender) peerHandler(ctx context.Context, peerMgr peerManager,
peerPubKey [33]byte) {
defer s.wg.Done()
// We'll start by requesting a notification for when the peer
@@ -252,7 +263,7 @@ out:
// check whether it's stale. This guarantees that
// AnnounceSignatures are sent at least once if we happen to
// already have signatures for both parties.
if s.cfg.IsMsgStale(msg) {
if s.cfg.IsMsgStale(ctx, msg) {
err := s.cfg.MessageStore.DeleteMessage(msg, peerPubKey)
if err != nil {
log.Errorf("Unable to remove stale %v message "+
@@ -321,7 +332,7 @@ out:
// resendPendingMsgs retrieves and sends all of the messages within the message
// store that should be reliably sent to their respective peers.
func (s *reliableSender) resendPendingMsgs() error {
func (s *reliableSender) resendPendingMsgs(ctx context.Context) error {
// Fetch all of the peers for which we have pending messages for and
// spawn a peerMsgHandler for each. Once the peer is seen as online, all
// of the pending messages will be sent.
@@ -331,7 +342,7 @@ func (s *reliableSender) resendPendingMsgs() error {
}
for peer := range peers {
s.spawnPeerHandler(peer)
s.spawnPeerHandler(ctx, peer)
}
return nil
+22 -19
View File
@@ -1,6 +1,7 @@
package discovery
import (
"context"
"fmt"
"sync/atomic"
"testing"
@@ -11,6 +12,7 @@ import (
"github.com/lightningnetwork/lnd/lnpeer"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
)
// newTestReliableSender creates a new reliable sender instance used for
@@ -32,7 +34,7 @@ func newTestReliableSender(t *testing.T) *reliableSender {
return c
},
MessageStore: newMockMessageStore(),
IsMsgStale: func(lnwire.Message) bool {
IsMsgStale: func(context.Context, lnwire.Message) bool {
return false
},
}
@@ -69,6 +71,7 @@ func assertMsgsSent(t *testing.T, msgChan chan lnwire.Message,
// a peer while taking into account its connection lifecycle works as expected.
func TestReliableSenderFlow(t *testing.T) {
t.Parallel()
ctx := context.Background()
reliableSender := newTestReliableSender(t)
@@ -98,9 +101,8 @@ func TestReliableSenderFlow(t *testing.T) {
msg1 := randChannelUpdate()
var peerPubKey [33]byte
copy(peerPubKey[:], pubKey.SerializeCompressed())
if err := reliableSender.sendMessage(msg1, peerPubKey); err != nil {
t.Fatalf("unable to reliably send message: %v", err)
}
err := reliableSender.sendMessage(ctx, msg1, peerPubKey)
require.NoError(t, err)
// Since there isn't a peerHandler for this peer currently active due to
// this being the first message being sent reliably, we should expect to
@@ -114,9 +116,8 @@ func TestReliableSenderFlow(t *testing.T) {
// We'll then attempt to send another additional message reliably.
msg2 := randAnnounceSignatures()
if err := reliableSender.sendMessage(msg2, peerPubKey); err != nil {
t.Fatalf("unable to reliably send message: %v", err)
}
err = reliableSender.sendMessage(ctx, msg2, peerPubKey)
require.NoError(t, err)
// This should not however request another peer online notification as
// the peerHandler has already been started and is waiting for the
@@ -145,9 +146,8 @@ func TestReliableSenderFlow(t *testing.T) {
// Then, we'll send one more message reliably.
msg3 := randChannelUpdate()
if err := reliableSender.sendMessage(msg3, peerPubKey); err != nil {
t.Fatalf("unable to reliably send message: %v", err)
}
err = reliableSender.sendMessage(ctx, msg3, peerPubKey)
require.NoError(t, err)
// Again, this should not request another peer online notification
// request since we are currently waiting for the peer to be offline.
@@ -188,6 +188,7 @@ func TestReliableSenderFlow(t *testing.T) {
// them as stale.
func TestReliableSenderStaleMessages(t *testing.T) {
t.Parallel()
ctx := context.Background()
reliableSender := newTestReliableSender(t)
@@ -206,7 +207,9 @@ func TestReliableSenderStaleMessages(t *testing.T) {
// We'll also override IsMsgStale to mark all messages as stale as we're
// interested in testing the stale message behavior.
reliableSender.cfg.IsMsgStale = func(_ lnwire.Message) bool {
reliableSender.cfg.IsMsgStale = func(_ context.Context,
_ lnwire.Message) bool {
return true
}
@@ -215,9 +218,8 @@ func TestReliableSenderStaleMessages(t *testing.T) {
msg1 := randAnnounceSignatures()
var peerPubKey [33]byte
copy(peerPubKey[:], pubKey.SerializeCompressed())
if err := reliableSender.sendMessage(msg1, peerPubKey); err != nil {
t.Fatalf("unable to reliably send message: %v", err)
}
err := reliableSender.sendMessage(ctx, msg1, peerPubKey)
require.NoError(t, err)
// Since there isn't a peerHandler for this peer currently active due to
// this being the first message being sent reliably, we should expect to
@@ -245,7 +247,7 @@ func TestReliableSenderStaleMessages(t *testing.T) {
// message store since it is seen as stale and has been sent at least
// once. Once the message is removed, the peerHandler should be torn
// down as there are no longer any pending messages within the store.
err := wait.NoError(func() error {
err = wait.NoError(func() error {
msgs, err := reliableSender.cfg.MessageStore.MessagesForPeer(
peerPubKey,
)
@@ -265,14 +267,15 @@ func TestReliableSenderStaleMessages(t *testing.T) {
}
// Override IsMsgStale to no longer mark messages as stale.
reliableSender.cfg.IsMsgStale = func(_ lnwire.Message) bool {
reliableSender.cfg.IsMsgStale = func(_ context.Context,
_ lnwire.Message) bool {
return false
}
// We'll request the message to be sent reliably.
if err := reliableSender.sendMessage(msg2, peerPubKey); err != nil {
t.Fatalf("unable to reliably send message: %v", err)
}
err = reliableSender.sendMessage(ctx, msg2, peerPubKey)
require.NoError(t, err)
// We should see an online notification request indicating that a new
// peerHandler has been spawned since it was previously torn down.
+62 -40
View File
@@ -409,16 +409,18 @@ func (g *GossipSyncer) Start() {
g.started.Do(func() {
log.Debugf("Starting GossipSyncer(%x)", g.cfg.peerPub[:])
ctx, _ := g.cg.Create(context.Background())
// TODO(conner): only spawn channelGraphSyncer if remote
// supports gossip queries, and only spawn replyHandler if we
// advertise support
if !g.cfg.noSyncChannels {
g.cg.WgAdd(1)
go g.channelGraphSyncer()
go g.channelGraphSyncer(ctx)
}
if !g.cfg.noReplyQueries {
g.cg.WgAdd(1)
go g.replyHandler()
go g.replyHandler(ctx)
}
})
}
@@ -437,9 +439,11 @@ func (g *GossipSyncer) Stop() {
// handleSyncingChans handles the state syncingChans for the GossipSyncer. When
// in this state, we will send a QueryChannelRange msg to our peer and advance
// the syncer's state to waitingQueryRangeReply.
func (g *GossipSyncer) handleSyncingChans() {
func (g *GossipSyncer) handleSyncingChans(ctx context.Context) {
// Prepare the query msg.
queryRangeMsg, err := g.genChanRangeQuery(g.genHistoricalChanRangeQuery)
queryRangeMsg, err := g.genChanRangeQuery(
ctx, g.genHistoricalChanRangeQuery,
)
if err != nil {
log.Errorf("Unable to gen chan range query: %v", err)
return
@@ -456,7 +460,6 @@ func (g *GossipSyncer) handleSyncingChans() {
// Send the msg to the remote peer, which is non-blocking as
// `sendToPeer` only queues the msg in Brontide.
ctx, _ := g.cg.Create(context.Background())
err = g.cfg.sendToPeer(ctx, queryRangeMsg)
if err != nil {
log.Errorf("Unable to send chan range query: %v", err)
@@ -471,7 +474,7 @@ func (g *GossipSyncer) handleSyncingChans() {
// channelGraphSyncer is the main goroutine responsible for ensuring that we
// properly channel graph state with the remote peer, and also that we only
// send them messages which actually pass their defined update horizon.
func (g *GossipSyncer) channelGraphSyncer() {
func (g *GossipSyncer) channelGraphSyncer(ctx context.Context) {
defer g.cg.WgDone()
for {
@@ -488,7 +491,7 @@ func (g *GossipSyncer) channelGraphSyncer() {
// understand, as we'll as responding to any other queries by
// them.
case syncingChans:
g.handleSyncingChans()
g.handleSyncingChans(ctx)
// In this state, we've sent out our initial channel range
// query and are waiting for the final response from the remote
@@ -507,7 +510,9 @@ func (g *GossipSyncer) channelGraphSyncer() {
// for the new channels.
queryReply, ok := msg.(*lnwire.ReplyChannelRange)
if ok {
err := g.processChanRangeReply(queryReply)
err := g.processChanRangeReply(
ctx, queryReply,
)
if err != nil {
log.Errorf("Unable to "+
"process chan range "+
@@ -522,6 +527,9 @@ func (g *GossipSyncer) channelGraphSyncer() {
case <-g.cg.Done():
return
case <-ctx.Done():
return
}
// We'll enter this state once we've discovered which channels
@@ -531,7 +539,7 @@ func (g *GossipSyncer) channelGraphSyncer() {
// First, we'll attempt to continue our channel
// synchronization by continuing to send off another
// query chunk.
done := g.synchronizeChanIDs()
done := g.synchronizeChanIDs(ctx)
// If this wasn't our last query, then we'll need to
// transition to our waiting state.
@@ -572,6 +580,9 @@ func (g *GossipSyncer) channelGraphSyncer() {
case <-g.cg.Done():
return
case <-ctx.Done():
return
}
// This is our final terminal state where we'll only reply to
@@ -591,7 +602,7 @@ func (g *GossipSyncer) channelGraphSyncer() {
syncType.IsActiveSync() {
err := g.sendGossipTimestampRange(
time.Now(), math.MaxUint32,
ctx, time.Now(), math.MaxUint32,
)
if err != nil {
log.Errorf("Unable to send update "+
@@ -611,13 +622,16 @@ func (g *GossipSyncer) channelGraphSyncer() {
case syncerIdle:
select {
case req := <-g.syncTransitionReqs:
req.errChan <- g.handleSyncTransition(req)
req.errChan <- g.handleSyncTransition(ctx, req)
case req := <-g.historicalSyncReqs:
g.handleHistoricalSync(req)
case <-g.cg.Done():
return
case <-ctx.Done():
return
}
}
}
@@ -630,13 +644,13 @@ func (g *GossipSyncer) channelGraphSyncer() {
// from the state machine maintained on the same node.
//
// NOTE: This method MUST be run as a goroutine.
func (g *GossipSyncer) replyHandler() {
func (g *GossipSyncer) replyHandler(ctx context.Context) {
defer g.cg.WgDone()
for {
select {
case msg := <-g.queryMsgs:
err := g.replyPeerQueries(msg)
err := g.replyPeerQueries(ctx, msg)
switch {
case err == ErrGossipSyncerExiting:
return
@@ -651,14 +665,17 @@ func (g *GossipSyncer) replyHandler() {
case <-g.cg.Done():
return
case <-ctx.Done():
return
}
}
}
// sendGossipTimestampRange constructs and sets a GossipTimestampRange for the
// syncer and sends it to the remote peer.
func (g *GossipSyncer) sendGossipTimestampRange(firstTimestamp time.Time,
timestampRange uint32) error {
func (g *GossipSyncer) sendGossipTimestampRange(ctx context.Context,
firstTimestamp time.Time, timestampRange uint32) error {
endTimestamp := firstTimestamp.Add(
time.Duration(timestampRange) * time.Second,
@@ -673,7 +690,6 @@ func (g *GossipSyncer) sendGossipTimestampRange(firstTimestamp time.Time,
TimestampRange: timestampRange,
}
ctx, _ := g.cg.Create(context.Background())
if err := g.cfg.sendToPeer(ctx, localUpdateHorizon); err != nil {
return err
}
@@ -693,7 +709,7 @@ func (g *GossipSyncer) sendGossipTimestampRange(firstTimestamp time.Time,
// been queried for with a response received. We'll chunk our requests as
// required to ensure they fit into a single message. We may re-renter this
// state in the case that chunking is required.
func (g *GossipSyncer) synchronizeChanIDs() bool {
func (g *GossipSyncer) synchronizeChanIDs(ctx context.Context) bool {
// If we're in this state yet there are no more new channels to query
// for, then we'll transition to our final synced state and return true
// to signal that we're fully synchronized.
@@ -730,7 +746,6 @@ func (g *GossipSyncer) synchronizeChanIDs() bool {
// With our chunk obtained, we'll send over our next query, then return
// false indicating that we're net yet fully synced.
ctx, _ := g.cg.Create(context.Background())
err := g.cfg.sendToPeer(ctx, &lnwire.QueryShortChanIDs{
ChainHash: g.cfg.chainHash,
EncodingType: lnwire.EncodingSortedPlain,
@@ -759,7 +774,9 @@ func isLegacyReplyChannelRange(query *lnwire.QueryChannelRange,
// processChanRangeReply is called each time the GossipSyncer receives a new
// reply to the initial range query to discover new channels that it didn't
// previously know of.
func (g *GossipSyncer) processChanRangeReply(msg *lnwire.ReplyChannelRange) error {
func (g *GossipSyncer) processChanRangeReply(_ context.Context,
msg *lnwire.ReplyChannelRange) error {
// isStale returns whether the timestamp is too far into the past.
isStale := func(timestamp time.Time) bool {
return time.Since(timestamp) > graph.DefaultChannelPruneExpiry
@@ -948,7 +965,7 @@ func (g *GossipSyncer) processChanRangeReply(msg *lnwire.ReplyChannelRange) erro
// party when we're kicking off the channel graph synchronization upon
// connection. The historicalQuery boolean can be used to generate a query from
// the genesis block of the chain.
func (g *GossipSyncer) genChanRangeQuery(
func (g *GossipSyncer) genChanRangeQuery(_ context.Context,
historicalQuery bool) (*lnwire.QueryChannelRange, error) {
// First, we'll query our channel graph time series for its highest
@@ -1005,18 +1022,20 @@ func (g *GossipSyncer) genChanRangeQuery(
// replyPeerQueries is called in response to any query by the remote peer.
// We'll examine our state and send back our best response.
func (g *GossipSyncer) replyPeerQueries(msg lnwire.Message) error {
func (g *GossipSyncer) replyPeerQueries(ctx context.Context,
msg lnwire.Message) error {
switch msg := msg.(type) {
// In this state, we'll also handle any incoming channel range queries
// from the remote peer as they're trying to sync their state as well.
case *lnwire.QueryChannelRange:
return g.replyChanRangeQuery(msg)
return g.replyChanRangeQuery(ctx, msg)
// If the remote peer skips straight to requesting new channels that
// they don't know of, then we'll ensure that we also handle this case.
case *lnwire.QueryShortChanIDs:
return g.replyShortChanIDs(msg)
return g.replyShortChanIDs(ctx, msg)
default:
return fmt.Errorf("unknown message: %T", msg)
@@ -1028,7 +1047,9 @@ func (g *GossipSyncer) replyPeerQueries(msg lnwire.Message) error {
// meet the channel range, then chunk our responses to the remote node. We also
// ensure that our final fragment carries the "complete" bit to indicate the
// end of our streaming response.
func (g *GossipSyncer) replyChanRangeQuery(query *lnwire.QueryChannelRange) error {
func (g *GossipSyncer) replyChanRangeQuery(ctx context.Context,
query *lnwire.QueryChannelRange) error {
// Before responding, we'll check to ensure that the remote peer is
// querying for the same chain that we're on. If not, we'll send back a
// response with a complete value of zero to indicate we're on a
@@ -1038,8 +1059,6 @@ func (g *GossipSyncer) replyChanRangeQuery(query *lnwire.QueryChannelRange) erro
"chain=%v, we're on chain=%v", query.ChainHash,
g.cfg.chainHash)
ctx, _ := g.cg.Create(context.Background())
return g.cfg.sendToPeerSync(ctx, &lnwire.ReplyChannelRange{
ChainHash: query.ChainHash,
FirstBlockHeight: query.FirstBlockHeight,
@@ -1113,8 +1132,6 @@ func (g *GossipSyncer) replyChanRangeQuery(query *lnwire.QueryChannelRange) erro
)
}
ctx, _ := g.cg.Create(context.Background())
return g.cfg.sendToPeerSync(ctx, &lnwire.ReplyChannelRange{
ChainHash: query.ChainHash,
NumBlocks: numBlocks,
@@ -1209,7 +1226,9 @@ func (g *GossipSyncer) replyChanRangeQuery(query *lnwire.QueryChannelRange) erro
// node for information concerning a set of short channel ID's. Our response
// will be sent in a streaming chunked manner to ensure that we remain below
// the current transport level message size.
func (g *GossipSyncer) replyShortChanIDs(query *lnwire.QueryShortChanIDs) error {
func (g *GossipSyncer) replyShortChanIDs(ctx context.Context,
query *lnwire.QueryShortChanIDs) error {
// Before responding, we'll check to ensure that the remote peer is
// querying for the same chain that we're on. If not, we'll send back a
// response with a complete value of zero to indicate we're on a
@@ -1219,8 +1238,6 @@ func (g *GossipSyncer) replyShortChanIDs(query *lnwire.QueryShortChanIDs) error
"chain=%v, we're on chain=%v", query.ChainHash,
g.cfg.chainHash)
ctx, _ := g.cg.Create(context.Background())
return g.cfg.sendToPeerSync(ctx, &lnwire.ReplyShortChanIDsEnd{
ChainHash: query.ChainHash,
Complete: 0,
@@ -1252,7 +1269,6 @@ func (g *GossipSyncer) replyShortChanIDs(query *lnwire.QueryShortChanIDs) error
// each one individually and synchronously to throttle the sends and
// perform buffering of responses in the syncer as opposed to the peer.
for _, msg := range replyMsgs {
ctx, _ := g.cg.Create(context.Background())
err := g.cfg.sendToPeerSync(ctx, msg)
if err != nil {
return err
@@ -1261,8 +1277,6 @@ func (g *GossipSyncer) replyShortChanIDs(query *lnwire.QueryShortChanIDs) error
// Regardless of whether we had any messages to reply with, send over
// the sentinel message to signal that the stream has terminated.
ctx, _ := g.cg.Create(context.Background())
return g.cfg.sendToPeerSync(ctx, &lnwire.ReplyShortChanIDsEnd{
ChainHash: query.ChainHash,
Complete: 1,
@@ -1272,7 +1286,9 @@ func (g *GossipSyncer) replyShortChanIDs(query *lnwire.QueryShortChanIDs) error
// ApplyGossipFilter applies a gossiper filter sent by the remote node to the
// state machine. Once applied, we'll ensure that we don't forward any messages
// to the peer that aren't within the time range of the filter.
func (g *GossipSyncer) ApplyGossipFilter(filter *lnwire.GossipTimestampRange) error {
func (g *GossipSyncer) ApplyGossipFilter(ctx context.Context,
filter *lnwire.GossipTimestampRange) error {
g.Lock()
g.remoteUpdateHorizon = filter
@@ -1294,6 +1310,8 @@ func (g *GossipSyncer) ApplyGossipFilter(filter *lnwire.GossipTimestampRange) er
case <-g.syncerSema:
case <-g.cg.Done():
return ErrGossipSyncerExiting
case <-ctx.Done():
return ctx.Err()
}
// We don't put this in a defer because if the goroutine is launched,
@@ -1329,7 +1347,6 @@ func (g *GossipSyncer) ApplyGossipFilter(filter *lnwire.GossipTimestampRange) er
defer returnSema()
for _, msg := range newUpdatestoSend {
ctx, _ := g.cg.Create(context.Background())
err := g.cfg.sendToPeerSync(ctx, msg)
switch {
case err == ErrGossipSyncerExiting:
@@ -1351,7 +1368,9 @@ func (g *GossipSyncer) ApplyGossipFilter(filter *lnwire.GossipTimestampRange) er
// FilterGossipMsgs takes a set of gossip messages, and only send it to a peer
// iff the message is within the bounds of their set gossip filter. If the peer
// doesn't have a gossip filter set, then no messages will be forwarded.
func (g *GossipSyncer) FilterGossipMsgs(msgs ...msgWithSenders) {
func (g *GossipSyncer) FilterGossipMsgs(ctx context.Context,
msgs ...msgWithSenders) {
// If the peer doesn't have an update horizon set, then we won't send
// it any new update messages.
if g.remoteUpdateHorizon == nil {
@@ -1365,6 +1384,8 @@ func (g *GossipSyncer) FilterGossipMsgs(msgs ...msgWithSenders) {
select {
case <-g.cg.Done():
return
case <-ctx.Done():
return
default:
}
@@ -1472,7 +1493,6 @@ func (g *GossipSyncer) FilterGossipMsgs(msgs ...msgWithSenders) {
return
}
ctx, _ := g.cg.Create(context.Background())
if err = g.cfg.sendToPeer(ctx, msgsToSend...); err != nil {
log.Errorf("unable to send gossip msgs: %v", err)
}
@@ -1573,7 +1593,9 @@ func (g *GossipSyncer) ProcessSyncTransition(newSyncType SyncerType) error {
//
// NOTE: The gossip syncer might have another sync state as a result of this
// transition.
func (g *GossipSyncer) handleSyncTransition(req *syncTransitionReq) error {
func (g *GossipSyncer) handleSyncTransition(ctx context.Context,
req *syncTransitionReq) error {
// Return early from any NOP sync transitions.
syncType := g.SyncType()
if syncType == req.newSyncType {
@@ -1608,7 +1630,7 @@ func (g *GossipSyncer) handleSyncTransition(req *syncTransitionReq) error {
req.newSyncType)
}
err := g.sendGossipTimestampRange(firstTimestamp, timestampRange)
err := g.sendGossipTimestampRange(ctx, firstTimestamp, timestampRange)
if err != nil {
return fmt.Errorf("unable to send local update horizon: %w",
err)
+33 -20
View File
@@ -228,6 +228,7 @@ func newTestSyncer(hID lnwire.ShortChannelID,
// doesn't have a horizon set, then we won't send any incoming messages to it.
func TestGossipSyncerFilterGossipMsgsNoHorizon(t *testing.T) {
t.Parallel()
ctx := context.Background()
// First, we'll create a GossipSyncer instance with a canned sendToPeer
// message to allow us to intercept their potential sends.
@@ -249,7 +250,7 @@ func TestGossipSyncerFilterGossipMsgsNoHorizon(t *testing.T) {
// We'll then attempt to filter the set of messages through the target
// peer.
syncer.FilterGossipMsgs(msgs...)
syncer.FilterGossipMsgs(ctx, msgs...)
// As the remote peer doesn't yet have a gossip timestamp set, we
// shouldn't receive any outbound messages.
@@ -273,6 +274,7 @@ func unixStamp(a int64) uint32 {
// channel ann that already has a channel update on disk.
func TestGossipSyncerFilterGossipMsgsAllInMemory(t *testing.T) {
t.Parallel()
ctx := context.Background()
// First, we'll create a GossipSyncer instance with a canned sendToPeer
// message to allow us to intercept their potential sends.
@@ -384,7 +386,7 @@ func TestGossipSyncerFilterGossipMsgsAllInMemory(t *testing.T) {
}()
// We'll then instruct the gossiper to filter this set of messages.
syncer.FilterGossipMsgs(msgs...)
syncer.FilterGossipMsgs(ctx, msgs...)
// Out of all the messages we sent in, we should only get 2 of them
// back.
@@ -415,6 +417,7 @@ func TestGossipSyncerFilterGossipMsgsAllInMemory(t *testing.T) {
// messages which are within their desired time horizon.
func TestGossipSyncerApplyNoHistoricalGossipFilter(t *testing.T) {
t.Parallel()
ctx := context.Background()
// First, we'll create a GossipSyncer instance with a canned sendToPeer
// message to allow us to intercept their potential sends.
@@ -451,7 +454,7 @@ func TestGossipSyncerApplyNoHistoricalGossipFilter(t *testing.T) {
}()
// We'll now attempt to apply the gossip filter for the remote peer.
syncer.ApplyGossipFilter(remoteHorizon)
require.NoError(t, syncer.ApplyGossipFilter(ctx, remoteHorizon))
// Ensure that the syncer's remote horizon was properly updated.
if !reflect.DeepEqual(syncer.remoteUpdateHorizon, remoteHorizon) {
@@ -475,6 +478,7 @@ func TestGossipSyncerApplyNoHistoricalGossipFilter(t *testing.T) {
// within their desired time horizon.
func TestGossipSyncerApplyGossipFilter(t *testing.T) {
t.Parallel()
ctx := context.Background()
// First, we'll create a GossipSyncer instance with a canned sendToPeer
// message to allow us to intercept their potential sends.
@@ -515,7 +519,7 @@ func TestGossipSyncerApplyGossipFilter(t *testing.T) {
}()
// We'll now attempt to apply the gossip filter for the remote peer.
err := syncer.ApplyGossipFilter(remoteHorizon)
err := syncer.ApplyGossipFilter(ctx, remoteHorizon)
require.NoError(t, err, "unable to apply filter")
// There should be no messages in the message queue as we didn't send
@@ -563,7 +567,7 @@ func TestGossipSyncerApplyGossipFilter(t *testing.T) {
errCh <- nil
}
}()
err = syncer.ApplyGossipFilter(remoteHorizon)
err = syncer.ApplyGossipFilter(ctx, remoteHorizon)
require.NoError(t, err, "unable to apply filter")
// We should get back the exact same message.
@@ -594,6 +598,7 @@ func TestGossipSyncerApplyGossipFilter(t *testing.T) {
// channels and complete=0.
func TestGossipSyncerQueryChannelRangeWrongChainHash(t *testing.T) {
t.Parallel()
ctx := context.Background()
// First, we'll create a GossipSyncer instance with a canned sendToPeer
// message to allow us to intercept their potential sends.
@@ -609,7 +614,7 @@ func TestGossipSyncerQueryChannelRangeWrongChainHash(t *testing.T) {
FirstBlockHeight: 0,
NumBlocks: math.MaxUint32,
}
err := syncer.replyChanRangeQuery(query)
err := syncer.replyChanRangeQuery(ctx, query)
require.NoError(t, err, "unable to process short chan ID's")
select {
@@ -646,6 +651,7 @@ func TestGossipSyncerQueryChannelRangeWrongChainHash(t *testing.T) {
// complete=0.
func TestGossipSyncerReplyShortChanIDsWrongChainHash(t *testing.T) {
t.Parallel()
ctx := context.Background()
// First, we'll create a GossipSyncer instance with a canned sendToPeer
// message to allow us to intercept their potential sends.
@@ -656,7 +662,7 @@ func TestGossipSyncerReplyShortChanIDsWrongChainHash(t *testing.T) {
// We'll now ask the syncer to reply to a chan ID query, but for a
// chain that it isn't aware of.
err := syncer.replyShortChanIDs(&lnwire.QueryShortChanIDs{
err := syncer.replyShortChanIDs(ctx, &lnwire.QueryShortChanIDs{
ChainHash: *chaincfg.SimNetParams.GenesisHash,
})
require.NoError(t, err, "unable to process short chan ID's")
@@ -695,6 +701,7 @@ func TestGossipSyncerReplyShortChanIDsWrongChainHash(t *testing.T) {
// announcements, as well as an ending ReplyShortChanIDsEnd message.
func TestGossipSyncerReplyShortChanIDs(t *testing.T) {
t.Parallel()
ctx := context.Background()
// First, we'll create a GossipSyncer instance with a canned sendToPeer
// message to allow us to intercept their potential sends.
@@ -745,7 +752,7 @@ func TestGossipSyncerReplyShortChanIDs(t *testing.T) {
// With our set up above complete, we'll now attempt to obtain a reply
// from the channel syncer for our target chan ID query.
err := syncer.replyShortChanIDs(&lnwire.QueryShortChanIDs{
err := syncer.replyShortChanIDs(ctx, &lnwire.QueryShortChanIDs{
ShortChanIDs: queryChanIDs,
})
require.NoError(t, err, "unable to query for chan IDs")
@@ -800,6 +807,7 @@ func TestGossipSyncerReplyShortChanIDs(t *testing.T) {
// the remote peer.
func TestGossipSyncerReplyChanRangeQuery(t *testing.T) {
t.Parallel()
ctx := context.Background()
// We'll use a smaller chunk size so we can easily test all the edge
// cases.
@@ -866,7 +874,7 @@ func TestGossipSyncerReplyChanRangeQuery(t *testing.T) {
}()
// With our goroutine active, we'll now issue the query.
if err := syncer.replyChanRangeQuery(query); err != nil {
if err := syncer.replyChanRangeQuery(ctx, query); err != nil {
t.Fatalf("unable to issue query: %v", err)
}
@@ -971,6 +979,7 @@ func TestGossipSyncerReplyChanRangeQuery(t *testing.T) {
// executed with the correct block range.
func TestGossipSyncerReplyChanRangeQueryBlockRange(t *testing.T) {
t.Parallel()
ctx := context.Background()
// First create our test gossip syncer that will handle and
// respond to the test queries
@@ -1052,7 +1061,8 @@ func TestGossipSyncerReplyChanRangeQueryBlockRange(t *testing.T) {
// will be reached
go func() {
for _, query := range queryReqs {
if err := syncer.replyChanRangeQuery(query); err != nil {
err := syncer.replyChanRangeQuery(ctx, query)
if err != nil {
errCh <- fmt.Errorf("unable to issue query: %w",
err)
return
@@ -1083,6 +1093,7 @@ func TestGossipSyncerReplyChanRangeQueryBlockRange(t *testing.T) {
// back a single response that signals completion.
func TestGossipSyncerReplyChanRangeQueryNoNewChans(t *testing.T) {
t.Parallel()
ctx := context.Background()
// We'll now create our test gossip syncer that will shortly respond to
// our canned query.
@@ -1121,7 +1132,7 @@ func TestGossipSyncerReplyChanRangeQueryNoNewChans(t *testing.T) {
}()
// With our goroutine active, we'll now issue the query.
if err := syncer.replyChanRangeQuery(query); err != nil {
if err := syncer.replyChanRangeQuery(ctx, query); err != nil {
t.Fatalf("unable to issue query: %v", err)
}
@@ -1162,6 +1173,7 @@ func TestGossipSyncerReplyChanRangeQueryNoNewChans(t *testing.T) {
// channel ID, we properly generate an correct initial channel range response.
func TestGossipSyncerGenChanRangeQuery(t *testing.T) {
t.Parallel()
ctx := context.Background()
// First, we'll create a GossipSyncer instance with a canned sendToPeer
// message to allow us to intercept their potential sends.
@@ -1174,7 +1186,7 @@ func TestGossipSyncerGenChanRangeQuery(t *testing.T) {
// If we now ask the syncer to generate an initial range query, it
// should return a start height that's back chanRangeQueryBuffer
// blocks.
rangeQuery, err := syncer.genChanRangeQuery(false)
rangeQuery, err := syncer.genChanRangeQuery(ctx, false)
require.NoError(t, err, "unable to resp")
firstHeight := uint32(startingHeight - chanRangeQueryBuffer)
@@ -1190,7 +1202,7 @@ func TestGossipSyncerGenChanRangeQuery(t *testing.T) {
// Generating a historical range query should result in a start height
// of 0.
rangeQuery, err = syncer.genChanRangeQuery(true)
rangeQuery, err = syncer.genChanRangeQuery(ctx, true)
require.NoError(t, err, "unable to resp")
if rangeQuery.FirstBlockHeight != 0 {
t.Fatalf("incorrect chan range query: expected %v, %v", 0,
@@ -1222,6 +1234,7 @@ func TestGossipSyncerProcessChanRangeReply(t *testing.T) {
// each reply instead.
func testGossipSyncerProcessChanRangeReply(t *testing.T, legacy bool) {
t.Parallel()
ctx := context.Background()
// First, we'll create a GossipSyncer instance with a canned sendToPeer
// message to allow us to intercept their potential sends.
@@ -1234,7 +1247,7 @@ func testGossipSyncerProcessChanRangeReply(t *testing.T, legacy bool) {
startingState := syncer.state
query, err := syncer.genChanRangeQuery(true)
query, err := syncer.genChanRangeQuery(ctx, true)
require.NoError(t, err, "unable to generate channel range query")
currentTimestamp := time.Now().Unix()
@@ -1359,13 +1372,13 @@ func testGossipSyncerProcessChanRangeReply(t *testing.T, legacy bool) {
// We'll begin by sending the syncer a set of non-complete channel
// range replies.
if err := syncer.processChanRangeReply(replies[0]); err != nil {
if err := syncer.processChanRangeReply(ctx, replies[0]); err != nil {
t.Fatalf("unable to process reply: %v", err)
}
if err := syncer.processChanRangeReply(replies[1]); err != nil {
if err := syncer.processChanRangeReply(ctx, replies[1]); err != nil {
t.Fatalf("unable to process reply: %v", err)
}
if err := syncer.processChanRangeReply(replies[2]); err != nil {
if err := syncer.processChanRangeReply(ctx, replies[2]); err != nil {
t.Fatalf("unable to process reply: %v", err)
}
@@ -1427,7 +1440,7 @@ func testGossipSyncerProcessChanRangeReply(t *testing.T, legacy bool) {
// If we send the final message, then we should transition to
// queryNewChannels as we've sent a non-empty set of new channels.
if err := syncer.processChanRangeReply(replies[3]); err != nil {
if err := syncer.processChanRangeReply(ctx, replies[3]); err != nil {
t.Fatalf("unable to process reply: %v", err)
}
@@ -1482,7 +1495,7 @@ func TestGossipSyncerSynchronizeChanIDs(t *testing.T) {
for i := 0; i < chunkSize*2; i += 2 {
// With our set up complete, we'll request a sync of chan ID's.
done := syncer.synchronizeChanIDs()
done := syncer.synchronizeChanIDs(context.Background())
// At this point, we shouldn't yet be done as only 2 items
// should have been queried for.
@@ -1529,7 +1542,7 @@ func TestGossipSyncerSynchronizeChanIDs(t *testing.T) {
}
// If we issue another query, the syncer should tell us that it's done.
done := syncer.synchronizeChanIDs()
done := syncer.synchronizeChanIDs(context.Background())
if done {
t.Fatalf("syncer should be finished!")
}
@@ -31,6 +31,14 @@
# Improvements
## Functional Updates
* Graph Store SQL implementation and migration project:
* Introduce an [abstract graph
store](https://github.com/lightningnetwork/lnd/pull/9791) interface.
* Start [validating](https://github.com/lightningnetwork/lnd/pull/9787) that
byte blobs at the end of gossip messages are valid TLV streams.
* Various [preparations](https://github.com/lightningnetwork/lnd/pull/9692)
of the graph code before the SQL implementation is added.
## RPC Updates
## lncli Updates
+4
View File
@@ -199,6 +199,10 @@ require (
sigs.k8s.io/yaml v1.2.0 // indirect
)
// TODO(elle): remove once all the schemas and queries for the graph
// store have been included in a tagged sqldb version.
replace github.com/lightningnetwork/lnd/sqldb => ./sqldb
// This replace is for https://github.com/advisories/GHSA-25xm-hr59-7c27
replace github.com/ulikunitz/xz => github.com/ulikunitz/xz v0.5.11
-2
View File
@@ -375,8 +375,6 @@ github.com/lightningnetwork/lnd/kvdb v1.4.16 h1:9BZgWdDfjmHRHLS97cz39bVuBAqMc4/p
github.com/lightningnetwork/lnd/kvdb v1.4.16/go.mod h1:HW+bvwkxNaopkz3oIgBV6NEnV4jCEZCACFUcNg4xSjM=
github.com/lightningnetwork/lnd/queue v1.1.1 h1:99ovBlpM9B0FRCGYJo6RSFDlt8/vOkQQZznVb18iNMI=
github.com/lightningnetwork/lnd/queue v1.1.1/go.mod h1:7A6nC1Qrm32FHuhx/mi1cieAiBZo5O6l8IBIoQxvkz4=
github.com/lightningnetwork/lnd/sqldb v1.0.9 h1:7OHi+Hui823mB/U9NzCdlZTAGSVdDCbjp33+6d/Q+G0=
github.com/lightningnetwork/lnd/sqldb v1.0.9/go.mod h1:OG09zL/PHPaBJefp4HsPz2YLUJ+zIQHbpgCtLnOx8I4=
github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6ijJlbdiZFbSM=
github.com/lightningnetwork/lnd/ticker v1.1.1/go.mod h1:waPTRAAcwtu7Ji3+3k+u/xH5GHovTsCoSVpho0KDvdA=
github.com/lightningnetwork/lnd/tlv v1.3.1 h1:o7CZg06y+rJZfUMAo0WzBLr0pgBWCzrt0f9gpujYUzk=
+1 -3
View File
@@ -13,7 +13,6 @@ import (
"github.com/lightningnetwork/lnd/chainntnfs"
graphdb "github.com/lightningnetwork/lnd/graph/db"
"github.com/lightningnetwork/lnd/graph/db/models"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lnutils"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"
@@ -1276,8 +1275,7 @@ func (b *Builder) ForAllOutgoingChannels(cb func(*models.ChannelEdgeInfo,
*models.ChannelEdgePolicy) error) error {
return b.cfg.Graph.ForEachNodeChannel(b.cfg.SelfNode,
func(_ kvdb.RTx, c *models.ChannelEdgeInfo,
e *models.ChannelEdgePolicy,
func(c *models.ChannelEdgeInfo, e *models.ChannelEdgePolicy,
_ *models.ChannelEdgePolicy) error {
if e == nil {
+11 -19
View File
@@ -1352,10 +1352,7 @@ func parseTestGraph(t *testing.T, useCache bool, path string) (
testAddrs = append(testAddrs, testAddr)
// Next, create a temporary graph database for usage within the test.
graph, graphBackend, err := makeTestGraph(t, useCache)
if err != nil {
return nil, err
}
graph := graphdb.MakeTestGraph(t, graphdb.WithUseGraphCache(useCache))
aliasMap := make(map[string]route.Vertex)
privKeyMap := make(map[string]*btcec.PrivateKey)
@@ -1562,12 +1559,11 @@ func parseTestGraph(t *testing.T, useCache bool, path string) (
}
return &testGraphInstance{
graph: graph,
graphBackend: graphBackend,
aliasMap: aliasMap,
privKeyMap: privKeyMap,
channelIDs: channelIDs,
links: links,
graph: graph,
aliasMap: aliasMap,
privKeyMap: privKeyMap,
channelIDs: channelIDs,
links: links,
}, nil
}
@@ -1730,10 +1726,7 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
testAddrs = append(testAddrs, testAddr)
// Next, create a temporary graph database for usage within the test.
graph, graphBackend, err := makeTestGraph(t, useCache)
if err != nil {
return nil, err
}
graph := graphdb.MakeTestGraph(t, graphdb.WithUseGraphCache(useCache))
aliasMap := make(map[string]route.Vertex)
privKeyMap := make(map[string]*btcec.PrivateKey)
@@ -1947,11 +1940,10 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
}
return &testGraphInstance{
graph: graph,
graphBackend: graphBackend,
aliasMap: aliasMap,
privKeyMap: privKeyMap,
links: links,
graph: graph,
aliasMap: aliasMap,
privKeyMap: privKeyMap,
links: links,
}, nil
}
+29 -21
View File
@@ -7,6 +7,7 @@ import (
"testing"
"github.com/lightningnetwork/lnd/tor"
"github.com/stretchr/testify/require"
)
type unknownAddrType struct{}
@@ -14,37 +15,47 @@ type unknownAddrType struct{}
func (t unknownAddrType) Network() string { return "unknown" }
func (t unknownAddrType) String() string { return "unknown" }
var testIP4 = net.ParseIP("192.168.1.1")
var testIP4 = net.ParseIP("192.168.1.1").To4()
var testIP6 = net.ParseIP("2001:0db8:0000:0000:0000:ff00:0042:8329")
var (
testIPV4Addr = &net.TCPAddr{
IP: testIP4,
Port: 12345,
}
testIPV6Addr = &net.TCPAddr{
IP: testIP6,
Port: 65535,
}
testOnionV2Addr = &tor.OnionAddr{
OnionService: "3g2upl4pq6kufc4m.onion",
Port: 9735,
}
testOnionV3Addr = &tor.OnionAddr{
OnionService: "vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd.onion", //nolint:ll
Port: 80,
}
)
var addrTests = []struct {
expAddr net.Addr
serErr string
}{
// Valid addresses.
{
expAddr: &net.TCPAddr{
IP: testIP4,
Port: 12345,
},
expAddr: testIPV4Addr,
},
{
expAddr: &net.TCPAddr{
IP: testIP6,
Port: 65535,
},
expAddr: testIPV6Addr,
},
{
expAddr: &tor.OnionAddr{
OnionService: "3g2upl4pq6kufc4m.onion",
Port: 9735,
},
expAddr: testOnionV2Addr,
},
{
expAddr: &tor.OnionAddr{
OnionService: "vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd.onion",
Port: 80,
},
expAddr: testOnionV3Addr,
},
// Invalid addresses.
@@ -141,9 +152,6 @@ func TestAddrSerialization(t *testing.T) {
t.Fatalf("unable to deserialize address: %v", err)
}
if addr.String() != test.expAddr.String() {
t.Fatalf("expected address %v after serialization, "+
"got %v", addr, test.expAddr)
}
require.Equal(t, test.expAddr, addr)
}
}
+52 -41
View File
@@ -5,33 +5,22 @@ import (
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/batch"
"github.com/lightningnetwork/lnd/graph/db/models"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
)
// ErrChanGraphShuttingDown indicates that the ChannelGraph has shutdown or is
// busy shutting down.
var ErrChanGraphShuttingDown = fmt.Errorf("ChannelGraph shutting down")
// Config is a struct that holds all the necessary dependencies for a
// ChannelGraph.
type Config struct {
// KVDB is the kvdb.Backend that will be used for initializing the
// KVStore CRUD layer.
KVDB kvdb.Backend
// KVStoreOpts is a list of functional options that will be used when
// initializing the KVStore.
KVStoreOpts []KVStoreOptionModifier
}
// ChannelGraph is a layer above the graph's CRUD layer.
//
// NOTE: currently, this is purely a pass-through layer directly to the backing
@@ -43,7 +32,7 @@ type ChannelGraph struct {
graphCache *GraphCache
*KVStore
V1Store
*topologyManager
quit chan struct{}
@@ -51,21 +40,16 @@ type ChannelGraph struct {
}
// NewChannelGraph creates a new ChannelGraph instance with the given backend.
func NewChannelGraph(cfg *Config, options ...ChanGraphOption) (*ChannelGraph,
error) {
func NewChannelGraph(v1Store V1Store,
options ...ChanGraphOption) (*ChannelGraph, error) {
opts := defaultChanGraphOptions()
for _, o := range options {
o(opts)
}
store, err := NewKVStore(cfg.KVDB, cfg.KVStoreOpts...)
if err != nil {
return nil, err
}
g := &ChannelGraph{
KVStore: store,
V1Store: v1Store,
topologyManager: newTopologyManager(),
quit: make(chan struct{}),
}
@@ -179,7 +163,7 @@ func (c *ChannelGraph) populateCache() error {
log.Info("Populating in-memory channel graph, this might take a " +
"while...")
err := c.KVStore.ForEachNodeCacheable(func(node route.Vertex,
err := c.V1Store.ForEachNodeCacheable(func(node route.Vertex,
features *lnwire.FeatureVector) error {
c.graphCache.AddNodeFeatures(node, features)
@@ -190,7 +174,7 @@ func (c *ChannelGraph) populateCache() error {
return err
}
err = c.KVStore.ForEachChannel(func(info *models.ChannelEdgeInfo,
err = c.V1Store.ForEachChannel(func(info *models.ChannelEdgeInfo,
policy1, policy2 *models.ChannelEdgePolicy) error {
c.graphCache.AddChannel(info, policy1, policy2)
@@ -224,7 +208,7 @@ func (c *ChannelGraph) ForEachNodeDirectedChannel(node route.Vertex,
return c.graphCache.ForEachChannel(node, cb)
}
return c.KVStore.ForEachNodeDirectedChannel(node, cb)
return c.V1Store.ForEachNodeDirectedChannel(node, cb)
}
// FetchNodeFeatures returns the features of the given node. If no features are
@@ -240,7 +224,7 @@ func (c *ChannelGraph) FetchNodeFeatures(node route.Vertex) (
return c.graphCache.GetFeatures(node), nil
}
return c.KVStore.FetchNodeFeatures(node)
return c.V1Store.FetchNodeFeatures(node)
}
// GraphSession will provide the call-back with access to a NodeTraverser
@@ -252,7 +236,7 @@ func (c *ChannelGraph) GraphSession(cb func(graph NodeTraverser) error) error {
return cb(c)
}
return c.KVStore.GraphSession(cb)
return c.V1Store.GraphSession(cb)
}
// ForEachNodeCached iterates through all the stored vertices/nodes in the
@@ -266,7 +250,7 @@ func (c *ChannelGraph) ForEachNodeCached(cb func(node route.Vertex,
return c.graphCache.ForEachNode(cb)
}
return c.KVStore.ForEachNodeCached(cb)
return c.V1Store.ForEachNodeCached(cb)
}
// AddLightningNode adds a vertex/node to the graph database. If the node is not
@@ -278,7 +262,7 @@ func (c *ChannelGraph) ForEachNodeCached(cb func(node route.Vertex,
func (c *ChannelGraph) AddLightningNode(node *models.LightningNode,
op ...batch.SchedulerOption) error {
err := c.KVStore.AddLightningNode(node, op...)
err := c.V1Store.AddLightningNode(node, op...)
if err != nil {
return err
}
@@ -301,7 +285,7 @@ func (c *ChannelGraph) AddLightningNode(node *models.LightningNode,
// DeleteLightningNode starts a new database transaction to remove a vertex/node
// from the database according to the node's public key.
func (c *ChannelGraph) DeleteLightningNode(nodePub route.Vertex) error {
err := c.KVStore.DeleteLightningNode(nodePub)
err := c.V1Store.DeleteLightningNode(nodePub)
if err != nil {
return err
}
@@ -322,7 +306,7 @@ func (c *ChannelGraph) DeleteLightningNode(nodePub route.Vertex) error {
func (c *ChannelGraph) AddChannelEdge(edge *models.ChannelEdgeInfo,
op ...batch.SchedulerOption) error {
err := c.KVStore.AddChannelEdge(edge, op...)
err := c.V1Store.AddChannelEdge(edge, op...)
if err != nil {
return err
}
@@ -344,7 +328,7 @@ func (c *ChannelGraph) AddChannelEdge(edge *models.ChannelEdgeInfo,
// If the cache is enabled, the edge will be added back to the graph cache if
// we still have a record of this channel in the DB.
func (c *ChannelGraph) MarkEdgeLive(chanID uint64) error {
err := c.KVStore.MarkEdgeLive(chanID)
err := c.V1Store.MarkEdgeLive(chanID)
if err != nil {
return err
}
@@ -352,7 +336,7 @@ func (c *ChannelGraph) MarkEdgeLive(chanID uint64) error {
if c.graphCache != nil {
// We need to add the channel back into our graph cache,
// otherwise we won't use it for path finding.
infos, err := c.KVStore.FetchChanInfos([]uint64{chanID})
infos, err := c.V1Store.FetchChanInfos([]uint64{chanID})
if err != nil {
return err
}
@@ -380,7 +364,7 @@ func (c *ChannelGraph) MarkEdgeLive(chanID uint64) error {
func (c *ChannelGraph) DeleteChannelEdges(strictZombiePruning, markZombie bool,
chanIDs ...uint64) error {
infos, err := c.KVStore.DeleteChannelEdges(
infos, err := c.V1Store.DeleteChannelEdges(
strictZombiePruning, markZombie, chanIDs...,
)
if err != nil {
@@ -409,7 +393,7 @@ func (c *ChannelGraph) DeleteChannelEdges(strictZombiePruning, markZombie bool,
func (c *ChannelGraph) DisconnectBlockAtHeight(height uint32) (
[]*models.ChannelEdgeInfo, error) {
edges, err := c.KVStore.DisconnectBlockAtHeight(height)
edges, err := c.V1Store.DisconnectBlockAtHeight(height)
if err != nil {
return nil, err
}
@@ -437,7 +421,7 @@ func (c *ChannelGraph) PruneGraph(spentOutputs []*wire.OutPoint,
blockHash *chainhash.Hash, blockHeight uint32) (
[]*models.ChannelEdgeInfo, error) {
edges, nodes, err := c.KVStore.PruneGraph(
edges, nodes, err := c.V1Store.PruneGraph(
spentOutputs, blockHash, blockHeight,
)
if err != nil {
@@ -482,7 +466,7 @@ func (c *ChannelGraph) PruneGraph(spentOutputs []*wire.OutPoint,
// that we only maintain a graph of reachable nodes. In the event that a pruned
// node gains more channels, it will be re-added back to the graph.
func (c *ChannelGraph) PruneGraphNodes() error {
nodes, err := c.KVStore.PruneGraphNodes()
nodes, err := c.V1Store.PruneGraphNodes()
if err != nil {
return err
}
@@ -504,7 +488,7 @@ func (c *ChannelGraph) PruneGraphNodes() error {
func (c *ChannelGraph) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo,
isZombieChan func(time.Time, time.Time) bool) ([]uint64, error) {
unknown, knownZombies, err := c.KVStore.FilterKnownChanIDs(chansInfo)
unknown, knownZombies, err := c.V1Store.FilterKnownChanIDs(chansInfo)
if err != nil {
return nil, err
}
@@ -533,7 +517,7 @@ func (c *ChannelGraph) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo,
// timestamps could bring it back from the dead, then we mark it
// alive, and we let it be added to the set of IDs to query our
// peer for.
err := c.KVStore.MarkEdgeLive(
err := c.V1Store.MarkEdgeLive(
info.ShortChannelID.ToUint64(),
)
// Since there is a chance that the edge could have been marked
@@ -554,7 +538,7 @@ func (c *ChannelGraph) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo,
func (c *ChannelGraph) MarkEdgeZombie(chanID uint64,
pubKey1, pubKey2 [33]byte) error {
err := c.KVStore.MarkEdgeZombie(chanID, pubKey1, pubKey2)
err := c.V1Store.MarkEdgeZombie(chanID, pubKey1, pubKey2)
if err != nil {
return err
}
@@ -576,7 +560,7 @@ func (c *ChannelGraph) MarkEdgeZombie(chanID uint64,
func (c *ChannelGraph) UpdateEdgePolicy(edge *models.ChannelEdgePolicy,
op ...batch.SchedulerOption) error {
from, to, err := c.KVStore.UpdateEdgePolicy(edge, op...)
from, to, err := c.V1Store.UpdateEdgePolicy(edge, op...)
if err != nil {
return err
}
@@ -598,3 +582,30 @@ func (c *ChannelGraph) UpdateEdgePolicy(edge *models.ChannelEdgePolicy,
return nil
}
// MakeTestGraphNew creates a new instance of the ChannelGraph for testing
// purposes. The backing V1Store implementation depends on the version of
// NewTestDB included in the current build.
//
// NOTE: this is currently unused, but is left here for future use to show how
// NewTestDB can be used. As the SQL implementation of the V1Store is
// implemented, unit tests will be switched to use this function instead of
// the existing MakeTestGraph helper. Once only this function is used, the
// existing MakeTestGraph function will be removed and this one will be renamed.
func MakeTestGraphNew(t testing.TB,
opts ...ChanGraphOption) *ChannelGraph {
t.Helper()
store := NewTestDB(t)
graph, err := NewChannelGraph(store, opts...)
require.NoError(t, err)
require.NoError(t, graph.Start())
t.Cleanup(func() {
require.NoError(t, graph.Stop())
})
return graph
}
+584 -457
View File
File diff suppressed because it is too large. Load diff
+327
View File
@@ -1,6 +1,13 @@
package graphdb
import (
"net"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/batch"
"github.com/lightningnetwork/lnd/graph/db/models"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
@@ -37,3 +44,323 @@ type NodeTraverser interface {
// FetchNodeFeatures returns the features of the given node.
FetchNodeFeatures(nodePub route.Vertex) (*lnwire.FeatureVector, error)
}
// V1Store represents the main interface for the channel graph database for all
// channels and nodes gossiped via the V1 gossip protocol as defined in BOLT 7.
type V1Store interface { //nolint:interfacebloat
NodeTraverser
// AddLightningNode adds a vertex/node to the graph database. If the
// node is not in the database from before, this will add a new,
// unconnected one to the graph. If it is present from before, this will
// update that node's information. Note that this method is expected to
// only be called to update an already present node from a node
// announcement, or to insert a node found in a channel update.
AddLightningNode(node *models.LightningNode,
op ...batch.SchedulerOption) error
// AddrsForNode returns all known addresses for the target node public
// key that the graph DB is aware of. The returned boolean indicates if
// the given node is unknown to the graph DB or not.
AddrsForNode(nodePub *btcec.PublicKey) (bool, []net.Addr, error)
// ForEachSourceNodeChannel iterates through all channels of the source
// node, executing the passed callback on each. The call-back is
// provided with the channel's outpoint, whether we have a policy for
// the channel and the channel peer's node information.
ForEachSourceNodeChannel(cb func(chanPoint wire.OutPoint,
havePolicy bool, otherNode *models.LightningNode) error) error
// ForEachNodeChannel iterates through all channels of the given node,
// executing the passed callback with an edge info structure and the
// policies of each end of the channel. The first edge policy is the
// outgoing edge *to* the connecting node, while the second is the
// incoming edge *from* the connecting node. If the callback returns an
// error, then the iteration is halted with the error propagated back up
// to the caller.
//
// Unknown policies are passed into the callback as nil values.
ForEachNodeChannel(nodePub route.Vertex,
cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy,
*models.ChannelEdgePolicy) error) error
// ForEachNodeCached is similar to forEachNode, but it returns
// DirectedChannel data to the call-back.
//
// NOTE: The callback contents MUST not be modified.
ForEachNodeCached(cb func(node route.Vertex,
chans map[uint64]*DirectedChannel) error) error
// ForEachNode iterates through all the stored vertices/nodes in the
// graph, executing the passed callback with each node encountered. If
// the callback returns an error, then the transaction is aborted and
// the iteration stops early. Any operations performed on the NodeTx
// passed to the call-back are executed under the same read transaction
// and so, methods on the NodeTx object _MUST_ only be called from
// within the call-back.
ForEachNode(cb func(tx NodeRTx) error) error
// ForEachNodeCacheable iterates through all the stored vertices/nodes
// in the graph, executing the passed callback with each node
// encountered. If the callback returns an error, then the transaction
// is aborted and the iteration stops early.
ForEachNodeCacheable(cb func(route.Vertex,
*lnwire.FeatureVector) error) error
// LookupAlias attempts to return the alias as advertised by the target
// node.
LookupAlias(pub *btcec.PublicKey) (string, error)
// DeleteLightningNode starts a new database transaction to remove a
// vertex/node from the database according to the node's public key.
DeleteLightningNode(nodePub route.Vertex) error
// NodeUpdatesInHorizon returns all the known lightning node which have
// an update timestamp within the passed range. This method can be used
// by two nodes to quickly determine if they have the same set of up to
// date node announcements.
NodeUpdatesInHorizon(startTime,
endTime time.Time) ([]models.LightningNode, error)
// 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 returned.
FetchLightningNode(nodePub route.Vertex) (
*models.LightningNode, error)
// HasLightningNode determines if the graph has a vertex identified by
// the target node identity public key. If the node exists in the
// database, a timestamp of when the data for the node was lasted
// updated is returned along with a true boolean. Otherwise, an empty
// time.Time is returned with a false boolean.
HasLightningNode(nodePub [33]byte) (time.Time, bool,
error)
// IsPublicNode is a helper method that determines whether the node with
// the given public key is seen as a public node in the graph from the
// graph's source node's point of view.
IsPublicNode(pubKey [33]byte) (bool, error)
// GraphSession will provide the call-back with access to a
// NodeTraverser instance which can be used to perform queries against
// the channel graph.
GraphSession(cb func(graph NodeTraverser) error) error
// ForEachChannel iterates through all the channel edges stored within
// the graph and invokes the passed callback for each edge. The callback
// takes two edges as since this is a directed graph, both the in/out
// edges are visited. If the callback returns an error, then the
// transaction is aborted and the iteration stops early.
//
// NOTE: If an edge can't be found, or wasn't advertised, then a nil
// pointer for that particular channel edge routing policy will be
// passed into the callback.
ForEachChannel(cb func(*models.ChannelEdgeInfo,
*models.ChannelEdgePolicy,
*models.ChannelEdgePolicy) error) error
// DisabledChannelIDs returns the channel ids of disabled channels.
// A channel is disabled when two of the associated ChanelEdgePolicies
// have their disabled bit on.
DisabledChannelIDs() ([]uint64, error)
// AddChannelEdge adds a new (undirected, blank) edge to the graph
// database. An undirected edge from the two target nodes are created.
// The information stored denotes the static attributes of the channel,
// such as the channelID, the keys involved in creation of the channel,
// and the set of features that the channel supports. The chanPoint and
// chanID are used to uniquely identify the edge globally within the
// database.
AddChannelEdge(edge *models.ChannelEdgeInfo,
op ...batch.SchedulerOption) error
// HasChannelEdge returns true if the database knows of a channel edge
// with the passed channel ID, and false otherwise. If an edge with that
// ID is found within the graph, then two time stamps representing the
// last time the edge was updated for both directed edges are returned
// along with the boolean. If it is not found, then the zombie index is
// checked and its result is returned as the second boolean.
HasChannelEdge(chanID uint64) (time.Time, time.Time, bool, bool,
error)
// DeleteChannelEdges removes edges with the given channel IDs from the
// database and marks them as zombies. This ensures that we're unable to
// re-add it to our database once again. If an edge does not exist
// within the database, then ErrEdgeNotFound will be returned. If
// strictZombiePruning is true, then when we mark these edges as
// zombies, we'll set up the keys such that we require the node that
// failed to send the fresh update to be the one that resurrects the
// channel from its zombie state. The markZombie bool denotes whether
// to mark the channel as a zombie.
DeleteChannelEdges(strictZombiePruning, markZombie bool,
chanIDs ...uint64) ([]*models.ChannelEdgeInfo, error)
// AddEdgeProof sets the proof of an existing edge in the graph
// database.
AddEdgeProof(chanID lnwire.ShortChannelID,
proof *models.ChannelAuthProof) error
// ChannelID attempt to lookup the 8-byte compact channel ID which maps
// to the passed channel point (outpoint). If the passed channel doesn't
// exist within the database, then ErrEdgeNotFound is returned.
ChannelID(chanPoint *wire.OutPoint) (uint64, error)
// HighestChanID returns the "highest" known channel ID in the channel
// graph. This represents the "newest" channel from the PoV of the
// chain. This method can be used by peers to quickly determine if
// they're graphs are in sync.
HighestChanID() (uint64, error)
// ChanUpdatesInHorizon returns all the known channel edges which have
// at least one edge that has an update timestamp within the specified
// horizon.
ChanUpdatesInHorizon(startTime, endTime time.Time) ([]ChannelEdge,
error)
// FilterKnownChanIDs takes a set of channel IDs and return the subset
// of chan ID's that we don't know and are not known zombies of the
// passed set. In other words, we perform a set difference of our set
// of chan ID's and the ones passed in. This method can be used by
// callers to determine the set of channels another peer knows of that
// we don't. The ChannelUpdateInfos for the known zombies is also
// returned.
FilterKnownChanIDs(chansInfo []ChannelUpdateInfo) ([]uint64,
[]ChannelUpdateInfo, error)
// FilterChannelRange returns the channel ID's of all known channels
// which were mined in a block height within the passed range. The
// channel IDs are grouped by their common block height. This method can
// be used to quickly share with a peer the set of channels we know of
// within a particular range to catch them up after a period of time
// offline. If withTimestamps is true then the timestamp info of the
// latest received channel update messages of the channel will be
// included in the response.
FilterChannelRange(startHeight, endHeight uint32, withTimestamps bool) (
[]BlockChannelRange, error)
// FetchChanInfos returns the set of channel edges that correspond to
// the passed channel ID's. If an edge is the query is unknown to the
// database, it will skipped and the result will contain only those
// edges that exist at the time of the query. This can be used to
// respond to peer queries that are seeking to fill in gaps in their
// view of the channel graph.
FetchChanInfos(chanIDs []uint64) ([]ChannelEdge, error)
// FetchChannelEdgesByOutpoint attempts to lookup the two directed edges
// for the channel identified by the funding outpoint. If the channel
// can't be found, then ErrEdgeNotFound is returned. A struct which
// houses the general information for the channel itself is returned as
// well as two structs that contain the routing policies for the channel
// in either direction.
FetchChannelEdgesByOutpoint(op *wire.OutPoint) (
*models.ChannelEdgeInfo, *models.ChannelEdgePolicy,
*models.ChannelEdgePolicy, error)
// FetchChannelEdgesByID attempts to lookup the two directed edges for
// the channel identified by the channel ID. If the channel can't be
// found, then ErrEdgeNotFound is returned. A struct which houses the
// general information for the channel itself is returned as well as
// two structs that contain the routing policies for the channel in
// either direction.
//
// ErrZombieEdge can be returned if the edge is currently marked as a
// zombie within the database. In this case, the ChannelEdgePolicy's
// will be nil, and the ChannelEdgeInfo will only include the public
// keys of each node.
FetchChannelEdgesByID(chanID uint64) (
*models.ChannelEdgeInfo, *models.ChannelEdgePolicy,
*models.ChannelEdgePolicy, error)
// ChannelView returns the verifiable edge information for each active
// channel within the known channel graph. The set of UTXO's (along with
// their scripts) returned are the ones that need to be watched on chain
// to detect channel closes on the resident blockchain.
ChannelView() ([]EdgePoint, error)
// MarkEdgeZombie attempts to mark a channel identified by its channel
// ID as a zombie. This method is used on an ad-hoc basis, when channels
// need to be marked as zombies outside the normal pruning cycle.
MarkEdgeZombie(chanID uint64,
pubKey1, pubKey2 [33]byte) error
// MarkEdgeLive clears an edge from our zombie index, deeming it as
// live.
MarkEdgeLive(chanID uint64) error
// IsZombieEdge returns whether the edge is considered zombie. If it is
// a zombie, then the two node public keys corresponding to this edge
// are also returned.
IsZombieEdge(chanID uint64) (bool, [33]byte, [33]byte)
// NumZombies returns the current number of zombie channels in the
// graph.
NumZombies() (uint64, error)
// PutClosedScid stores a SCID for a closed channel in the database.
// This is so that we can ignore channel announcements that we know to
// be closed without having to validate them and fetch a block.
PutClosedScid(scid lnwire.ShortChannelID) error
// IsClosedScid checks whether a channel identified by the passed in
// scid is closed. This helps avoid having to perform expensive
// validation checks.
IsClosedScid(scid lnwire.ShortChannelID) (bool, error)
// UpdateEdgePolicy updates the edge routing policy for a single
// directed edge within the database for the referenced channel. The
// `flags` attribute within the ChannelEdgePolicy determines which of
// the directed edges are being updated. If the flag is 1, then the
// first node's information is being updated, otherwise it's the second
// node's information. The node ordering is determined by the
// lexicographical ordering of the identity public keys of the nodes on
// either side of the channel.
UpdateEdgePolicy(edge *models.ChannelEdgePolicy,
op ...batch.SchedulerOption) (route.Vertex, route.Vertex, error)
// SourceNode returns the source node of the graph. The source node is
// treated as the center node within a star-graph. This method may be
// used to kick off a path finding algorithm in order to explore the
// reachability of another node based off the source node.
SourceNode() (*models.LightningNode, error)
// SetSourceNode sets the source node within the graph database. The
// source node is to be used as the center of a star-graph within path
// finding algorithms.
SetSourceNode(node *models.LightningNode) error
// PruneTip returns the block height and hash of the latest block that
// has been used to prune channels in the graph. Knowing the "prune tip"
// allows callers to tell if the graph is currently in sync with the
// current best known UTXO state.
PruneTip() (*chainhash.Hash, uint32, error)
// PruneGraphNodes is a garbage collection method which attempts to
// prune out any nodes from the channel graph that are currently
// unconnected. This ensures that we only maintain a graph of reachable
// nodes. In the event that a pruned node gains more channels, it will
// be re-added back to the graph.
PruneGraphNodes() ([]route.Vertex, error)
// PruneGraph prunes newly closed channels from the channel graph in
// response to a new block being solved on the network. Any transactions
// which spend the funding output of any known channels within he graph
// will be deleted. Additionally, the "prune tip", or the last block
// which has been used to prune the graph is stored so callers can
// ensure the graph is fully in sync with the current UTXO state. A
// slice of channels that have been closed by the target block along
// with any pruned nodes are returned if the function succeeds without
// error.
PruneGraph(spentOutputs []*wire.OutPoint,
blockHash *chainhash.Hash, blockHeight uint32) (
[]*models.ChannelEdgeInfo, []route.Vertex, error)
// DisconnectBlockAtHeight is used to indicate that the block specified
// by the passed height has been disconnected from the main chain. This
// will "rewind" the graph back to the height below, deleting channels
// that are no longer confirmed from the graph. The prune log will be
// set to the last prune height valid for the remaining chain.
// Channels that were removed from the graph resulting from the
// disconnected block are returned.
DisconnectBlockAtHeight(height uint32) ([]*models.ChannelEdgeInfo,
error)
}
+102 -92
View File
@@ -2,6 +2,7 @@ package graphdb
import (
"bytes"
"context"
"crypto/sha256"
"encoding/binary"
"errors"
@@ -192,10 +193,14 @@ type KVStore struct {
rejectCache *rejectCache
chanCache *channelCache
chanScheduler batch.Scheduler
nodeScheduler batch.Scheduler
chanScheduler batch.Scheduler[kvdb.RwTx]
nodeScheduler batch.Scheduler[kvdb.RwTx]
}
// A compile-time assertion to ensure that the KVStore struct implements the
// V1Store interface.
var _ V1Store = (*KVStore)(nil)
// NewKVStore allocates a new KVStore backed by a DB instance. The
// returned instance has its own unique reject cache and channel cache.
func NewKVStore(db kvdb.Backend, options ...KVStoreOptionModifier) (*KVStore,
@@ -218,10 +223,12 @@ func NewKVStore(db kvdb.Backend, options ...KVStoreOptionModifier) (*KVStore,
chanCache: newChannelCache(opts.ChannelCacheSize),
}
g.chanScheduler = batch.NewTimeScheduler(
db, &g.cacheMu, opts.BatchCommitInterval,
batch.NewBoltBackend[kvdb.RwTx](db), &g.cacheMu,
opts.BatchCommitInterval,
)
g.nodeScheduler = batch.NewTimeScheduler(
db, nil, opts.BatchCommitInterval,
batch.NewBoltBackend[kvdb.RwTx](db), nil,
opts.BatchCommitInterval,
)
return g, nil
@@ -299,29 +306,6 @@ var graphTopLevelBuckets = [][]byte{
closedScidBucket,
}
// Wipe completely deletes all saved state within all used buckets within the
// database. The deletion is done in a single transaction, therefore this
// operation is fully atomic.
func (c *KVStore) Wipe() error {
err := kvdb.Update(c.db, func(tx kvdb.RwTx) error {
for _, tlb := range graphTopLevelBuckets {
err := tx.DeleteTopLevelBucket(tlb)
if err != nil &&
!errors.Is(err, kvdb.ErrBucketNotFound) {
return err
}
}
return nil
}, func() {})
if err != nil {
return err
}
return initKVStore(c.db)
}
// createChannelDB creates and initializes a fresh version of In
// the case that the target path has not yet been created or doesn't yet exist,
// then the path is created. Additionally, all required top-level buckets used
@@ -584,7 +568,7 @@ func (c *KVStore) ForEachNodeCached(cb func(node route.Vertex,
channels := make(map[uint64]*DirectedChannel)
err := c.ForEachNodeChannelTx(tx, node.PubKeyBytes,
err := c.forEachNodeChannelTx(tx, node.PubKeyBytes,
func(tx kvdb.RTx, e *models.ChannelEdgeInfo,
p1 *models.ChannelEdgePolicy,
p2 *models.ChannelEdgePolicy) error {
@@ -871,19 +855,18 @@ func (c *KVStore) SetSourceNode(node *models.LightningNode) error {
//
// TODO(roasbeef): also need sig of announcement.
func (c *KVStore) AddLightningNode(node *models.LightningNode,
op ...batch.SchedulerOption) error {
opts ...batch.SchedulerOption) error {
r := &batch.Request{
Update: func(tx kvdb.RwTx) error {
ctx := context.TODO()
r := &batch.Request[kvdb.RwTx]{
Opts: batch.NewSchedulerOptions(opts...),
Do: func(tx kvdb.RwTx) error {
return addLightningNode(tx, node)
},
}
for _, f := range op {
f(r)
}
return c.nodeScheduler.Execute(r)
return c.nodeScheduler.Execute(ctx, r)
}
func addLightningNode(tx kvdb.RwTx, node *models.LightningNode) error {
@@ -1009,14 +992,17 @@ func (c *KVStore) deleteLightningNode(nodes kvdb.RwBucket,
// supports. The chanPoint and chanID are used to uniquely identify the edge
// globally within the database.
func (c *KVStore) AddChannelEdge(edge *models.ChannelEdgeInfo,
op ...batch.SchedulerOption) error {
opts ...batch.SchedulerOption) error {
ctx := context.TODO()
var alreadyExists bool
r := &batch.Request{
r := &batch.Request[kvdb.RwTx]{
Opts: batch.NewSchedulerOptions(opts...),
Reset: func() {
alreadyExists = false
},
Update: func(tx kvdb.RwTx) error {
Do: func(tx kvdb.RwTx) error {
err := c.addChannelEdge(tx, edge)
// Silence ErrEdgeAlreadyExist so that the batch can
@@ -1042,15 +1028,7 @@ func (c *KVStore) AddChannelEdge(edge *models.ChannelEdgeInfo,
},
}
for _, f := range op {
if f == nil {
return fmt.Errorf("nil scheduler option was used")
}
f(r)
}
return c.chanScheduler.Execute(r)
return c.chanScheduler.Execute(ctx, r)
}
// addChannelEdge is the private form of AddChannelEdge that allows callers to
@@ -2719,20 +2697,22 @@ func makeZombiePubkeys(info *models.ChannelEdgeInfo,
// determined by the lexicographical ordering of the identity public keys of the
// nodes on either side of the channel.
func (c *KVStore) UpdateEdgePolicy(edge *models.ChannelEdgePolicy,
op ...batch.SchedulerOption) (route.Vertex, route.Vertex, error) {
opts ...batch.SchedulerOption) (route.Vertex, route.Vertex, error) {
var (
ctx = context.TODO()
isUpdate1 bool
edgeNotFound bool
from, to route.Vertex
)
r := &batch.Request{
r := &batch.Request[kvdb.RwTx]{
Opts: batch.NewSchedulerOptions(opts...),
Reset: func() {
isUpdate1 = false
edgeNotFound = false
},
Update: func(tx kvdb.RwTx) error {
Do: func(tx kvdb.RwTx) error {
var err error
from, to, isUpdate1, err = updateEdgePolicy(tx, edge)
if err != nil {
@@ -2761,11 +2741,7 @@ func (c *KVStore) UpdateEdgePolicy(edge *models.ChannelEdgePolicy,
},
}
for _, f := range op {
f(r)
}
err := c.chanScheduler.Execute(r)
err := c.chanScheduler.Execute(ctx, r)
return from, to, err
}
@@ -2873,7 +2849,7 @@ func (c *KVStore) isPublic(tx kvdb.RTx, nodePub route.Vertex,
// used to terminate the check early.
nodeIsPublic := false
errDone := errors.New("done")
err := c.ForEachNodeChannelTx(tx, nodePub, func(tx kvdb.RTx,
err := c.forEachNodeChannelTx(tx, nodePub, func(tx kvdb.RTx,
info *models.ChannelEdgeInfo, _ *models.ChannelEdgePolicy,
_ *models.ChannelEdgePolicy) error {
@@ -3126,13 +3102,56 @@ func nodeTraversal(tx kvdb.RTx, nodePub []byte, db kvdb.Backend,
//
// Unknown policies are passed into the callback as nil values.
func (c *KVStore) ForEachNodeChannel(nodePub route.Vertex,
cb func(kvdb.RTx, *models.ChannelEdgeInfo, *models.ChannelEdgePolicy,
cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy,
*models.ChannelEdgePolicy) error) error {
return nodeTraversal(nil, nodePub[:], c.db, cb)
return nodeTraversal(nil, nodePub[:], c.db, func(_ kvdb.RTx,
info *models.ChannelEdgeInfo, policy,
policy2 *models.ChannelEdgePolicy) error {
return cb(info, policy, policy2)
})
}
// ForEachNodeChannelTx iterates through all channels of the given node,
// ForEachSourceNodeChannel iterates through all channels of the source node,
// executing the passed callback on each. The callback is provided with the
// channel's outpoint, whether we have a policy for the channel and the channel
// peer's node information.
func (c *KVStore) ForEachSourceNodeChannel(cb func(chanPoint wire.OutPoint,
havePolicy bool, otherNode *models.LightningNode) error) error {
return kvdb.View(c.db, func(tx kvdb.RTx) error {
nodes := tx.ReadBucket(nodeBucket)
if nodes == nil {
return ErrGraphNotFound
}
node, err := c.sourceNode(nodes)
if err != nil {
return err
}
return nodeTraversal(
tx, node.PubKeyBytes[:], c.db, func(tx kvdb.RTx,
info *models.ChannelEdgeInfo,
policy, _ *models.ChannelEdgePolicy) error {
peer, err := c.fetchOtherNode(
tx, info, node.PubKeyBytes[:],
)
if err != nil {
return err
}
return cb(
info.ChannelPoint, policy != nil, peer,
)
},
)
}, func() {})
}
// forEachNodeChannelTx iterates through all channels of the given node,
// executing the passed callback with an edge info structure and the policies
// of each end of the channel. The first edge policy is the outgoing edge *to*
// the connecting node, while the second is the incoming edge *from* the
@@ -3145,7 +3164,7 @@ func (c *KVStore) ForEachNodeChannel(nodePub route.Vertex,
// 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 *KVStore) ForEachNodeChannelTx(tx kvdb.RTx,
func (c *KVStore) forEachNodeChannelTx(tx kvdb.RTx,
nodePub route.Vertex, cb func(kvdb.RTx, *models.ChannelEdgeInfo,
*models.ChannelEdgePolicy,
*models.ChannelEdgePolicy) error) error {
@@ -3153,11 +3172,11 @@ func (c *KVStore) ForEachNodeChannelTx(tx kvdb.RTx,
return nodeTraversal(tx, nodePub[:], c.db, cb)
}
// FetchOtherNode attempts to fetch the full LightningNode that's opposite of
// fetchOtherNode attempts to fetch the full LightningNode that's opposite of
// the target node in the channel. This is useful when one knows the pubkey of
// one of the nodes, and wishes to obtain the full LightningNode for the other
// end of the channel.
func (c *KVStore) FetchOtherNode(tx kvdb.RTx,
func (c *KVStore) fetchOtherNode(tx kvdb.RTx,
channel *models.ChannelEdgeInfo, thisNodeKey []byte) (
*models.LightningNode, error) {
@@ -4687,7 +4706,7 @@ func (c *chanGraphNodeTx) FetchNode(nodePub route.Vertex) (NodeRTx, error) {
func (c *chanGraphNodeTx) ForEachChannel(f func(*models.ChannelEdgeInfo,
*models.ChannelEdgePolicy, *models.ChannelEdgePolicy) error) error {
return c.db.ForEachNodeChannelTx(c.tx, c.node.PubKeyBytes,
return c.db.forEachNodeChannelTx(c.tx, c.node.PubKeyBytes,
func(_ kvdb.RTx, info *models.ChannelEdgeInfo, policy1,
policy2 *models.ChannelEdgePolicy) error {
@@ -4696,40 +4715,31 @@ func (c *chanGraphNodeTx) ForEachChannel(f func(*models.ChannelEdgeInfo,
)
}
// MakeTestGraph creates a new instance of the KVStore for testing
// MakeTestGraph creates a new instance of the ChannelGraph for testing
// purposes.
func MakeTestGraph(t testing.TB, modifiers ...KVStoreOptionModifier) (
*ChannelGraph, error) {
opts := DefaultOptions()
for _, modifier := range modifiers {
modifier(opts)
}
//
// NOTE: this helper currently creates a ChannelGraph that is only ever backed
// by the `KVStore` of the `V1Store` interface.
func MakeTestGraph(t testing.TB, opts ...ChanGraphOption) *ChannelGraph {
t.Helper()
// Next, create KVStore for the first time.
backend, backendCleanup, err := kvdb.GetTestBackend(t.TempDir(), "cgr")
if err != nil {
backendCleanup()
return nil, err
}
graph, err := NewChannelGraph(&Config{
KVDB: backend,
KVStoreOpts: modifiers,
})
if err != nil {
backendCleanup()
return nil, err
}
require.NoError(t, graph.Start())
t.Cleanup(backendCleanup)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, backend.Close())
})
graphStore, err := NewKVStore(backend)
require.NoError(t, err)
graph, err := NewChannelGraph(graphStore, opts...)
require.NoError(t, err)
require.NoError(t, graph.Start())
t.Cleanup(func() {
_ = backend.Close()
backendCleanup()
require.NoError(t, graph.Stop())
})
return graph, nil
return graph
}
+45
View File
@@ -0,0 +1,45 @@
package graphdb
import (
"github.com/lightningnetwork/lnd/sqldb"
)
// SQLQueries is a subset of the sqlc.Querier interface that can be used to
// execute queries against the SQL graph tables.
type SQLQueries interface {
}
// BatchedSQLQueries is a version of SQLQueries that's capable of batched
// database operations.
type BatchedSQLQueries interface {
SQLQueries
sqldb.BatchedTx[SQLQueries]
}
// SQLStore is an implementation of the V1Store interface that uses a SQL
// database as the backend.
//
// NOTE: currently, this temporarily embeds the KVStore struct so that we can
// implement the V1Store interface incrementally. For any method not
// implemented, things will fall back to the KVStore. This is ONLY the case
// for the time being while this struct is purely used in unit tests only.
type SQLStore struct {
db BatchedSQLQueries
// Temporary fall-back to the KVStore so that we can implement the
// interface incrementally.
*KVStore
}
// A compile-time assertion to ensure that SQLStore implements the V1Store
// interface.
var _ V1Store = (*SQLStore)(nil)
// NewSQLStore creates a new SQLStore instance given an open BatchedSQLQueries
// storage backend.
func NewSQLStore(db BatchedSQLQueries, kvStore *KVStore) *SQLStore {
return &SQLStore{
db: db,
KVStore: kvStore,
}
}
+23
View File
@@ -0,0 +1,23 @@
//go:build !test_db_sqlite && !test_db_postgres
package graphdb
import (
"testing"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/stretchr/testify/require"
)
// NewTestDB is a helper function that creates an BBolt database for testing.
func NewTestDB(t testing.TB) *KVStore {
backend, backendCleanup, err := kvdb.GetTestBackend(t.TempDir(), "cgr")
require.NoError(t, err)
t.Cleanup(backendCleanup)
graphStore, err := NewKVStore(backend)
require.NoError(t, err)
return graphStore
}
+42
View File
@@ -0,0 +1,42 @@
//go:build test_db_postgres && !test_db_sqlite
package graphdb
import (
"database/sql"
"testing"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/sqldb"
"github.com/stretchr/testify/require"
)
// NewTestDB is a helper function that creates a SQLStore backed by a postgres
// database for testing. At the moment, it embeds a KVStore but once the
// SQLStore fully implements the V1Store interface, the KVStore will be removed.
func NewTestDB(t testing.TB) *SQLStore {
backend, backendCleanup, err := kvdb.GetTestBackend(t.TempDir(), "cgr")
require.NoError(t, err)
t.Cleanup(backendCleanup)
graphStore, err := NewKVStore(backend)
require.NoError(t, err)
pgFixture := sqldb.NewTestPgFixture(
t, sqldb.DefaultPostgresFixtureLifetime,
)
t.Cleanup(func() {
pgFixture.TearDown(t)
})
db := sqldb.NewTestPostgresDB(t, pgFixture).BaseDB
executor := sqldb.NewTransactionExecutor(
db, func(tx *sql.Tx) SQLQueries {
return db.WithTx(tx)
},
)
return NewSQLStore(executor, graphStore)
}
+35
View File
@@ -0,0 +1,35 @@
//go:build !test_db_posgres && test_db_sqlite
package graphdb
import (
"database/sql"
"testing"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/sqldb"
"github.com/stretchr/testify/require"
)
// NewTestDB is a helper function that creates a SQLStore backed by a sqlite
// database for testing. At the moment, it embeds a KVStore but once the
// SQLStore fully implements the V1Store interface, the KVStore will be removed.
func NewTestDB(t testing.TB) *SQLStore {
backend, backendCleanup, err := kvdb.GetTestBackend(t.TempDir(), "cgr")
require.NoError(t, err)
t.Cleanup(backendCleanup)
graphStore, err := NewKVStore(backend)
require.NoError(t, err)
db := sqldb.NewTestSqliteDB(t).BaseDB
executor := sqldb.NewTransactionExecutor(
db, func(tx *sql.Tx) SQLQueries {
return db.WithTx(tx)
},
)
return NewSQLStore(executor, graphStore)
}
+3 -5
View File
@@ -8,7 +8,6 @@ import (
"github.com/lightningnetwork/lnd/batch"
graphdb "github.com/lightningnetwork/lnd/graph/db"
"github.com/lightningnetwork/lnd/graph/db/models"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
)
@@ -254,10 +253,9 @@ type DB interface {
// to the caller.
//
// Unknown policies are passed into the callback as nil values.
ForEachNodeChannel(nodePub route.Vertex, cb func(kvdb.RTx,
*models.ChannelEdgeInfo,
*models.ChannelEdgePolicy,
*models.ChannelEdgePolicy) error) error
ForEachNodeChannel(nodePub route.Vertex,
cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy,
*models.ChannelEdgePolicy) error) error
// AddEdgeProof sets the proof of an existing edge in the graph
// database.
+4 -37
View File
@@ -22,7 +22,6 @@ import (
"github.com/lightningnetwork/lnd/graph/db/models"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/kvdb"
lnmock "github.com/lightningnetwork/lnd/lntest/mock"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"
@@ -89,7 +88,7 @@ func createTestNode(t *testing.T) *models.LightningNode {
LastUpdate: time.Unix(updateTime, 0),
Addresses: testAddrs,
Color: color.RGBA{1, 2, 3, 0},
Alias: "kek" + string(pub[:]),
Alias: "kek" + hex.EncodeToString(pub),
AuthSigBytes: testSig.Serialize(),
Features: testFeatures,
}
@@ -1034,9 +1033,7 @@ type testCtx struct {
func createTestCtxSingleNode(t *testing.T,
startingHeight uint32) *testCtx {
graph, graphBackend, err := makeTestGraph(t, true)
require.NoError(t, err, "failed to make test graph")
graph := graphdb.MakeTestGraph(t)
sourceNode := createTestNode(t)
require.NoError(t,
@@ -1044,8 +1041,7 @@ func createTestCtxSingleNode(t *testing.T,
)
graphInstance := &testGraphInstance{
graph: graph,
graphBackend: graphBackend,
graph: graph,
}
return createTestCtxFromGraphInstance(
@@ -1084,37 +1080,8 @@ func (c *testCtx) RestartBuilder(t *testing.T) {
c.builder = builder
}
// makeTestGraph creates a new instance of a channeldb.ChannelGraph for testing
// purposes.
func makeTestGraph(t *testing.T, useCache bool) (*graphdb.ChannelGraph,
kvdb.Backend, error) {
// Create channelgraph for the first time.
backend, backendCleanup, err := kvdb.GetTestBackend(t.TempDir(), "cgr")
if err != nil {
return nil, nil, err
}
t.Cleanup(backendCleanup)
graph, err := graphdb.NewChannelGraph(
&graphdb.Config{KVDB: backend},
graphdb.WithUseGraphCache(useCache),
)
if err != nil {
return nil, nil, err
}
require.NoError(t, graph.Start())
t.Cleanup(func() {
require.NoError(t, graph.Stop())
})
return graph, backend, nil
}
type testGraphInstance struct {
graph *graphdb.ChannelGraph
graphBackend kvdb.Backend
graph *graphdb.ChannelGraph
// aliasMap is a map from a node's alias to its public key. This type is
// provided in order to allow easily look up from the human memorable
+1 -1
View File
@@ -274,7 +274,7 @@ func GetTestBackend(path, name string) (Backend, func(), error) {
return nil, empty, err
}
backend, err := Open(
EtcdBackendName, context.TODO(), etcdConfig,
EtcdBackendName, context.Background(), etcdConfig,
)
return backend, cancel, err
+1 -1
View File
@@ -19,7 +19,7 @@ func TestDump(t *testing.T) {
f := NewEtcdTestFixture(t)
db, err := newEtcdBackend(context.TODO(), f.BackendConfig())
db, err := newEtcdBackend(context.Background(), f.BackendConfig())
require.NoError(t, err)
err = db.Update(func(tx walletdb.ReadWriteTx) error {
+10 -4
View File
@@ -76,7 +76,7 @@ func (f *EtcdTestFixture) NewBackend(singleWriter bool) walletdb.DB {
cfg.SingleWriter = true
}
db, err := newEtcdBackend(context.TODO(), cfg)
db, err := newEtcdBackend(context.Background(), cfg)
require.NoError(f.t, err)
return db
@@ -84,7 +84,9 @@ func (f *EtcdTestFixture) NewBackend(singleWriter bool) walletdb.DB {
// Put puts a string key/value into the test etcd database.
func (f *EtcdTestFixture) Put(key, value string) {
ctx, cancel := context.WithTimeout(context.TODO(), testEtcdTimeout)
ctx, cancel := context.WithTimeout(
context.Background(), testEtcdTimeout,
)
defer cancel()
_, err := f.cli.Put(ctx, key, value)
@@ -95,7 +97,9 @@ func (f *EtcdTestFixture) Put(key, value string) {
// Get queries a key and returns the stored value from the test etcd database.
func (f *EtcdTestFixture) Get(key string) string {
ctx, cancel := context.WithTimeout(context.TODO(), testEtcdTimeout)
ctx, cancel := context.WithTimeout(
context.Background(), testEtcdTimeout,
)
defer cancel()
resp, err := f.cli.Get(ctx, key)
@@ -112,7 +116,9 @@ func (f *EtcdTestFixture) Get(key string) string {
// Dump scans and returns all key/values from the test etcd database.
func (f *EtcdTestFixture) Dump() map[string]string {
ctx, cancel := context.WithTimeout(context.TODO(), testEtcdTimeout)
ctx, cancel := context.WithTimeout(
context.Background(), testEtcdTimeout,
)
defer cancel()
resp, err := f.cli.Get(ctx, "\x00", clientv3.WithFromKey())
+2 -2
View File
@@ -16,7 +16,7 @@ func TestChangeDuringManualTx(t *testing.T) {
f := NewEtcdTestFixture(t)
db, err := newEtcdBackend(context.TODO(), f.BackendConfig())
db, err := newEtcdBackend(context.Background(), f.BackendConfig())
require.NoError(t, err)
tx, err := db.BeginReadWriteTx()
@@ -44,7 +44,7 @@ func TestChangeDuringUpdate(t *testing.T) {
f := NewEtcdTestFixture(t)
db, err := newEtcdBackend(context.TODO(), f.BackendConfig())
db, err := newEtcdBackend(context.Background(), f.BackendConfig())
require.NoError(t, err)
count := 0
+1 -1
View File
@@ -15,5 +15,5 @@ import (
func TestWalletDBInterface(t *testing.T) {
f := NewEtcdTestFixture(t)
cfg := f.BackendConfig()
walletdbtest.TestInterface(t, dbType, context.TODO(), &cfg)
walletdbtest.TestInterface(t, dbType, context.Background(), &cfg)
}
+2 -2
View File
@@ -626,7 +626,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg,
// Set up the core server which will listen for incoming peer
// connections.
server, err := newServer(
cfg, cfg.Listeners, dbs, activeChainControl, &idKeyDesc,
ctx, cfg, cfg.Listeners, dbs, activeChainControl, &idKeyDesc,
activeChainControl.Cfg.WalletUnlockParams.ChansToRestore,
multiAcceptor, torController, tlsManager, leaderElector,
implCfg,
@@ -758,7 +758,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg,
// case the startup of the subservers do not behave as expected.
errChan := make(chan error)
go func() {
errChan <- server.Start()
errChan <- server.Start(ctx)
}()
defer func() {
+2 -2
View File
@@ -198,7 +198,7 @@ func (s *Server) Status(ctx context.Context,
// ModifyStatus activates the current autopilot agent, if active.
//
// NOTE: Part of the AutopilotServer interface.
func (s *Server) ModifyStatus(ctx context.Context,
func (s *Server) ModifyStatus(_ context.Context,
in *ModifyStatusRequest) (*ModifyStatusResponse, error) {
log.Debugf("Setting agent enabled=%v", in.Enable)
@@ -236,7 +236,7 @@ func (s *Server) QueryScores(ctx context.Context, in *QueryScoresRequest) (
// Query the heuristics.
heuristicScores, err := s.manager.QueryHeuristics(
nodes, !in.IgnoreLocalState,
ctx, nodes, !in.IgnoreLocalState,
)
if err != nil {
return nil, err
+7 -2
View File
@@ -70,8 +70,8 @@ var _ SizeableMessage = (*ChannelAnnouncement1)(nil)
// io.Reader observing the specified protocol version.
//
// This is part of the lnwire.Message interface.
func (a *ChannelAnnouncement1) Decode(r io.Reader, pver uint32) error {
return ReadElements(r,
func (a *ChannelAnnouncement1) Decode(r io.Reader, _ uint32) error {
err := ReadElements(r,
&a.NodeSig1,
&a.NodeSig2,
&a.BitcoinSig1,
@@ -85,6 +85,11 @@ func (a *ChannelAnnouncement1) Decode(r io.Reader, pver uint32) error {
&a.BitcoinKey2,
&a.ExtraOpaqueData,
)
if err != nil {
return err
}
return a.ExtraOpaqueData.ValidateTLV()
}
// Encode serializes the target ChannelAnnouncement into the passed io.Writer
+1 -1
View File
@@ -126,7 +126,7 @@ func (c *ChannelAnnouncement2) DecodeTLVRecords(r io.Reader) error {
c.ExtraOpaqueData = tlvRecords
}
return nil
return c.ExtraOpaqueData.ValidateTLV()
}
// Encode serializes the target AnnounceSignatures1 into the passed io.Writer
+7 -2
View File
@@ -132,7 +132,7 @@ var _ SizeableMessage = (*ChannelUpdate1)(nil)
// io.Reader observing the specified protocol version.
//
// This is part of the lnwire.Message interface.
func (a *ChannelUpdate1) Decode(r io.Reader, pver uint32) error {
func (a *ChannelUpdate1) Decode(r io.Reader, _ uint32) error {
err := ReadElements(r,
&a.Signature,
a.ChainHash[:],
@@ -156,7 +156,12 @@ func (a *ChannelUpdate1) Decode(r io.Reader, pver uint32) error {
}
}
return a.ExtraOpaqueData.Decode(r)
err = a.ExtraOpaqueData.Decode(r)
if err != nil {
return err
}
return a.ExtraOpaqueData.ValidateTLV()
}
// Encode serializes the target ChannelUpdate into the passed io.Writer
+1 -1
View File
@@ -154,7 +154,7 @@ func (c *ChannelUpdate2) DecodeTLVRecords(r io.Reader) error {
c.ExtraOpaqueData = tlvRecords
}
return nil
return c.ExtraOpaqueData.ValidateTLV()
}
// Encode serializes the target ChannelUpdate2 into the passed io.Writer
+22
View File
@@ -63,6 +63,28 @@ func (e *ExtraOpaqueData) Decode(r io.Reader) error {
return nil
}
// ValidateTLV checks that the raw bytes that make up the ExtraOpaqueData
// instance are a valid TLV stream.
func (e *ExtraOpaqueData) ValidateTLV() error {
// There is nothing to validate if the ExtraOpaqueData is nil or empty.
if e == nil || len(*e) == 0 {
return nil
}
tlvStream, err := tlv.NewStream()
if err != nil {
return err
}
// Ensure that the TLV stream is valid by attempting to decode it.
_, err = tlvStream.DecodeWithParsedTypesP2P(bytes.NewReader(*e))
if err != nil {
return fmt.Errorf("invalid TLV stream: %w: %v", err, *e)
}
return nil
}
// PackRecords attempts to encode the set of tlv records into the target
// ExtraOpaqueData instance. The records will be encoded as a raw TLV stream
// and stored within the backing slice pointer.
+7 -2
View File
@@ -112,8 +112,8 @@ var _ SizeableMessage = (*NodeAnnouncement)(nil)
// io.Reader observing the specified protocol version.
//
// This is part of the lnwire.Message interface.
func (a *NodeAnnouncement) Decode(r io.Reader, pver uint32) error {
return ReadElements(r,
func (a *NodeAnnouncement) Decode(r io.Reader, _ uint32) error {
err := ReadElements(r,
&a.Signature,
&a.Features,
&a.Timestamp,
@@ -123,6 +123,11 @@ func (a *NodeAnnouncement) Decode(r io.Reader, pver uint32) error {
&a.Addresses,
&a.ExtraOpaqueData,
)
if err != nil {
return err
}
return a.ExtraOpaqueData.ValidateTLV()
}
// Encode serializes the target NodeAnnouncement into the passed io.Writer
+9 -6
View File
@@ -55,6 +55,7 @@ func setupTestRootKeyStorage(t *testing.T) kvdb.Backend {
// TestNewService tests the creation of the macaroon service.
func TestNewService(t *testing.T) {
t.Parallel()
ctx := context.Background()
// First, initialize a dummy DB file with a store that the service
// can read from. Make sure the file is removed in the end.
@@ -74,13 +75,13 @@ func TestNewService(t *testing.T) {
require.NoError(t, err, "Error unlocking root key storage")
// Third, check if the created service can bake macaroons.
_, err = service.NewMacaroon(context.TODO(), nil, testOperation)
_, err = service.NewMacaroon(ctx, nil, testOperation)
if err != macaroons.ErrMissingRootKeyID {
t.Fatalf("Received %v instead of ErrMissingRootKeyID", err)
}
macaroon, err := service.NewMacaroon(
context.TODO(), macaroons.DefaultRootKeyID, testOperation,
ctx, macaroons.DefaultRootKeyID, testOperation,
)
require.NoError(t, err, "Error creating macaroon from service")
if macaroon.Namespace().String() != "std:" {
@@ -108,6 +109,7 @@ func TestNewService(t *testing.T) {
// incoming context.
func TestValidateMacaroon(t *testing.T) {
t.Parallel()
ctx := context.Background()
// First, initialize the service and unlock it.
db := setupTestRootKeyStorage(t)
@@ -124,7 +126,7 @@ func TestValidateMacaroon(t *testing.T) {
// Then, create a new macaroon that we can serialize.
macaroon, err := service.NewMacaroon(
context.TODO(), macaroons.DefaultRootKeyID, testOperation,
ctx, macaroons.DefaultRootKeyID, testOperation,
testOperationURI,
)
require.NoError(t, err, "Error creating macaroon from service")
@@ -136,7 +138,7 @@ func TestValidateMacaroon(t *testing.T) {
md := metadata.New(map[string]string{
"macaroon": hex.EncodeToString(macaroonBinary),
})
mockContext := metadata.NewIncomingContext(context.Background(), md)
mockContext := metadata.NewIncomingContext(ctx, md)
// Finally, validate the macaroon against the required permissions.
err = service.ValidateMacaroon(
@@ -155,6 +157,7 @@ func TestValidateMacaroon(t *testing.T) {
// TestListMacaroonIDs checks that ListMacaroonIDs returns the expected result.
func TestListMacaroonIDs(t *testing.T) {
t.Parallel()
ctx := context.Background()
// First, initialize a dummy DB file with a store that the service
// can read from. Make sure the file is removed in the end.
@@ -176,12 +179,12 @@ func TestListMacaroonIDs(t *testing.T) {
// Third, make 3 new macaroons with different root key IDs.
expectedIDs := [][]byte{{1}, {2}, {3}}
for _, v := range expectedIDs {
_, err := service.NewMacaroon(context.TODO(), v, testOperation)
_, err := service.NewMacaroon(ctx, v, testOperation)
require.NoError(t, err, "Error creating macaroon from service")
}
// Finally, check that calling List return the expected values.
ids, _ := service.ListMacaroonIDs(context.TODO())
ids, _ := service.ListMacaroonIDs(ctx)
require.Equal(t, expectedIDs, ids, "root key IDs mismatch")
}
+8 -5
View File
@@ -58,12 +58,15 @@ func openTestStore(t *testing.T, tempDir string) *macaroons.RootKeyStorage {
// TestStore tests the normal use cases of the store like creating, unlocking,
// reading keys and closing it.
func TestStore(t *testing.T) {
t.Parallel()
ctx := context.Background()
tempDir, store := newTestStore(t)
_, _, err := store.RootKey(context.TODO())
_, _, err := store.RootKey(ctx)
require.Equal(t, macaroons.ErrStoreLocked, err)
_, err = store.Get(context.TODO(), nil)
_, err = store.Get(ctx, nil)
require.Equal(t, macaroons.ErrStoreLocked, err)
pw := []byte("weks")
@@ -72,18 +75,18 @@ func TestStore(t *testing.T) {
// Check ErrContextRootKeyID is returned when no root key ID found in
// context.
_, _, err = store.RootKey(context.TODO())
_, _, err = store.RootKey(ctx)
require.Equal(t, macaroons.ErrContextRootKeyID, err)
// Check ErrMissingRootKeyID is returned when empty root key ID is used.
emptyKeyID := make([]byte, 0)
badCtx := macaroons.ContextWithRootKeyID(context.TODO(), emptyKeyID)
badCtx := macaroons.ContextWithRootKeyID(ctx, emptyKeyID)
_, _, err = store.RootKey(badCtx)
require.Equal(t, macaroons.ErrMissingRootKeyID, err)
// Create a context with illegal root key ID value.
encryptedKeyID := []byte("enckey")
badCtx = macaroons.ContextWithRootKeyID(context.TODO(), encryptedKeyID)
badCtx = macaroons.ContextWithRootKeyID(ctx, encryptedKeyID)
_, _, err = store.RootKey(badCtx)
require.Equal(t, macaroons.ErrKeyValueForbidden, err)
+5 -1
View File
@@ -1989,9 +1989,13 @@ func newChanMsgStream(p *Brontide, cid lnwire.ChannelID) *msgStream {
// channel announcements.
func newDiscMsgStream(p *Brontide) *msgStream {
apply := func(msg lnwire.Message) {
// TODO(elle): thread contexts through the peer system properly
// so that a parent context can be passed in here.
ctx := context.TODO()
// TODO(yy): `ProcessRemoteAnnouncement` returns an error chan
// and we need to process it.
p.cfg.AuthGossiper.ProcessRemoteAnnouncement(msg, p)
p.cfg.AuthGossiper.ProcessRemoteAnnouncement(ctx, msg, p)
}
return newMsgStream(
+11
View File
@@ -0,0 +1,11 @@
package peer
import (
"testing"
"github.com/lightningnetwork/lnd/kvdb"
)
func TestMain(m *testing.M) {
kvdb.RunTests(m)
}
+2 -18
View File
@@ -23,7 +23,6 @@ import (
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lntest/channels"
"github.com/lightningnetwork/lnd/lntest/mock"
"github.com/lightningnetwork/lnd/lntypes"
@@ -603,28 +602,13 @@ func createTestPeer(t *testing.T) *peerTestCtx {
const chanActiveTimeout = time.Minute
dbPath := t.TempDir()
graphBackend, err := kvdb.GetBoltBackend(&kvdb.BoltBackendConfig{
DBPath: dbPath,
DBFileName: "graph.db",
NoFreelistSync: true,
AutoCompact: false,
AutoCompactMinAge: kvdb.DefaultBoltAutoCompactMinAge,
DBTimeout: kvdb.DefaultDBTimeout,
})
require.NoError(t, err)
dbAliceGraph, err := graphdb.NewChannelGraph(&graphdb.Config{
KVDB: graphBackend,
})
require.NoError(t, err)
dbAliceGraph := graphdb.MakeTestGraph(t)
require.NoError(t, dbAliceGraph.Start())
t.Cleanup(func() {
require.NoError(t, dbAliceGraph.Stop())
})
dbAliceChannel := channeldb.OpenForTesting(t, dbPath)
dbAliceChannel := channeldb.OpenForTesting(t, t.TempDir())
nodeSignerAlice := netann.NewNodeSigner(aliceKeySigner)
+24 -29
View File
@@ -159,26 +159,21 @@ func makeTestGraph(t *testing.T, useCache bool) (*graphdb.ChannelGraph,
kvdb.Backend, error) {
// Create channelgraph for the first time.
backend, backendCleanup, err := kvdb.GetTestBackend(t.TempDir(), "cgr")
if err != nil {
return nil, nil, err
}
t.Cleanup(backendCleanup)
graph, err := graphdb.NewChannelGraph(
&graphdb.Config{KVDB: backend},
graphdb.WithUseGraphCache(useCache),
)
if err != nil {
return nil, nil, err
}
graph := graphdb.MakeTestGraph(t, graphdb.WithUseGraphCache(useCache))
require.NoError(t, graph.Start())
t.Cleanup(func() {
require.NoError(t, graph.Stop())
})
return graph, backend, nil
mcBackend, backendCleanup, err := kvdb.GetTestBackend(
t.TempDir(), "mission_control",
)
if err != nil {
return nil, nil, err
}
t.Cleanup(backendCleanup)
return graph, mcBackend, nil
}
// parseTestGraph returns a fully populated ChannelGraph given a path to a JSON
@@ -210,7 +205,7 @@ func parseTestGraph(t *testing.T, useCache bool, path string) (
testAddrs = append(testAddrs, testAddr)
// Next, create a temporary graph database for usage within the test.
graph, graphBackend, err := makeTestGraph(t, useCache)
graph, mcBackend, err := makeTestGraph(t, useCache)
if err != nil {
return nil, err
}
@@ -410,12 +405,12 @@ func parseTestGraph(t *testing.T, useCache bool, path string) (
}
return &testGraphInstance{
graph: graph,
graphBackend: graphBackend,
aliasMap: aliasMap,
privKeyMap: privKeyMap,
channelIDs: channelIDs,
links: links,
graph: graph,
mcBackend: mcBackend,
aliasMap: aliasMap,
privKeyMap: privKeyMap,
channelIDs: channelIDs,
links: links,
}, nil
}
@@ -479,8 +474,8 @@ type testChannel struct {
}
type testGraphInstance struct {
graph *graphdb.ChannelGraph
graphBackend kvdb.Backend
graph *graphdb.ChannelGraph
mcBackend kvdb.Backend
// aliasMap is a map from a node's alias to its public key. This type is
// provided in order to allow easily look up from the human memorable alias
@@ -747,11 +742,11 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
}
return &testGraphInstance{
graph: graph,
graphBackend: graphBackend,
aliasMap: aliasMap,
privKeyMap: privKeyMap,
links: links,
graph: graph,
mcBackend: graphBackend,
aliasMap: aliasMap,
privKeyMap: privKeyMap,
links: links,
}, nil
}
+1 -1
View File
@@ -123,7 +123,7 @@ func createTestCtxFromGraphInstanceAssumeValid(t *testing.T,
mcConfig := &MissionControlConfig{Estimator: estimator}
mcController, err := NewMissionController(
graphInstance.graphBackend, route.Vertex{}, mcConfig,
graphInstance.mcBackend, route.Vertex{}, mcConfig,
)
require.NoError(t, err, "failed to create missioncontrol")
+4 -5
View File
@@ -56,7 +56,6 @@ import (
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/labels"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc"
@@ -6857,7 +6856,7 @@ func (r *rpcServer) GetNodeMetrics(ctx context.Context,
if err != nil {
return nil, err
}
if err := centralityMetric.Refresh(channelGraph); err != nil {
if err := centralityMetric.Refresh(ctx, channelGraph); err != nil {
return nil, err
}
@@ -6959,7 +6958,7 @@ func (r *rpcServer) GetNodeInfo(ctx context.Context,
)
err = graph.ForEachNodeChannel(node.PubKeyBytes,
func(_ kvdb.RTx, edge *models.ChannelEdgeInfo,
func(edge *models.ChannelEdgeInfo,
c1, c2 *models.ChannelEdgePolicy) error {
numChannels++
@@ -7140,7 +7139,7 @@ func (r *rpcServer) GetNetworkInfo(ctx context.Context,
// Graph diameter.
channelGraph := autopilot.ChannelGraphFromCachedDatabase(graph)
simpleGraph, err := autopilot.NewSimpleGraph(channelGraph)
simpleGraph, err := autopilot.NewSimpleGraph(ctx, channelGraph)
if err != nil {
return nil, err
}
@@ -7641,7 +7640,7 @@ func (r *rpcServer) FeeReport(ctx context.Context,
var feeReports []*lnrpc.ChannelFeeReport
err = channelGraph.ForEachNodeChannel(selfNode.PubKeyBytes,
func(_ kvdb.RTx, chanInfo *models.ChannelEdgeInfo,
func(chanInfo *models.ChannelEdgeInfo,
edgePolicy, _ *models.ChannelEdgePolicy) error {
// Self node should always have policies for its
+20 -36
View File
@@ -51,7 +51,6 @@ import (
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnencrypt"
"github.com/lightningnetwork/lnd/lnpeer"
@@ -554,7 +553,9 @@ func noiseDial(idKey keychain.SingleKeyECDH,
// newServer creates a new instance of the server which is to listen using the
// passed listener address.
func newServer(cfg *Config, listenAddrs []net.Addr,
//
//nolint:funlen
func newServer(_ context.Context, cfg *Config, listenAddrs []net.Addr,
dbs *DatabaseInstances, cc *chainreg.ChainControl,
nodeKeyDesc *keychain.KeyDescriptor,
chansToRestore walletunlocker.ChannelsToRecover,
@@ -1210,7 +1211,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
*models.ChannelEdgePolicy) error) error {
return s.graphDB.ForEachNodeChannel(selfVertex,
func(_ kvdb.RTx, c *models.ChannelEdgeInfo,
func(c *models.ChannelEdgeInfo,
e *models.ChannelEdgePolicy,
_ *models.ChannelEdgePolicy) error {
@@ -2217,7 +2218,7 @@ func (s *server) startLowLevelServices() error {
// NOTE: This function is safe for concurrent access.
//
//nolint:funlen
func (s *server) Start() error {
func (s *server) Start(ctx context.Context) error {
// Get the current blockbeat.
beat, err := s.getStartingBeat()
if err != nil {
@@ -2626,7 +2627,9 @@ func (s *server) Start() error {
}
s.wg.Add(1)
go s.peerBootstrapper(defaultMinPeers, bootstrappers)
go s.peerBootstrapper(
ctx, defaultMinPeers, bootstrappers,
)
} else {
srvrLog.Infof("Auto peer bootstrapping is disabled")
}
@@ -3073,7 +3076,7 @@ func (s *server) createBootstrapIgnorePeers() map[autopilot.NodeID]struct{} {
// invariant, we ensure that our node is connected to a diverse set of peers
// and that nodes newly joining the network receive an up to date network view
// as soon as possible.
func (s *server) peerBootstrapper(numTargetPeers uint32,
func (s *server) peerBootstrapper(ctx context.Context, numTargetPeers uint32,
bootstrappers []discovery.NetworkPeerBootstrapper) {
defer s.wg.Done()
@@ -3083,7 +3086,7 @@ func (s *server) peerBootstrapper(numTargetPeers uint32,
// We'll start off by aggressively attempting connections to peers in
// order to be a part of the network as soon as possible.
s.initialPeerBootstrap(ignoreList, numTargetPeers, bootstrappers)
s.initialPeerBootstrap(ctx, ignoreList, numTargetPeers, bootstrappers)
// Once done, we'll attempt to maintain our target minimum number of
// peers.
@@ -3161,7 +3164,7 @@ func (s *server) peerBootstrapper(numTargetPeers uint32,
ignoreList = s.createBootstrapIgnorePeers()
peerAddrs, err := discovery.MultiSourceBootstrap(
ignoreList, numNeeded*2, bootstrappers...,
ctx, ignoreList, numNeeded*2, bootstrappers...,
)
if err != nil {
srvrLog.Errorf("Unable to retrieve bootstrap "+
@@ -3210,8 +3213,8 @@ const bootstrapBackOffCeiling = time.Minute * 5
// initialPeerBootstrap attempts to continuously connect to peers on startup
// until the target number of peers has been reached. This ensures that nodes
// receive an up to date network view as soon as possible.
func (s *server) initialPeerBootstrap(ignore map[autopilot.NodeID]struct{},
numTargetPeers uint32,
func (s *server) initialPeerBootstrap(ctx context.Context,
ignore map[autopilot.NodeID]struct{}, numTargetPeers uint32,
bootstrappers []discovery.NetworkPeerBootstrapper) {
srvrLog.Debugf("Init bootstrap with targetPeers=%v, bootstrappers=%v, "+
@@ -3270,7 +3273,7 @@ func (s *server) initialPeerBootstrap(ignore map[autopilot.NodeID]struct{},
// in order to reach our target.
peersNeeded := numTargetPeers - numActivePeers
bootstrapAddrs, err := discovery.MultiSourceBootstrap(
ignore, peersNeeded, bootstrappers...,
ctx, ignore, peersNeeded, bootstrappers...,
)
if err != nil {
srvrLog.Errorf("Unable to retrieve initial bootstrap "+
@@ -3549,36 +3552,17 @@ func (s *server) establishPersistentConnections() error {
// After checking our previous connections for addresses to connect to,
// iterate through the nodes in our channel graph to find addresses
// that have been added via NodeAnnouncement messages.
sourceNode, err := s.graphDB.SourceNode()
if err != nil {
return fmt.Errorf("failed to fetch source node: %w", err)
}
// TODO(roasbeef): instead iterate over link nodes and query graph for
// each of the nodes.
selfPub := s.identityECDH.PubKey().SerializeCompressed()
err = s.graphDB.ForEachNodeChannel(sourceNode.PubKeyBytes, func(
tx kvdb.RTx,
chanInfo *models.ChannelEdgeInfo,
policy, _ *models.ChannelEdgePolicy) error {
err = s.graphDB.ForEachSourceNodeChannel(func(chanPoint wire.OutPoint,
havePolicy bool, channelPeer *models.LightningNode) error {
// If the remote party has announced the channel to us, but we
// haven't yet, then we won't have a policy. However, we don't
// need this to connect to the peer, so we'll log it and move on.
if policy == nil {
if !havePolicy {
srvrLog.Warnf("No channel policy found for "+
"ChannelPoint(%v): ", chanInfo.ChannelPoint)
}
// We'll now fetch the peer opposite from us within this
// channel so we can queue up a direct connection to them.
channelPeer, err := s.graphDB.FetchOtherNode(
tx, chanInfo, selfPub,
)
if err != nil {
return fmt.Errorf("unable to fetch channel peer for "+
"ChannelPoint(%v): %v", chanInfo.ChannelPoint,
err)
"ChannelPoint(%v): ", chanPoint)
}
pubStr := string(channelPeer.PubKeyBytes[:])
@@ -3638,8 +3622,8 @@ func (s *server) establishPersistentConnections() error {
return nil
})
if err != nil {
srvrLog.Errorf("Failed to iterate channels for node %x",
sourceNode.PubKeyBytes)
srvrLog.Errorf("Failed to iterate over source node channels: "+
"%v", err)
if !errors.Is(err, graphdb.ErrGraphNoEdgesFound) &&
!errors.Is(err, graphdb.ErrEdgeNotFound) {
+2 -2
View File
@@ -30,7 +30,7 @@ var (
//
// NOTE: The migration function may have runtime dependencies, which
// must be injected during runtime.
migrationConfig = []MigrationConfig{
migrationConfig = append([]MigrationConfig{
{
Name: "000001_invoices",
Version: 1,
@@ -70,7 +70,7 @@ var (
// schema. This is optional and can be disabled by the
// user if necessary.
},
}
}, migrationAdditions...)
)
// MigrationConfig is a configuration struct that describes SQL migrations. Each
+5
View File
@@ -0,0 +1,5 @@
//go:build test_db_postgres || test_db_sqlite
package sqldb
var migrationAdditions = []MigrationConfig{}
+5
View File
@@ -0,0 +1,5 @@
//go:build !test_db_postgres && !test_db_sqlite
package sqldb
var migrationAdditions []MigrationConfig
+4 -4
View File
@@ -39,7 +39,7 @@ type TestPgFixture struct {
// NewTestPgFixture constructs a new TestPgFixture starting up a docker
// container running Postgres 11. The started container will expire in after
// the passed duration.
func NewTestPgFixture(t *testing.T, expiry time.Duration) *TestPgFixture {
func NewTestPgFixture(t testing.TB, expiry time.Duration) *TestPgFixture {
// Use a sensible default on Windows (tcp/http) and linux/osx (socket)
// by specifying an empty endpoint.
pool, err := dockertest.NewPool("")
@@ -119,13 +119,13 @@ func (f *TestPgFixture) GetConfig(dbName string) *PostgresConfig {
}
// TearDown stops the underlying docker container.
func (f *TestPgFixture) TearDown(t *testing.T) {
func (f *TestPgFixture) TearDown(t testing.TB) {
err := f.pool.Purge(f.resource)
require.NoError(t, err, "Could not purge resource")
}
// randomDBName generates a random database name.
func randomDBName(t *testing.T) string {
func randomDBName(t testing.TB) string {
randBytes := make([]byte, 8)
_, err := rand.Read(randBytes)
require.NoError(t, err)
@@ -135,7 +135,7 @@ func randomDBName(t *testing.T) string {
// NewTestPostgresDB is a helper function that creates a Postgres database for
// testing using the given fixture.
func NewTestPostgresDB(t *testing.T, fixture *TestPgFixture) *PostgresStore {
func NewTestPostgresDB(t testing.TB, fixture *TestPgFixture) *PostgresStore {
t.Helper()
dbName := randomDBName(t)
+1 -1
View File
@@ -220,7 +220,7 @@ func (s *SqliteStore) SetSchemaVersion(version int, dirty bool) error {
// NewTestSqliteDB is a helper function that creates an SQLite database for
// testing.
func NewTestSqliteDB(t *testing.T) *SqliteStore {
func NewTestSqliteDB(t testing.TB) *SqliteStore {
t.Helper()
t.Logf("Creating new SQLite DB for testing")