github.com/MetalBlockchain/metalgo@v1.11.9/snow/consensus/snowman/topological.go (about) 1 // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. 2 // See the file LICENSE for licensing terms. 3 4 package snowman 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "time" 11 12 "go.uber.org/zap" 13 14 "github.com/MetalBlockchain/metalgo/ids" 15 "github.com/MetalBlockchain/metalgo/snow" 16 "github.com/MetalBlockchain/metalgo/snow/consensus/snowball" 17 "github.com/MetalBlockchain/metalgo/utils/bag" 18 "github.com/MetalBlockchain/metalgo/utils/set" 19 ) 20 21 var ( 22 errDuplicateAdd = errors.New("duplicate block add") 23 errUnknownParentBlock = errors.New("unknown parent block") 24 errTooManyProcessingBlocks = errors.New("too many processing blocks") 25 errBlockProcessingTooLong = errors.New("block processing too long") 26 27 _ Factory = (*TopologicalFactory)(nil) 28 _ Consensus = (*Topological)(nil) 29 ) 30 31 // TopologicalFactory implements Factory by returning a topological struct 32 type TopologicalFactory struct{} 33 34 func (TopologicalFactory) New() Consensus { 35 return &Topological{} 36 } 37 38 // Topological implements the Snowman interface by using a tree tracking the 39 // strongly preferred branch. This tree structure amortizes network polls to 40 // vote on more than just the next block. 41 type Topological struct { 42 metrics *metrics 43 44 // pollNumber is the number of times RecordPolls has been called 45 pollNumber uint64 46 47 // ctx is the context this snowman instance is executing in 48 ctx *snow.ConsensusContext 49 50 // params are the parameters that should be used to initialize snowball 51 // instances 52 params snowball.Parameters 53 54 lastAcceptedID ids.ID 55 lastAcceptedHeight uint64 56 57 // blocks stores the last accepted block and all the pending blocks 58 blocks map[ids.ID]*snowmanBlock // blockID -> snowmanBlock 59 60 // preferredIDs stores the set of IDs that are currently preferred. 61 preferredIDs set.Set[ids.ID] 62 63 // preferredHeights maps a height to the currently preferred block ID at 64 // that height. 65 preferredHeights map[uint64]ids.ID // height -> blockID 66 67 // preference is the preferred block with highest height 68 preference ids.ID 69 70 // Used in [calculateInDegree] and. 71 // Should only be accessed in that method. 72 // We use this one instance of set.Set instead of creating a 73 // new set.Set during each call to [calculateInDegree]. 74 leaves set.Set[ids.ID] 75 76 // Kahn nodes used in [calculateInDegree] and [markAncestorInDegrees]. 77 // Should only be accessed in those methods. 78 // We use this one map instead of creating a new map 79 // during each call to [calculateInDegree]. 80 kahnNodes map[ids.ID]kahnNode 81 } 82 83 // Used to track the kahn topological sort status 84 type kahnNode struct { 85 // inDegree is the number of children that haven't been processed yet. If 86 // inDegree is 0, then this node is a leaf 87 inDegree int 88 // votes for all the children of this node, so far 89 votes bag.Bag[ids.ID] 90 } 91 92 // Used to track which children should receive votes 93 type votes struct { 94 // parentID is the parent of all the votes provided in the votes bag 95 parentID ids.ID 96 // votes for all the children of the parent 97 votes bag.Bag[ids.ID] 98 } 99 100 func (ts *Topological) Initialize( 101 ctx *snow.ConsensusContext, 102 params snowball.Parameters, 103 lastAcceptedID ids.ID, 104 lastAcceptedHeight uint64, 105 lastAcceptedTime time.Time, 106 ) error { 107 err := params.Verify() 108 if err != nil { 109 return err 110 } 111 112 ts.metrics, err = newMetrics( 113 ctx.Log, 114 ctx.Registerer, 115 lastAcceptedHeight, 116 lastAcceptedTime, 117 ) 118 if err != nil { 119 return err 120 } 121 122 ts.leaves = set.Set[ids.ID]{} 123 ts.kahnNodes = make(map[ids.ID]kahnNode) 124 ts.ctx = ctx 125 ts.params = params 126 ts.lastAcceptedID = lastAcceptedID 127 ts.lastAcceptedHeight = lastAcceptedHeight 128 ts.blocks = map[ids.ID]*snowmanBlock{ 129 lastAcceptedID: {t: ts}, 130 } 131 ts.preferredHeights = make(map[uint64]ids.ID) 132 ts.preference = lastAcceptedID 133 return nil 134 } 135 136 func (ts *Topological) NumProcessing() int { 137 return len(ts.blocks) - 1 138 } 139 140 func (ts *Topological) Add(blk Block) error { 141 blkID := blk.ID() 142 height := blk.Height() 143 ts.ctx.Log.Verbo("adding block", 144 zap.Stringer("blkID", blkID), 145 zap.Uint64("height", height), 146 ) 147 148 // Make sure a block is not inserted twice. 149 if ts.Processing(blkID) { 150 return errDuplicateAdd 151 } 152 153 ts.metrics.Verified(height) 154 ts.metrics.Issued(blkID, ts.pollNumber) 155 156 parentID := blk.Parent() 157 parentNode, ok := ts.blocks[parentID] 158 if !ok { 159 return errUnknownParentBlock 160 } 161 162 // add the block as a child of its parent, and add the block to the tree 163 parentNode.AddChild(blk) 164 ts.blocks[blkID] = &snowmanBlock{ 165 t: ts, 166 blk: blk, 167 } 168 169 // If we are extending the preference, this is the new preference 170 if ts.preference == parentID { 171 ts.preference = blkID 172 ts.preferredIDs.Add(blkID) 173 ts.preferredHeights[height] = blkID 174 } 175 176 ts.ctx.Log.Verbo("added block", 177 zap.Stringer("blkID", blkID), 178 zap.Uint64("height", height), 179 zap.Stringer("parentID", parentID), 180 ) 181 return nil 182 } 183 184 func (ts *Topological) Processing(blkID ids.ID) bool { 185 // The last accepted block is in the blocks map, so we first must ensure the 186 // requested block isn't the last accepted block. 187 if blkID == ts.lastAcceptedID { 188 return false 189 } 190 // If the block is in the map of current blocks and not the last accepted 191 // block, then it is currently processing. 192 _, ok := ts.blocks[blkID] 193 return ok 194 } 195 196 func (ts *Topological) IsPreferred(blkID ids.ID) bool { 197 return blkID == ts.lastAcceptedID || ts.preferredIDs.Contains(blkID) 198 } 199 200 func (ts *Topological) LastAccepted() (ids.ID, uint64) { 201 return ts.lastAcceptedID, ts.lastAcceptedHeight 202 } 203 204 func (ts *Topological) Preference() ids.ID { 205 return ts.preference 206 } 207 208 func (ts *Topological) PreferenceAtHeight(height uint64) (ids.ID, bool) { 209 if height == ts.lastAcceptedHeight { 210 return ts.lastAcceptedID, true 211 } 212 blkID, ok := ts.preferredHeights[height] 213 return blkID, ok 214 } 215 216 // The votes bag contains at most K votes for blocks in the tree. If there is a 217 // vote for a block that isn't in the tree, the vote is dropped. 218 // 219 // Votes are propagated transitively towards the genesis. All blocks in the tree 220 // that result in at least Alpha votes will record the poll on their children. 221 // Every other block will have an unsuccessful poll registered. 222 // 223 // After collecting which blocks should be voted on, the polls are registered 224 // and blocks are accepted/rejected as needed. The preference is then updated to 225 // equal the leaf on the preferred branch. 226 // 227 // To optimize the theoretical complexity of the vote propagation, a topological 228 // sort is done over the blocks that are reachable from the provided votes. 229 // During the sort, votes are pushed towards the genesis. To prevent interating 230 // over all blocks that had unsuccessful polls, we set a flag on the block to 231 // know that any future traversal through that block should register an 232 // unsuccessful poll on that block and every descendant block. 233 // 234 // The complexity of this function is: 235 // - Runtime = 4 * |live set| + |votes| 236 // - Space = 2 * |live set| + |votes| 237 func (ts *Topological) RecordPoll(ctx context.Context, voteBag bag.Bag[ids.ID]) error { 238 // Register a new poll call 239 ts.pollNumber++ 240 241 var voteStack []votes 242 if voteBag.Len() >= ts.params.AlphaPreference { 243 // Since we received at least alpha votes, it's possible that 244 // we reached an alpha majority on a processing block. 245 // We must perform the traversals to calculate all block 246 // that reached an alpha majority. 247 248 // Populates [ts.kahnNodes] and [ts.leaves] 249 // Runtime = |live set| + |votes| ; Space = |live set| + |votes| 250 ts.calculateInDegree(voteBag) 251 252 // Runtime = |live set| ; Space = |live set| 253 voteStack = ts.pushVotes() 254 } 255 256 // Runtime = |live set| ; Space = Constant 257 preferred, err := ts.vote(ctx, voteStack) 258 if err != nil { 259 return err 260 } 261 262 // If the set of preferred IDs already contains the preference, then the 263 // preference is guaranteed to already be set correctly. This is because the 264 // value returned from vote reports the next preferred block after the last 265 // preferred block that was voted for. If this block was previously 266 // preferred, then we know that following the preferences down the chain 267 // will return the current preference. 268 if ts.preferredIDs.Contains(preferred) { 269 return nil 270 } 271 272 // Runtime = 2 * |live set| ; Space = Constant 273 ts.preferredIDs.Clear() 274 clear(ts.preferredHeights) 275 276 ts.preference = preferred 277 startBlock := ts.blocks[ts.preference] 278 279 // Runtime = |live set| ; Space = Constant 280 // Traverse from the preferred ID to the last accepted ancestor. 281 // 282 // It is guaranteed that the first decided block we encounter is the last 283 // accepted block because the startBlock is the preferred block. The 284 // preferred block is guaranteed to either be the last accepted block or 285 // extend the accepted chain. 286 for block := startBlock; !block.Decided(); { 287 blkID := block.blk.ID() 288 ts.preferredIDs.Add(blkID) 289 ts.preferredHeights[block.blk.Height()] = blkID 290 block = ts.blocks[block.blk.Parent()] 291 } 292 // Traverse from the preferred ID to the preferred child until there are no 293 // children. 294 for block := startBlock; block.sb != nil; { 295 ts.preference = block.sb.Preference() 296 ts.preferredIDs.Add(ts.preference) 297 block = ts.blocks[ts.preference] 298 // Invariant: Because the prior block had an initialized snowball 299 // instance, it must have a processing child. This guarantees that 300 // block.blk is non-nil here. 301 ts.preferredHeights[block.blk.Height()] = ts.preference 302 } 303 return nil 304 } 305 306 // HealthCheck returns information about the consensus health. 307 func (ts *Topological) HealthCheck(context.Context) (interface{}, error) { 308 var errs []error 309 310 numProcessingBlks := ts.NumProcessing() 311 if numProcessingBlks > ts.params.MaxOutstandingItems { 312 err := fmt.Errorf("%w: %d > %d", 313 errTooManyProcessingBlocks, 314 numProcessingBlks, 315 ts.params.MaxOutstandingItems, 316 ) 317 errs = append(errs, err) 318 } 319 320 maxTimeProcessing := ts.metrics.MeasureAndGetOldestDuration() 321 if maxTimeProcessing > ts.params.MaxItemProcessingTime { 322 err := fmt.Errorf("%w: %s > %s", 323 errBlockProcessingTooLong, 324 maxTimeProcessing, 325 ts.params.MaxItemProcessingTime, 326 ) 327 errs = append(errs, err) 328 } 329 330 return map[string]interface{}{ 331 "processingBlocks": numProcessingBlks, 332 "longestProcessingBlock": maxTimeProcessing.String(), // .String() is needed here to ensure a human readable format 333 "lastAcceptedID": ts.lastAcceptedID, 334 "lastAcceptedHeight": ts.lastAcceptedHeight, 335 }, errors.Join(errs...) 336 } 337 338 // takes in a list of votes and sets up the topological ordering. Returns the 339 // reachable section of the graph annotated with the number of inbound edges and 340 // the non-transitively applied votes. Also returns the list of leaf blocks. 341 func (ts *Topological) calculateInDegree(votes bag.Bag[ids.ID]) { 342 // Clear the Kahn node set 343 clear(ts.kahnNodes) 344 // Clear the leaf set 345 ts.leaves.Clear() 346 347 for _, vote := range votes.List() { 348 votedBlock, validVote := ts.blocks[vote] 349 350 // If the vote is for a block that isn't in the current pending set, 351 // then the vote is dropped 352 if !validVote { 353 continue 354 } 355 356 // If the vote is for the last accepted block, the vote is dropped 357 if votedBlock.Decided() { 358 continue 359 } 360 361 // The parent contains the snowball instance of its children 362 parentID := votedBlock.blk.Parent() 363 364 // Add the votes for this block to the parent's set of responses 365 numVotes := votes.Count(vote) 366 kahn, previouslySeen := ts.kahnNodes[parentID] 367 kahn.votes.AddCount(vote, numVotes) 368 ts.kahnNodes[parentID] = kahn 369 370 // If the parent block already had registered votes, then there is no 371 // need to iterate into the parents 372 if previouslySeen { 373 continue 374 } 375 376 // If I've never seen this parent block before, it is currently a leaf. 377 ts.leaves.Add(parentID) 378 379 // iterate through all the block's ancestors and set up the inDegrees of 380 // the blocks 381 for n := ts.blocks[parentID]; !n.Decided(); n = ts.blocks[parentID] { 382 parentID = n.blk.Parent() 383 384 // Increase the inDegree by one 385 kahn, previouslySeen := ts.kahnNodes[parentID] 386 kahn.inDegree++ 387 ts.kahnNodes[parentID] = kahn 388 389 // If we have already seen this block, then we shouldn't increase 390 // the inDegree of the ancestors through this block again. 391 if previouslySeen { 392 // Nodes are only leaves if they have no inbound edges. 393 ts.leaves.Remove(parentID) 394 break 395 } 396 } 397 } 398 } 399 400 // convert the tree into a branch of snowball instances with at least alpha 401 // votes 402 func (ts *Topological) pushVotes() []votes { 403 voteStack := make([]votes, 0, len(ts.kahnNodes)) 404 for ts.leaves.Len() > 0 { 405 // Pop one element of [leaves] 406 leafID, _ := ts.leaves.Pop() 407 // Should never return false because we just 408 // checked that [ts.leaves] is non-empty. 409 410 // get the block and sort information about the block 411 kahnNode := ts.kahnNodes[leafID] 412 block := ts.blocks[leafID] 413 414 // If there are at least Alpha votes, then this block needs to record 415 // the poll on the snowball instance 416 if kahnNode.votes.Len() >= ts.params.AlphaPreference { 417 voteStack = append(voteStack, votes{ 418 parentID: leafID, 419 votes: kahnNode.votes, 420 }) 421 } 422 423 // If the block is accepted, then we don't need to push votes to the 424 // parent block 425 if block.Decided() { 426 continue 427 } 428 429 parentID := block.blk.Parent() 430 431 // Remove an inbound edge from the parent kahn node and push the votes. 432 parentKahnNode := ts.kahnNodes[parentID] 433 parentKahnNode.inDegree-- 434 parentKahnNode.votes.AddCount(leafID, kahnNode.votes.Len()) 435 ts.kahnNodes[parentID] = parentKahnNode 436 437 // If the inDegree is zero, then the parent node is now a leaf 438 if parentKahnNode.inDegree == 0 { 439 ts.leaves.Add(parentID) 440 } 441 } 442 return voteStack 443 } 444 445 // apply votes to the branch that received an Alpha threshold and returns the 446 // next preferred block after the last preferred block that received an Alpha 447 // threshold. 448 func (ts *Topological) vote(ctx context.Context, voteStack []votes) (ids.ID, error) { 449 // If the voteStack is empty, then the full tree should falter. This won't 450 // change the preferred branch. 451 if len(voteStack) == 0 { 452 lastAcceptedBlock := ts.blocks[ts.lastAcceptedID] 453 lastAcceptedBlock.shouldFalter = true 454 455 if numProcessing := len(ts.blocks) - 1; numProcessing > 0 { 456 ts.ctx.Log.Verbo("no progress was made after processing pending blocks", 457 zap.Int("numProcessing", numProcessing), 458 ) 459 ts.metrics.FailedPoll() 460 } 461 return ts.preference, nil 462 } 463 464 // keep track of the new preferred block 465 newPreferred := ts.lastAcceptedID 466 onPreferredBranch := true 467 pollSuccessful := false 468 for len(voteStack) > 0 { 469 // pop a vote off the stack 470 newStackSize := len(voteStack) - 1 471 vote := voteStack[newStackSize] 472 voteStack = voteStack[:newStackSize] 473 474 // get the block that we are going to vote on 475 parentBlock, notRejected := ts.blocks[vote.parentID] 476 477 // if the block we are going to vote on was already rejected, then 478 // we should stop applying the votes 479 if !notRejected { 480 break 481 } 482 483 // keep track of transitive falters to propagate to this block's 484 // children 485 shouldTransitivelyFalter := parentBlock.shouldFalter 486 487 // if the block was previously marked as needing to falter, the block 488 // should falter before applying the vote 489 if shouldTransitivelyFalter { 490 ts.ctx.Log.Verbo("resetting confidence below parent", 491 zap.Stringer("parentID", vote.parentID), 492 ) 493 494 parentBlock.sb.RecordUnsuccessfulPoll() 495 parentBlock.shouldFalter = false 496 } 497 498 // apply the votes for this snowball instance 499 pollSuccessful = parentBlock.sb.RecordPoll(vote.votes) || pollSuccessful 500 501 // Only accept when you are finalized and a child of the last accepted 502 // block. 503 if parentBlock.sb.Finalized() && ts.lastAcceptedID == vote.parentID { 504 if err := ts.acceptPreferredChild(ctx, parentBlock); err != nil { 505 return ids.Empty, err 506 } 507 508 // by accepting the child of parentBlock, the last accepted block is 509 // no longer voteParentID, but its child. So, voteParentID can be 510 // removed from the tree. 511 delete(ts.blocks, vote.parentID) 512 } 513 514 // If we are on the preferred branch, then the parent's preference is 515 // the next block on the preferred branch. 516 parentPreference := parentBlock.sb.Preference() 517 if onPreferredBranch { 518 newPreferred = parentPreference 519 } 520 521 // Get the ID of the child that is having a RecordPoll called. All other 522 // children will need to have their confidence reset. If there isn't a 523 // child having RecordPoll called, then the nextID will default to the 524 // nil ID. 525 nextID := ids.Empty 526 if len(voteStack) > 0 { 527 nextID = voteStack[newStackSize-1].parentID 528 } 529 530 // If we are on the preferred branch and the nextID is the preference of 531 // the snowball instance, then we are following the preferred branch. 532 onPreferredBranch = onPreferredBranch && nextID == parentPreference 533 534 // If there wasn't an alpha threshold on the branch (either on this vote 535 // or a past transitive vote), I should falter now. 536 for childID := range parentBlock.children { 537 // If we don't need to transitively falter and the child is going to 538 // have RecordPoll called on it, then there is no reason to reset 539 // the block's confidence 540 if !shouldTransitivelyFalter && childID == nextID { 541 continue 542 } 543 544 // If we finalized a child of the current block, then all other 545 // children will have been rejected and removed from the tree. 546 // Therefore, we need to make sure the child is still in the tree. 547 childBlock, notRejected := ts.blocks[childID] 548 if notRejected { 549 ts.ctx.Log.Verbo("defering confidence reset of child block", 550 zap.Stringer("childID", childID), 551 ) 552 553 ts.ctx.Log.Verbo("voting for next block", 554 zap.Stringer("nextID", nextID), 555 ) 556 557 // If the child is ever voted for positively, the confidence 558 // must be reset first. 559 childBlock.shouldFalter = true 560 } 561 } 562 } 563 564 if pollSuccessful { 565 ts.metrics.SuccessfulPoll() 566 } else { 567 ts.metrics.FailedPoll() 568 } 569 return newPreferred, nil 570 } 571 572 // Accepts the preferred child of the provided snowman block. By accepting the 573 // preferred child, all other children will be rejected. When these children are 574 // rejected, all their descendants will be rejected. 575 // 576 // We accept a block once its parent's snowball instance has finalized 577 // with it as the preference. 578 func (ts *Topological) acceptPreferredChild(ctx context.Context, n *snowmanBlock) error { 579 // We are finalizing the block's child, so we need to get the preference 580 pref := n.sb.Preference() 581 582 // Get the child and accept it 583 child := n.children[pref] 584 // Notify anyone listening that this block was accepted. 585 bytes := child.Bytes() 586 // Note that BlockAcceptor.Accept must be called before child.Accept to 587 // honor Acceptor.Accept's invariant. 588 if err := ts.ctx.BlockAcceptor.Accept(ts.ctx, pref, bytes); err != nil { 589 return err 590 } 591 592 height := child.Height() 593 timestamp := child.Timestamp() 594 ts.ctx.Log.Trace("accepting block", 595 zap.Stringer("blkID", pref), 596 zap.Uint64("height", height), 597 zap.Time("timestamp", timestamp), 598 ) 599 if err := child.Accept(ctx); err != nil { 600 return err 601 } 602 603 // Update the last accepted values to the newly accepted block. 604 ts.lastAcceptedID = pref 605 ts.lastAcceptedHeight = height 606 // Remove the decided block from the set of processing IDs, as its status 607 // now implies its preferredness. 608 ts.preferredIDs.Remove(pref) 609 delete(ts.preferredHeights, height) 610 611 ts.metrics.Accepted( 612 pref, 613 height, 614 timestamp, 615 ts.pollNumber, 616 len(bytes), 617 ) 618 619 // Because ts.blocks contains the last accepted block, we don't delete the 620 // block from the blocks map here. 621 622 rejects := make([]ids.ID, 0, len(n.children)-1) 623 for childID, child := range n.children { 624 if childID == pref { 625 // don't reject the block we just accepted 626 continue 627 } 628 629 ts.ctx.Log.Trace("rejecting block", 630 zap.String("reason", "conflict with accepted block"), 631 zap.Stringer("blkID", childID), 632 zap.Uint64("height", child.Height()), 633 zap.Stringer("conflictID", pref), 634 ) 635 if err := child.Reject(ctx); err != nil { 636 return err 637 } 638 ts.metrics.Rejected(childID, ts.pollNumber, len(child.Bytes())) 639 640 // Track which blocks have been directly rejected 641 rejects = append(rejects, childID) 642 } 643 644 // reject all the descendants of the blocks we just rejected 645 return ts.rejectTransitively(ctx, rejects) 646 } 647 648 // Takes in a list of rejected ids and rejects all descendants of these IDs 649 func (ts *Topological) rejectTransitively(ctx context.Context, rejected []ids.ID) error { 650 // the rejected array is treated as a stack, with the next element at index 651 // 0 and the last element at the end of the slice. 652 for len(rejected) > 0 { 653 // pop the rejected ID off the stack 654 newRejectedSize := len(rejected) - 1 655 rejectedID := rejected[newRejectedSize] 656 rejected = rejected[:newRejectedSize] 657 658 // get the rejected node, and remove it from the tree 659 rejectedNode := ts.blocks[rejectedID] 660 delete(ts.blocks, rejectedID) 661 662 for childID, child := range rejectedNode.children { 663 ts.ctx.Log.Trace("rejecting block", 664 zap.String("reason", "rejected ancestor"), 665 zap.Stringer("blkID", childID), 666 zap.Uint64("height", child.Height()), 667 zap.Stringer("parentID", rejectedID), 668 ) 669 if err := child.Reject(ctx); err != nil { 670 return err 671 } 672 ts.metrics.Rejected(childID, ts.pollNumber, len(child.Bytes())) 673 674 // add the newly rejected block to the end of the stack 675 rejected = append(rejected, childID) 676 } 677 } 678 return nil 679 }