github.com/decred/dcrlnd@v0.7.6/autopilot/prefattach_test.go (about)

     1  package autopilot
     2  
     3  import (
     4  	"bytes"
     5  	"io/ioutil"
     6  	"os"
     7  	"testing"
     8  	"time"
     9  
    10  	prand "math/rand"
    11  
    12  	"github.com/decred/dcrd/dcrec/secp256k1/v4"
    13  	"github.com/decred/dcrd/dcrutil/v4"
    14  	"github.com/decred/dcrlnd/channeldb"
    15  )
    16  
    17  type genGraphFunc func() (testGraph, func(), error)
    18  
    19  type testGraph interface {
    20  	ChannelGraph
    21  
    22  	addRandChannel(*secp256k1.PublicKey, *secp256k1.PublicKey,
    23  		dcrutil.Amount) (*ChannelEdge, *ChannelEdge, error)
    24  
    25  	addRandNode() (*secp256k1.PublicKey, error)
    26  }
    27  
    28  func newDiskChanGraph() (testGraph, func(), error) {
    29  	// First, create a temporary directory to be used for the duration of
    30  	// this test.
    31  	tempDirName, err := ioutil.TempDir("", "channeldb")
    32  	if err != nil {
    33  		return nil, nil, err
    34  	}
    35  
    36  	// Next, create channeldb for the first time.
    37  	cdb, err := channeldb.Open(tempDirName)
    38  	if err != nil {
    39  		return nil, nil, err
    40  	}
    41  
    42  	cleanUp := func() {
    43  		cdb.Close()
    44  		os.RemoveAll(tempDirName)
    45  	}
    46  
    47  	return &databaseChannelGraph{
    48  		db: cdb.ChannelGraph(),
    49  	}, cleanUp, nil
    50  }
    51  
    52  var _ testGraph = (*databaseChannelGraph)(nil)
    53  
    54  func newMemChanGraph() (testGraph, func(), error) {
    55  	return newMemChannelGraph(), nil, nil
    56  }
    57  
    58  var _ testGraph = (*memChannelGraph)(nil)
    59  
    60  var chanGraphs = []struct {
    61  	name    string
    62  	genFunc genGraphFunc
    63  }{
    64  	{
    65  		name:    "disk_graph",
    66  		genFunc: newDiskChanGraph,
    67  	},
    68  	{
    69  		name:    "mem_graph",
    70  		genFunc: newMemChanGraph,
    71  	},
    72  }
    73  
    74  // TestPrefAttachmentSelectEmptyGraph ensures that when passed an
    75  // empty graph, the NodeSores function always returns a score of 0.
    76  func TestPrefAttachmentSelectEmptyGraph(t *testing.T) {
    77  	prefAttach := NewPrefAttachment()
    78  
    79  	// Create a random public key, which we will query to get a score for.
    80  	pub, err := randKey()
    81  	if err != nil {
    82  		t.Fatalf("unable to generate key: %v", err)
    83  	}
    84  
    85  	nodes := map[NodeID]struct{}{
    86  		NewNodeID(pub): {},
    87  	}
    88  
    89  	for _, graph := range chanGraphs {
    90  		success := t.Run(graph.name, func(t1 *testing.T) {
    91  			graph, cleanup, err := graph.genFunc()
    92  			if err != nil {
    93  				t1.Fatalf("unable to create graph: %v", err)
    94  			}
    95  			if cleanup != nil {
    96  				defer cleanup()
    97  			}
    98  
    99  			// With the necessary state initialized, we'll now
   100  			// attempt to get the score for this one node.
   101  			const walletFunds = dcrutil.AtomsPerCoin
   102  			scores, err := prefAttach.NodeScores(graph, nil,
   103  				walletFunds, nodes)
   104  			if err != nil {
   105  				t1.Fatalf("unable to select attachment "+
   106  					"directives: %v", err)
   107  			}
   108  
   109  			// Since the graph is empty, we expect the score to be
   110  			// 0, giving an empty return map.
   111  			if len(scores) != 0 {
   112  				t1.Fatalf("expected empty score map, "+
   113  					"instead got %v ", len(scores))
   114  			}
   115  		})
   116  		if !success {
   117  			break
   118  		}
   119  	}
   120  }
   121  
   122  // TestPrefAttachmentSelectTwoVertexes ensures that when passed a graph with
   123  // only two eligible vertexes, then both are given the same score, and the
   124  // funds are appropriately allocated across each peer.
   125  func TestPrefAttachmentSelectTwoVertexes(t *testing.T) {
   126  	t.Parallel()
   127  
   128  	prand.Seed(time.Now().Unix())
   129  
   130  	const (
   131  		maxChanSize = dcrutil.Amount(dcrutil.AtomsPerCoin)
   132  	)
   133  
   134  	for _, graph := range chanGraphs {
   135  		success := t.Run(graph.name, func(t1 *testing.T) {
   136  			graph, cleanup, err := graph.genFunc()
   137  			if err != nil {
   138  				t1.Fatalf("unable to create graph: %v", err)
   139  			}
   140  			if cleanup != nil {
   141  				defer cleanup()
   142  			}
   143  
   144  			prefAttach := NewPrefAttachment()
   145  
   146  			// For this set, we'll load the memory graph with two
   147  			// nodes, and a random channel connecting them.
   148  			const chanCapacity = dcrutil.AtomsPerCoin
   149  			edge1, edge2, err := graph.addRandChannel(nil, nil, chanCapacity)
   150  			if err != nil {
   151  				t1.Fatalf("unable to generate channel: %v", err)
   152  			}
   153  
   154  			// We also add a third, non-connected node to the graph.
   155  			_, err = graph.addRandNode()
   156  			if err != nil {
   157  				t1.Fatalf("unable to add random node: %v", err)
   158  			}
   159  
   160  			// Get the score for all nodes found in the graph at
   161  			// this point.
   162  			nodes := make(map[NodeID]struct{})
   163  			if err := graph.ForEachNode(func(n Node) error {
   164  				nodes[n.PubKey()] = struct{}{}
   165  				return nil
   166  			}); err != nil {
   167  				t1.Fatalf("unable to traverse graph: %v", err)
   168  			}
   169  
   170  			if len(nodes) != 3 {
   171  				t1.Fatalf("expected 2 nodes, found %d", len(nodes))
   172  			}
   173  
   174  			// With the necessary state initialized, we'll now
   175  			// attempt to get our candidates channel score given
   176  			// the current state of the graph.
   177  			candidates, err := prefAttach.NodeScores(graph, nil,
   178  				maxChanSize, nodes)
   179  			if err != nil {
   180  				t1.Fatalf("unable to select attachment "+
   181  					"directives: %v", err)
   182  			}
   183  
   184  			// We expect two candidates, since one of the nodes
   185  			// doesn't have any channels.
   186  			if len(candidates) != 2 {
   187  				t1.Fatalf("2 nodes should be scored, "+
   188  					"instead %v were", len(candidates))
   189  			}
   190  
   191  			// The candidates should be amongst the two edges
   192  			// created above.
   193  			for nodeID, candidate := range candidates {
   194  				edge1Pub := edge1.Peer.PubKey()
   195  				edge2Pub := edge2.Peer.PubKey()
   196  
   197  				switch {
   198  				case bytes.Equal(nodeID[:], edge1Pub[:]):
   199  				case bytes.Equal(nodeID[:], edge2Pub[:]):
   200  				default:
   201  					t1.Fatalf("attached to unknown node: %x",
   202  						nodeID[:])
   203  				}
   204  
   205  				// Since each of the nodes has 1 channel, out
   206  				// of only one channel in the graph, we expect
   207  				// their score to be 1.0.
   208  				expScore := float64(1.0)
   209  				if candidate.Score != expScore {
   210  					t1.Fatalf("expected candidate score "+
   211  						"to be %v, instead was %v",
   212  						expScore, candidate.Score)
   213  				}
   214  			}
   215  		})
   216  		if !success {
   217  			break
   218  		}
   219  	}
   220  }
   221  
   222  // TestPrefAttachmentSelectGreedyAllocation tests that if upon
   223  // returning node scores, the NodeScores method will attempt to greedily
   224  // allocate all funds to each vertex (up to the max channel size).
   225  func TestPrefAttachmentSelectGreedyAllocation(t *testing.T) {
   226  	t.Parallel()
   227  
   228  	prand.Seed(time.Now().Unix())
   229  
   230  	const (
   231  		maxChanSize = dcrutil.Amount(dcrutil.AtomsPerCoin)
   232  	)
   233  
   234  	for _, graph := range chanGraphs {
   235  		success := t.Run(graph.name, func(t1 *testing.T) {
   236  			graph, cleanup, err := graph.genFunc()
   237  			if err != nil {
   238  				t1.Fatalf("unable to create graph: %v", err)
   239  			}
   240  			if cleanup != nil {
   241  				defer cleanup()
   242  			}
   243  
   244  			prefAttach := NewPrefAttachment()
   245  
   246  			const chanCapacity = dcrutil.AtomsPerCoin
   247  
   248  			// Next, we'll add 3 nodes to the graph, creating an
   249  			// "open triangle topology".
   250  			edge1, _, err := graph.addRandChannel(nil, nil,
   251  				chanCapacity)
   252  			if err != nil {
   253  				t1.Fatalf("unable to create channel: %v", err)
   254  			}
   255  			peerPubBytes := edge1.Peer.PubKey()
   256  			peerPub, err := secp256k1.ParsePubKey(peerPubBytes[:])
   257  			if err != nil {
   258  				t.Fatalf("unable to parse pubkey: %v", err)
   259  			}
   260  			_, _, err = graph.addRandChannel(
   261  				peerPub, nil, chanCapacity,
   262  			)
   263  			if err != nil {
   264  				t1.Fatalf("unable to create channel: %v", err)
   265  			}
   266  
   267  			// At this point, there should be three nodes in the
   268  			// graph, with node node having two edges.
   269  			numNodes := 0
   270  			twoChans := false
   271  			nodes := make(map[NodeID]struct{})
   272  			if err := graph.ForEachNode(func(n Node) error {
   273  				numNodes++
   274  				nodes[n.PubKey()] = struct{}{}
   275  				numChans := 0
   276  				err := n.ForEachChannel(func(c ChannelEdge) error {
   277  					numChans++
   278  					return nil
   279  				})
   280  				if err != nil {
   281  					return err
   282  				}
   283  
   284  				twoChans = twoChans || (numChans == 2)
   285  
   286  				return nil
   287  			}); err != nil {
   288  				t1.Fatalf("unable to traverse graph: %v", err)
   289  			}
   290  			if numNodes != 3 {
   291  				t1.Fatalf("expected 3 nodes, instead have: %v",
   292  					numNodes)
   293  			}
   294  			if !twoChans {
   295  				t1.Fatalf("expected node to have two channels")
   296  			}
   297  
   298  			// We'll now begin our test, modeling the available
   299  			// wallet balance to be 5.5 DCR. We're shooting for a
   300  			// 50/50 allocation, and have 3 DCR in channels. As a
   301  			// result, the heuristic should try to greedily
   302  			// allocate funds to channels.
   303  			scores, err := prefAttach.NodeScores(graph, nil,
   304  				maxChanSize, nodes)
   305  			if err != nil {
   306  				t1.Fatalf("unable to select attachment "+
   307  					"directives: %v", err)
   308  			}
   309  
   310  			if len(scores) != len(nodes) {
   311  				t1.Fatalf("all nodes should be scored, "+
   312  					"instead %v were", len(scores))
   313  			}
   314  
   315  			// The candidates should have a non-zero score, and
   316  			// have the max chan size funds recommended channel
   317  			// size.
   318  			for _, candidate := range scores {
   319  				if candidate.Score == 0 {
   320  					t1.Fatalf("Expected non-zero score")
   321  				}
   322  			}
   323  
   324  			// Imagine a few channels are being opened, and there's
   325  			// only 0.5 DCR left. That should leave us with channel
   326  			// candidates of that size.
   327  			const remBalance = dcrutil.AtomsPerCoin * 0.5
   328  			scores, err = prefAttach.NodeScores(graph, nil,
   329  				remBalance, nodes)
   330  			if err != nil {
   331  				t1.Fatalf("unable to select attachment "+
   332  					"directives: %v", err)
   333  			}
   334  
   335  			if len(scores) != len(nodes) {
   336  				t1.Fatalf("all nodes should be scored, "+
   337  					"instead %v were", len(scores))
   338  			}
   339  
   340  			// Check that the recommended channel sizes are now the
   341  			// remaining channel balance.
   342  			for _, candidate := range scores {
   343  				if candidate.Score == 0 {
   344  					t1.Fatalf("Expected non-zero score")
   345  				}
   346  			}
   347  		})
   348  		if !success {
   349  			break
   350  		}
   351  	}
   352  }
   353  
   354  // TestPrefAttachmentSelectSkipNodes ensures that if a node was
   355  // already selected as a channel counterparty, then that node will get a score
   356  // of zero during scoring.
   357  func TestPrefAttachmentSelectSkipNodes(t *testing.T) {
   358  	t.Parallel()
   359  
   360  	prand.Seed(time.Now().Unix())
   361  
   362  	const (
   363  		maxChanSize = dcrutil.Amount(dcrutil.AtomsPerCoin)
   364  	)
   365  
   366  	for _, graph := range chanGraphs {
   367  		success := t.Run(graph.name, func(t1 *testing.T) {
   368  			graph, cleanup, err := graph.genFunc()
   369  			if err != nil {
   370  				t1.Fatalf("unable to create graph: %v", err)
   371  			}
   372  			if cleanup != nil {
   373  				defer cleanup()
   374  			}
   375  
   376  			prefAttach := NewPrefAttachment()
   377  
   378  			// Next, we'll create a simple topology of two nodes,
   379  			// with a single channel connecting them.
   380  			const chanCapacity = dcrutil.AtomsPerCoin
   381  			_, _, err = graph.addRandChannel(nil, nil,
   382  				chanCapacity)
   383  			if err != nil {
   384  				t1.Fatalf("unable to create channel: %v", err)
   385  			}
   386  
   387  			nodes := make(map[NodeID]struct{})
   388  			if err := graph.ForEachNode(func(n Node) error {
   389  				nodes[n.PubKey()] = struct{}{}
   390  				return nil
   391  			}); err != nil {
   392  				t1.Fatalf("unable to traverse graph: %v", err)
   393  			}
   394  
   395  			if len(nodes) != 2 {
   396  				t1.Fatalf("expected 2 nodes, found %d", len(nodes))
   397  			}
   398  
   399  			// With our graph created, we'll now get the scores for
   400  			// all nodes in the graph.
   401  			scores, err := prefAttach.NodeScores(graph, nil,
   402  				maxChanSize, nodes)
   403  			if err != nil {
   404  				t1.Fatalf("unable to select attachment "+
   405  					"directives: %v", err)
   406  			}
   407  
   408  			if len(scores) != len(nodes) {
   409  				t1.Fatalf("all nodes should be scored, "+
   410  					"instead %v were", len(scores))
   411  			}
   412  
   413  			// THey should all have a score, and a maxChanSize
   414  			// channel size recommendation.
   415  			for _, candidate := range scores {
   416  				if candidate.Score == 0 {
   417  					t1.Fatalf("Expected non-zero score")
   418  				}
   419  			}
   420  
   421  			// We'll simulate a channel update by adding the nodes
   422  			// to our set of channels.
   423  			var chans []LocalChannel
   424  			for _, candidate := range scores {
   425  				chans = append(chans,
   426  					LocalChannel{
   427  						Node: candidate.NodeID,
   428  					},
   429  				)
   430  			}
   431  
   432  			// If we attempt to make a call to the NodeScores
   433  			// function, without providing any new information,
   434  			// then all nodes should have a score of zero, since we
   435  			// already got channels to them.
   436  			scores, err = prefAttach.NodeScores(graph, chans,
   437  				maxChanSize, nodes)
   438  			if err != nil {
   439  				t1.Fatalf("unable to select attachment "+
   440  					"directives: %v", err)
   441  			}
   442  
   443  			// Since all should be given a score of 0, the map
   444  			// should be empty.
   445  			if len(scores) != 0 {
   446  				t1.Fatalf("expected empty score map, "+
   447  					"instead got %v ", len(scores))
   448  			}
   449  		})
   450  		if !success {
   451  			break
   452  		}
   453  	}
   454  }