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 }