github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/common/follower/pending_tree/pending_tree.go (about)

     1  package pending_tree
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/onflow/flow-go/consensus/hotstuff/model"
     7  	"github.com/onflow/flow-go/model/flow"
     8  	"github.com/onflow/flow-go/module/forest"
     9  	"github.com/onflow/flow-go/module/mempool"
    10  )
    11  
    12  // PendingBlockVertex wraps a block proposal to implement forest.Vertex
    13  // so the proposal can be stored in forest.LevelledForest
    14  type PendingBlockVertex struct {
    15  	flow.CertifiedBlock
    16  	connectedToFinalized bool
    17  }
    18  
    19  var _ forest.Vertex = (*PendingBlockVertex)(nil)
    20  
    21  // NewVertex creates new vertex while performing a sanity check of data correctness.
    22  func NewVertex(certifiedBlock flow.CertifiedBlock, connectedToFinalized bool) (*PendingBlockVertex, error) {
    23  	return &PendingBlockVertex{
    24  		CertifiedBlock:       certifiedBlock,
    25  		connectedToFinalized: connectedToFinalized,
    26  	}, nil
    27  }
    28  
    29  func (v *PendingBlockVertex) VertexID() flow.Identifier { return v.CertifyingQC.BlockID }
    30  func (v *PendingBlockVertex) Level() uint64             { return v.CertifyingQC.View }
    31  func (v *PendingBlockVertex) Parent() (flow.Identifier, uint64) {
    32  	return v.Block.Header.ParentID, v.Block.Header.ParentView
    33  }
    34  
    35  // PendingTree is a mempool holding certified blocks that eventually might be connected to the finalized state.
    36  // As soon as a valid fork of certified blocks descending from the latest finalized block is observed,
    37  // we pass this information to caller. Internally, the mempool utilizes the LevelledForest.
    38  // PendingTree is NOT safe to use in concurrent environment.
    39  // Note:
    40  //   - The ability to skip ahead is irrelevant for staked nodes, which continuously follow the chain.
    41  //     However, light clients don't necessarily follow the chain block by block. Assume a light client
    42  //     that knows the EpochCommit event, i.e. the consensus committee authorized to certify blocks. A
    43  //     staked node can easily ship a proof of finalization for a block within that epoch to such a
    44  //     light client. This would be much cheaper for the light client than downloading the headers for
    45  //     all blocks in the epoch.
    46  //   - The pending tree supports skipping ahead, as this is a more general and simpler algorithm.
    47  //     Removing the ability to skip ahead would restrict the PendingTree's domain of potential
    48  //     applications _and_ would require additional code and additional tests making it more complex.
    49  //
    50  // Outlook:
    51  //   - At the moment, PendingTree relies on notion of a `Certified Block` which is a valid block accompanied
    52  //     by a certifying QC (proving block validity). This works well for consensus follower, as it is designed
    53  //     to work with certified blocks.
    54  //   - In the future, we could use the PendingTree also for consensus participants. Therefore, we would need
    55  //     to abstract out CertifiedBlock or replace it with a generic argument that satisfies some contract
    56  //     (returns View, Height, BlockID). Then, consensus participants could use the Pending Tree without
    57  //     QCs and instead fully validate inbound blocks (incl. payloads) to guarantee block validity.
    58  type PendingTree struct {
    59  	forest          *forest.LevelledForest
    60  	lastFinalizedID flow.Identifier
    61  }
    62  
    63  // NewPendingTree creates new instance of PendingTree. Accepts finalized block to set up initial state.
    64  func NewPendingTree(finalized *flow.Header) *PendingTree {
    65  	return &PendingTree{
    66  		forest:          forest.NewLevelledForest(finalized.View),
    67  		lastFinalizedID: finalized.ID(),
    68  	}
    69  }
    70  
    71  // AddBlocks accepts a batch of certified blocks, adds them to the tree of pending blocks and finds blocks connected to
    72  // the finalized state.
    73  //
    74  // Details:
    75  // Adding blocks might result in additional blocks now being connected to the latest finalized block. The returned
    76  // slice contains:
    77  //  1. the subset of `certifiedBlocks` that are connected to the finalized block
    78  //     - excluding any blocks whose view is smaller or equal to the finalized block
    79  //     - if a block `B ∈ certifiedBlocks` is already known to the PendingTree and connected,
    80  //     `B` and all its connected descendants will be in the returned list
    81  //  2. additionally, all of the _connected_ descendants of the blocks from step 1.
    82  //
    83  // PendingTree treats its input as a potentially repetitive stream of information: repeated inputs are already
    84  // consistent with the current state. While repetitive inputs might cause repetitive outputs, the implementation
    85  // has some general heuristics to avoid extra work:
    86  //   - It drops blocks whose view is smaller or equal to the finalized block
    87  //   - It deduplicates incoming blocks. We don't store additional vertices in tree if we have that block already stored.
    88  //
    89  // Expected errors during normal operations:
    90  //   - model.ByzantineThresholdExceededError - detected two certified blocks at the same view
    91  //
    92  // All other errors should be treated as exceptions.
    93  func (t *PendingTree) AddBlocks(certifiedBlocks []flow.CertifiedBlock) ([]flow.CertifiedBlock, error) {
    94  	var allConnectedBlocks []flow.CertifiedBlock
    95  	for _, block := range certifiedBlocks {
    96  		// skip blocks lower than finalized view
    97  		if block.View() <= t.forest.LowestLevel {
    98  			continue
    99  		}
   100  
   101  		iter := t.forest.GetVerticesAtLevel(block.View())
   102  		if iter.HasNext() {
   103  			v := iter.NextVertex().(*PendingBlockVertex)
   104  
   105  			if v.VertexID() == block.ID() {
   106  				// this vertex is already in tree, skip it
   107  				continue
   108  			} else {
   109  				return nil, model.ByzantineThresholdExceededError{Evidence: fmt.Sprintf(
   110  					"conflicting QCs at view %d: %v and %v",
   111  					block.View(), v.ID(), block.ID(),
   112  				)}
   113  			}
   114  		}
   115  
   116  		vertex, err := NewVertex(block, false)
   117  		if err != nil {
   118  			return nil, fmt.Errorf("could not create new vertex: %w", err)
   119  		}
   120  		err = t.forest.VerifyVertex(vertex)
   121  		if err != nil {
   122  			return nil, fmt.Errorf("failed to store certified block into the tree: %w", err)
   123  		}
   124  		t.forest.AddVertex(vertex)
   125  
   126  		if t.connectsToFinalizedBlock(block) {
   127  			allConnectedBlocks = t.updateAndCollectFork(allConnectedBlocks, vertex)
   128  		}
   129  	}
   130  
   131  	return allConnectedBlocks, nil
   132  }
   133  
   134  // connectsToFinalizedBlock checks if candidate block connects to the finalized state.
   135  func (t *PendingTree) connectsToFinalizedBlock(block flow.CertifiedBlock) bool {
   136  	if block.Block.Header.ParentID == t.lastFinalizedID {
   137  		return true
   138  	}
   139  	if parentVertex, found := t.forest.GetVertex(block.Block.Header.ParentID); found {
   140  		return parentVertex.(*PendingBlockVertex).connectedToFinalized
   141  	}
   142  	return false
   143  }
   144  
   145  // FinalizeFork takes last finalized block and prunes all blocks below the finalized view.
   146  // PendingTree treats its input as a potentially repetitive stream of information: repeated
   147  // and older inputs (out of order) are already consistent with the current state. Repetitive
   148  // inputs might cause repetitive outputs.
   149  // When a block is finalized we don't care for any blocks below it, since they were already finalized.
   150  // Finalizing a block might causes the pending PendingTree to detect _additional_ blocks as now
   151  // being connected to the latest finalized block. This happens if some connecting blocks are missing
   152  // and then a block higher than the missing blocks is finalized.
   153  // In the following example, B is the last finalized block known to the PendingTree
   154  //
   155  //	A ← B  ←-?-?-?-- X ← Y ← Z
   156  //
   157  // The network has already progressed to finalizing block X. However, the interim blocks denoted
   158  // by '←-?-?-?--' have not been received by our PendingTree. Therefore, we still consider X,Y,Z
   159  // as disconnected. If the PendingTree tree is now informed that X is finalized, it can fast-
   160  // forward to the respective state, as it anyway would prune all the blocks below X.
   161  // Note:
   162  //   - The ability to skip ahead is irrelevant for staked nodes, which continuously follows the chain.
   163  //     However, light clients don't necessarily follow the chain block by block. Assume a light client
   164  //     that knows the EpochCommit event, i.e. the consensus committee authorized to certify blocks. A
   165  //     staked node can easily ship a proof of finalization for a block within that epoch to such a
   166  //     light client. This would be much cheaper for the light client than downloading the headers for
   167  //     all blocks in the epoch.
   168  //   - The pending tree supports skipping ahead, as this is a more general and simpler algorithm.
   169  //     Removing the ability to skip ahead would restrict the PendingTree's its domain of potential
   170  //     applications _and_ would require additional code and additional tests making it more complex.
   171  //
   172  // If the PendingTree detects additional blocks as descending from the latest finalized block, it
   173  // returns these blocks. Returned blocks are ordered such that parents appear before their children.
   174  //
   175  // No errors are expected during normal operation.
   176  func (t *PendingTree) FinalizeFork(finalized *flow.Header) ([]flow.CertifiedBlock, error) {
   177  	var connectedBlocks []flow.CertifiedBlock
   178  
   179  	err := t.forest.PruneUpToLevel(finalized.View)
   180  	if err != nil {
   181  		if mempool.IsBelowPrunedThresholdError(err) {
   182  			return nil, nil
   183  		}
   184  		return connectedBlocks, fmt.Errorf("could not prune tree up to view %d: %w", finalized.View, err)
   185  	}
   186  	t.lastFinalizedID = finalized.ID()
   187  
   188  	iter := t.forest.GetChildren(t.lastFinalizedID)
   189  	for iter.HasNext() {
   190  		v := iter.NextVertex().(*PendingBlockVertex)
   191  		connectedBlocks = t.updateAndCollectFork(connectedBlocks, v)
   192  	}
   193  
   194  	return connectedBlocks, nil
   195  }
   196  
   197  // updateAndCollectFork marks the subtree rooted at `vertex.Block` as connected to the finalized state
   198  // and returns all blocks in this subtree. No parents of `vertex.Block` are modified or included in the output.
   199  // The output list will be ordered so that parents appear before children.
   200  // The caller must ensure that `vertex.Block` is connected to the finalized state.
   201  //
   202  //	A ← B ← C ←D
   203  //	      ↖ E
   204  //
   205  // For example, suppose B is the input vertex. Then:
   206  //   - A must already be connected to the finalized state
   207  //   - B, E, C, D are marked as connected to the finalized state and included in the output list
   208  //
   209  // This method has a similar signature as `append` for performance reasons:
   210  //   - any connected certified blocks are appended to `queue`
   211  //   - we return the _resulting slice_ after all appends
   212  func (t *PendingTree) updateAndCollectFork(queue []flow.CertifiedBlock, vertex *PendingBlockVertex) []flow.CertifiedBlock {
   213  	if vertex.connectedToFinalized {
   214  		return queue // no-op if already connected
   215  	}
   216  	vertex.connectedToFinalized = true
   217  	queue = append(queue, vertex.CertifiedBlock)
   218  
   219  	iter := t.forest.GetChildren(vertex.VertexID())
   220  	for iter.HasNext() {
   221  		nextVertex := iter.NextVertex().(*PendingBlockVertex)
   222  		queue = t.updateAndCollectFork(queue, nextVertex)
   223  	}
   224  	return queue
   225  }