github.com/onflow/flow-go@v0.33.17/cmd/util/common/checkpoint.go (about)

     1  package common
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  
     7  	"github.com/rs/zerolog"
     8  	"github.com/rs/zerolog/log"
     9  
    10  	"github.com/onflow/flow-go/ledger"
    11  	"github.com/onflow/flow-go/ledger/complete/wal"
    12  	"github.com/onflow/flow-go/model/flow"
    13  	"github.com/onflow/flow-go/state/protocol"
    14  	"github.com/onflow/flow-go/state/protocol/snapshots"
    15  	"github.com/onflow/flow-go/storage"
    16  )
    17  
    18  // FindHeightsByCheckpoints finds the sealed height that produces the state commitment included in the checkpoint file.
    19  func FindHeightsByCheckpoints(
    20  	logger zerolog.Logger,
    21  	headers storage.Headers,
    22  	seals storage.Seals,
    23  	checkpointFilePath string,
    24  	blocksToSkip uint,
    25  	startHeight uint64,
    26  	endHeight uint64,
    27  ) (
    28  	uint64, // sealed height that produces the state commitment included in the checkpoint file
    29  	flow.StateCommitment, // the state commitment that matches the sealed height
    30  	uint64, // the finalized height that seals the sealed height
    31  	error,
    32  ) {
    33  
    34  	// find all trie root hashes in the checkpoint file
    35  	dir, fileName := filepath.Split(checkpointFilePath)
    36  	hashes, err := wal.ReadTriesRootHash(logger, dir, fileName)
    37  	if err != nil {
    38  		return 0, flow.DummyStateCommitment, 0,
    39  			fmt.Errorf("could not read trie root hashes from checkpoint file %v: %w",
    40  				checkpointFilePath, err)
    41  	}
    42  
    43  	// convert all trie root hashes to state commitments
    44  	commitments := hashesToCommits(hashes)
    45  
    46  	commitMap := make(map[flow.StateCommitment]struct{}, len(commitments))
    47  	for _, commit := range commitments {
    48  		commitMap[commit] = struct{}{}
    49  	}
    50  
    51  	// iterate backwards from the end height to the start height
    52  	// to find the block that produces a state commitment in the given list
    53  	// It is safe to skip blocks in this linear search because we expect `stateCommitments` to hold commits
    54  	// for a contiguous range of blocks (for correct operation we assume `blocksToSkip` is smaller than this range).
    55  	// end height must be a sealed block
    56  	step := blocksToSkip + 1
    57  	for height := endHeight; height >= startHeight; height -= uint64(step) {
    58  		finalizedID, err := headers.BlockIDByHeight(height)
    59  		if err != nil {
    60  			return 0, flow.DummyStateCommitment, 0,
    61  				fmt.Errorf("could not find block by height %v: %w", height, err)
    62  		}
    63  
    64  		// since height is a sealed block height, then we must be able to find the seal for this block
    65  		finalizedSeal, err := seals.HighestInFork(finalizedID)
    66  		if err != nil {
    67  			return 0, flow.DummyStateCommitment, 0,
    68  				fmt.Errorf("could not find seal for block %v at height %v: %w", finalizedID, height, err)
    69  		}
    70  
    71  		commit := finalizedSeal.FinalState
    72  
    73  		_, ok := commitMap[commit]
    74  		if ok {
    75  			sealedBlock, err := headers.ByBlockID(finalizedSeal.BlockID)
    76  			if err != nil {
    77  				return 0, flow.DummyStateCommitment, 0,
    78  					fmt.Errorf("could not find block by ID %v: %w", finalizedSeal.BlockID, err)
    79  			}
    80  
    81  			log.Info().Msgf("successfully found block %v (%v) that seals block %v (%v) for commit %x in checkpoint file %v",
    82  				height, finalizedID,
    83  				sealedBlock.Height, finalizedSeal.BlockID,
    84  				commit, checkpointFilePath)
    85  
    86  			return sealedBlock.Height, commit, height, nil
    87  		}
    88  
    89  		if height < uint64(step) {
    90  			break
    91  		}
    92  	}
    93  
    94  	return 0, flow.DummyStateCommitment, 0,
    95  		fmt.Errorf("could not find commit within height range [%v,%v]", startHeight, endHeight)
    96  }
    97  
    98  // GenerateProtocolSnapshotForCheckpoint finds a sealed block that produces the state commitment contained in the latest
    99  // checkpoint file, and return a protocol snapshot for the finalized block that seals the sealed block.
   100  // The returned protocol snapshot can be used for dynamic bootstrapping an execution node along with the latest checkpoint file.
   101  //
   102  // When finding a sealed block it iterates backwards through each sealed height from the last sealed height, and see
   103  // if the state commitment matches with one of the state commitments contained in the checkpoint file.
   104  // However, the iteration could be slow, in order to speed up the iteration, we can skip some blocks each time.
   105  // Since a checkpoint file usually contains 500 tries, which might cover around 250 blocks (assuming 2 tries per block),
   106  // then skipping 10 blocks each time will still allow us to find the sealed block while not missing the height contained
   107  // by the checkpoint file.
   108  // So the blocksToSkip parameter is used to skip some blocks each time when iterating the sealed heights.
   109  func GenerateProtocolSnapshotForCheckpoint(
   110  	logger zerolog.Logger,
   111  	state protocol.State,
   112  	headers storage.Headers,
   113  	seals storage.Seals,
   114  	checkpointDir string,
   115  	blocksToSkip uint,
   116  ) (protocol.Snapshot, uint64, flow.StateCommitment, string, error) {
   117  	// skip X blocks (i.e. 10) each time to find the block that produces the state commitment in the checkpoint file
   118  	// since a checkpoint file contains 500 tries, this allows us to find the block more efficiently
   119  	sealed, err := state.Sealed().Head()
   120  	if err != nil {
   121  		return nil, 0, flow.DummyStateCommitment, "", err
   122  	}
   123  	endHeight := sealed.Height
   124  
   125  	return GenerateProtocolSnapshotForCheckpointWithHeights(logger, state, headers, seals,
   126  		checkpointDir,
   127  		blocksToSkip,
   128  		endHeight,
   129  	)
   130  }
   131  
   132  // findLatestCheckpointFilePath finds the latest checkpoint file in the given directory
   133  // it returns the header file name of the latest checkpoint file
   134  func findLatestCheckpointFilePath(checkpointDir string) (string, error) {
   135  	_, last, err := wal.ListCheckpoints(checkpointDir)
   136  	if err != nil {
   137  		return "", fmt.Errorf("could not list checkpoints in directory %v: %w", checkpointDir, err)
   138  	}
   139  
   140  	fileName := wal.NumberToFilename(last)
   141  	if last < 0 {
   142  		fileName = "root.checkpoint"
   143  	}
   144  
   145  	checkpointFilePath := filepath.Join(checkpointDir, fileName)
   146  	return checkpointFilePath, nil
   147  }
   148  
   149  // GenerateProtocolSnapshotForCheckpointWithHeights does the same thing as GenerateProtocolSnapshotForCheckpoint
   150  // except that it allows the caller to specify the end height of the sealed block that we iterate backwards from.
   151  func GenerateProtocolSnapshotForCheckpointWithHeights(
   152  	logger zerolog.Logger,
   153  	state protocol.State,
   154  	headers storage.Headers,
   155  	seals storage.Seals,
   156  	checkpointDir string,
   157  	blocksToSkip uint,
   158  	endHeight uint64,
   159  ) (protocol.Snapshot, uint64, flow.StateCommitment, string, error) {
   160  	// Stop searching after 10,000 iterations or upon reaching the minimum height, whichever comes first.
   161  	startHeight := uint64(0)
   162  	// preventing startHeight from being negative
   163  	length := uint64(blocksToSkip+1) * 10000
   164  	if endHeight > length {
   165  		startHeight = endHeight - length
   166  	}
   167  
   168  	checkpointFilePath, err := findLatestCheckpointFilePath(checkpointDir)
   169  	if err != nil {
   170  		return nil, 0, flow.DummyStateCommitment, "", fmt.Errorf("could not find latest checkpoint file in directory %v: %w", checkpointDir, err)
   171  	}
   172  
   173  	log.Info().
   174  		Uint64("start_height", startHeight).
   175  		Uint64("end_height", endHeight).
   176  		Uint("blocksToSkip", blocksToSkip).
   177  		Msgf("generating protocol snapshot for checkpoint file %v", checkpointFilePath)
   178  	// find the height of the finalized block that produces the state commitment contained in the checkpoint file
   179  	sealedHeight, commit, finalizedHeight, err := FindHeightsByCheckpoints(logger, headers, seals, checkpointFilePath, blocksToSkip, startHeight, endHeight)
   180  	if err != nil {
   181  		return nil, 0, flow.DummyStateCommitment, "", fmt.Errorf("could not find sealed height in range [%v:%v] (blocksToSkip: %v) by checkpoints: %w",
   182  			startHeight, endHeight, blocksToSkip,
   183  			err)
   184  	}
   185  
   186  	snapshot := state.AtHeight(finalizedHeight)
   187  	validSnapshot, err := snapshots.GetDynamicBootstrapSnapshot(state, snapshot)
   188  	if err != nil {
   189  		return nil, 0, flow.DummyStateCommitment, "", fmt.Errorf("could not get dynamic bootstrap snapshot: %w", err)
   190  	}
   191  
   192  	return validSnapshot, sealedHeight, commit, checkpointFilePath, nil
   193  }
   194  
   195  // hashesToCommits converts a list of ledger.RootHash to a list of flow.StateCommitment
   196  func hashesToCommits(hashes []ledger.RootHash) []flow.StateCommitment {
   197  	commits := make([]flow.StateCommitment, len(hashes))
   198  	for i, h := range hashes {
   199  		commits[i] = flow.StateCommitment(h)
   200  	}
   201  	return commits
   202  }