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 }