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