github.com/onflow/flow-go@v0.33.17/ledger/complete/wal/checkpoint_v6_writer.go (about)

     1  package wal
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/binary"
     6  	"encoding/hex"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  
    13  	"github.com/docker/go-units"
    14  	"github.com/hashicorp/go-multierror"
    15  	"github.com/rs/zerolog"
    16  
    17  	"github.com/onflow/flow-go/ledger"
    18  	"github.com/onflow/flow-go/ledger/complete/mtrie/flattener"
    19  	"github.com/onflow/flow-go/ledger/complete/mtrie/node"
    20  	"github.com/onflow/flow-go/ledger/complete/mtrie/trie"
    21  	utilsio "github.com/onflow/flow-go/utils/io"
    22  )
    23  
    24  const subtrieLevel = 4
    25  const subtrieCount = 1 << subtrieLevel // 16
    26  
    27  func subtrieCountByLevel(level uint16) int {
    28  	return 1 << level
    29  }
    30  
    31  // StoreCheckpointV6SingleThread stores checkpoint file in v6 in a single threaded manner,
    32  // useful when EN is executing block.
    33  func StoreCheckpointV6SingleThread(tries []*trie.MTrie, outputDir string, outputFile string, logger zerolog.Logger) error {
    34  	return StoreCheckpointV6(tries, outputDir, outputFile, logger, 1)
    35  }
    36  
    37  // StoreCheckpointV6Concurrently stores checkpoint file in v6 in max workers,
    38  // useful during state extraction
    39  func StoreCheckpointV6Concurrently(tries []*trie.MTrie, outputDir string, outputFile string, logger zerolog.Logger) error {
    40  	return StoreCheckpointV6(tries, outputDir, outputFile, logger, 16)
    41  }
    42  
    43  // StoreCheckpointV6 stores checkpoint file into a main file and 17 file parts.
    44  // the main file stores:
    45  //   - version
    46  //   - checksum of each part file (17 in total)
    47  //   - checksum of the main file itself
    48  //     the first 16 files parts contain the trie nodes below the subtrieLevel
    49  //     the last part file contains the top level trie nodes above the subtrieLevel and all the trie root nodes.
    50  //
    51  // nWorker specifies how many workers to encode subtrie concurrently, valid range [1,16]
    52  func StoreCheckpointV6(
    53  	tries []*trie.MTrie, outputDir string, outputFile string, logger zerolog.Logger, nWorker uint) error {
    54  	err := storeCheckpointV6(tries, outputDir, outputFile, logger, nWorker)
    55  	if err != nil {
    56  		cleanupErr := deleteCheckpointFiles(outputDir, outputFile)
    57  		if cleanupErr != nil {
    58  			return fmt.Errorf("fail to cleanup temp file %s, after running into error: %w", cleanupErr, err)
    59  		}
    60  		return err
    61  	}
    62  
    63  	return nil
    64  }
    65  
    66  func storeCheckpointV6(
    67  	tries []*trie.MTrie, outputDir string, outputFile string, logger zerolog.Logger, nWorker uint) error {
    68  	if len(tries) == 0 {
    69  		logger.Info().Msg("no tries to be checkpointed")
    70  		return nil
    71  	}
    72  
    73  	first, last := tries[0], tries[len(tries)-1]
    74  	lg := logger.With().
    75  		Int("version", 6).
    76  		Int("trie_count", len(tries)).
    77  		Str("checkpoint_file", path.Join(outputDir, outputFile)).
    78  		Logger()
    79  
    80  	lg.Info().
    81  		Str("first_hash", first.RootHash().String()).
    82  		Uint64("first_reg_count", first.AllocatedRegCount()).
    83  		Str("first_reg_size", units.BytesSize(float64(first.AllocatedRegSize()))).
    84  		Str("last_hash", last.RootHash().String()).
    85  		Uint64("last_reg_count", last.AllocatedRegCount()).
    86  		Str("last_reg_size", units.BytesSize(float64(last.AllocatedRegSize()))).
    87  		Msg("storing checkpoint")
    88  
    89  	// make sure a checkpoint file with same name doesn't exist
    90  	// part file with same name doesn't exist either
    91  	matched, err := findCheckpointPartFiles(outputDir, outputFile)
    92  	if err != nil {
    93  		return fmt.Errorf("fail to check if checkpoint file already exist: %w", err)
    94  	}
    95  
    96  	// found checkpoint file with the same checkpoint number
    97  	if len(matched) != 0 {
    98  		return fmt.Errorf("checkpoint part file already exists: %v", matched)
    99  	}
   100  
   101  	subtrieRoots := createSubTrieRoots(tries)
   102  
   103  	subTrieRootIndices, subTriesNodeCount, subTrieChecksums, err := storeSubTrieConcurrently(
   104  		subtrieRoots,
   105  		estimateSubtrieNodeCount(last), // considering the last trie most likely have more registers than others
   106  		subTrieRootAndTopLevelTrieCount(tries),
   107  		outputDir,
   108  		outputFile,
   109  		lg,
   110  		nWorker,
   111  	)
   112  	if err != nil {
   113  		return fmt.Errorf("could not store sub trie: %w", err)
   114  	}
   115  
   116  	lg.Info().Msgf("subtrie have been stored. sub trie node count: %v", subTriesNodeCount)
   117  
   118  	topTrieChecksum, err := storeTopLevelNodesAndTrieRoots(
   119  		tries, subTrieRootIndices, subTriesNodeCount, outputDir, outputFile, lg)
   120  	if err != nil {
   121  		return fmt.Errorf("could not store top level tries: %w", err)
   122  	}
   123  
   124  	err = storeCheckpointHeader(subTrieChecksums, topTrieChecksum, outputDir, outputFile, lg)
   125  	if err != nil {
   126  		return fmt.Errorf("could not store checkpoint header: %w", err)
   127  	}
   128  
   129  	lg.Info().Uint32("topsum", topTrieChecksum).Msg("checkpoint file has been successfully stored")
   130  
   131  	return nil
   132  }
   133  
   134  // 1. version
   135  // 2. checksum of each part file (17 in total)
   136  // 3. checksum of the main file itself
   137  func storeCheckpointHeader(
   138  	subTrieChecksums []uint32,
   139  	topTrieChecksum uint32,
   140  	outputDir string,
   141  	outputFile string,
   142  	logger zerolog.Logger,
   143  ) (
   144  	errToReturn error,
   145  ) {
   146  	// sanity check
   147  	if len(subTrieChecksums) != subtrieCountByLevel(subtrieLevel) {
   148  		return fmt.Errorf("expect subtrie level %v to have %v checksums, but got %v",
   149  			subtrieLevel, subtrieCountByLevel(subtrieLevel), len(subTrieChecksums))
   150  	}
   151  
   152  	closable, err := createWriterForCheckpointHeader(outputDir, outputFile, logger)
   153  	if err != nil {
   154  		return fmt.Errorf("could not store checkpoint header: %w", err)
   155  	}
   156  	defer func() {
   157  		errToReturn = closeAndMergeError(closable, errToReturn)
   158  	}()
   159  
   160  	writer := NewCRC32Writer(closable)
   161  
   162  	// write version
   163  	_, err = writer.Write(encodeVersion(MagicBytesCheckpointHeader, VersionV6))
   164  	if err != nil {
   165  		return fmt.Errorf("cannot write version into checkpoint header: %w", err)
   166  	}
   167  
   168  	// encode subtrieCount
   169  	_, err = writer.Write(encodeSubtrieCount(subtrieCount))
   170  	if err != nil {
   171  		return fmt.Errorf("cannot write subtrie level into checkpoint header: %w", err)
   172  	}
   173  
   174  	//  write subtrie checksums
   175  	for i, subtrieSum := range subTrieChecksums {
   176  		_, err = writer.Write(encodeCRC32Sum(subtrieSum))
   177  		if err != nil {
   178  			return fmt.Errorf("cannot write %v-th subtriechecksum into checkpoint header: %w", i, err)
   179  		}
   180  	}
   181  
   182  	// write top level trie checksum
   183  	_, err = writer.Write(encodeCRC32Sum(topTrieChecksum))
   184  	if err != nil {
   185  		return fmt.Errorf("cannot write top level trie checksum into checkpoint header: %w", err)
   186  	}
   187  
   188  	// write checksum to the end of the file
   189  	checksum := writer.Crc32()
   190  	_, err = writer.Write(encodeCRC32Sum(checksum))
   191  	if err != nil {
   192  		return fmt.Errorf("cannot write CRC32 checksum to checkpoint header: %w", err)
   193  	}
   194  	return nil
   195  }
   196  
   197  var createWriterForCheckpointHeader = createClosableWriter
   198  
   199  // 17th part file contains:
   200  // 1. checkpoint version
   201  // 2. subtrieNodeCount
   202  // 3. top level nodes
   203  // 4. trie roots
   204  // 5. node count
   205  // 6. trie count
   206  // 7. checksum
   207  func storeTopLevelNodesAndTrieRoots(
   208  	tries []*trie.MTrie,
   209  	subTrieRootIndices map[*node.Node]uint64,
   210  	subTriesNodeCount uint64,
   211  	outputDir string,
   212  	outputFile string,
   213  	logger zerolog.Logger,
   214  ) (
   215  	checksumOfTopTriePartFile uint32,
   216  	errToReturn error,
   217  ) {
   218  	// the remaining nodes and data will be stored into the same file
   219  	closable, err := createWriterForTopTries(outputDir, outputFile, logger)
   220  	if err != nil {
   221  		return 0, fmt.Errorf("could not create writer for top tries: %w", err)
   222  	}
   223  	defer func() {
   224  		errToReturn = closeAndMergeError(closable, errToReturn)
   225  	}()
   226  
   227  	writer := NewCRC32Writer(closable)
   228  
   229  	// write version
   230  	_, err = writer.Write(encodeVersion(MagicBytesCheckpointToptrie, VersionV6))
   231  	if err != nil {
   232  		return 0, fmt.Errorf("cannot write version into checkpoint header: %w", err)
   233  	}
   234  
   235  	// write subTriesNodeCount
   236  	_, err = writer.Write(encodeNodeCount(subTriesNodeCount))
   237  	if err != nil {
   238  		return 0, fmt.Errorf("could not write subtrie node count: %w", err)
   239  	}
   240  
   241  	scratch := make([]byte, 1024*4)
   242  
   243  	// write top level nodes
   244  	topLevelNodeIndices, topLevelNodesCount, err := storeTopLevelNodes(
   245  		scratch,
   246  		tries,
   247  		subTrieRootIndices,
   248  		subTriesNodeCount+1, // the counter is 1 more than the node count, because the first item is nil
   249  		writer)
   250  
   251  	if err != nil {
   252  		return 0, fmt.Errorf("could not store top level nodes: %w", err)
   253  	}
   254  
   255  	logger.Info().Msgf("top level nodes have been stored. top level node count: %v", topLevelNodesCount)
   256  
   257  	// write tries
   258  	err = storeTries(scratch, tries, topLevelNodeIndices, writer)
   259  	if err != nil {
   260  		return 0, fmt.Errorf("could not store trie root nodes: %w", err)
   261  	}
   262  
   263  	// write checksum
   264  	checksum, err := storeTopLevelTrieFooter(topLevelNodesCount, uint16(len(tries)), writer)
   265  	if err != nil {
   266  		return 0, fmt.Errorf("could not store footer: %w", err)
   267  	}
   268  
   269  	return checksum, nil
   270  }
   271  
   272  func createSubTrieRoots(tries []*trie.MTrie) [subtrieCount][]*node.Node {
   273  	var subtrieRoots [subtrieCount][]*node.Node
   274  	for i := 0; i < len(subtrieRoots); i++ {
   275  		subtrieRoots[i] = make([]*node.Node, len(tries))
   276  	}
   277  
   278  	for trieIndex, t := range tries {
   279  		// subtries is an array with subtrieCount trie nodes
   280  		// in breadth-first order at subtrieLevel of the trie `t`
   281  		subtries := getNodesAtLevel(t.RootNode(), subtrieLevel)
   282  		for subtrieIndex, subtrieRoot := range subtries {
   283  			subtrieRoots[subtrieIndex][trieIndex] = subtrieRoot
   284  		}
   285  	}
   286  	return subtrieRoots
   287  }
   288  
   289  // estimateSubtrieNodeCount estimate the average number of registers in each subtrie.
   290  func estimateSubtrieNodeCount(trie *trie.MTrie) int {
   291  	estimatedTrieNodeCount := 2*int(trie.AllocatedRegCount()) - 1
   292  	return estimatedTrieNodeCount / subtrieCount
   293  }
   294  
   295  // subTrieRootAndTopLevelTrieCount return the total number of subtrie root nodes
   296  // and top level trie nodes for given number of tries
   297  // it is used for preallocating memory for the map that holds all unique nodes in
   298  // all sub trie roots and top level trie nodoes.
   299  // the top level trie nodes has nearly same number of nodes as subtrie node count at subtrieLevel
   300  // that's it needs to * 2.
   301  func subTrieRootAndTopLevelTrieCount(tries []*trie.MTrie) int {
   302  	return len(tries) * subtrieCount * 2
   303  }
   304  
   305  type resultStoringSubTrie struct {
   306  	Index     int
   307  	Roots     map[*node.Node]uint64 // node index for root nodes
   308  	NodeCount uint64
   309  	Checksum  uint32
   310  	Err       error
   311  }
   312  
   313  type jobStoreSubTrie struct {
   314  	Index  int
   315  	Roots  []*node.Node
   316  	Result chan<- *resultStoringSubTrie
   317  }
   318  
   319  func storeSubTrieConcurrently(
   320  	subtrieRoots [subtrieCount][]*node.Node,
   321  	estimatedSubtrieNodeCount int, // useful for preallocating memory for building unique node map when processing sub tries
   322  	subAndTopNodeCount int, // useful for preallocating memory for the node indices map to be returned
   323  	outputDir string,
   324  	outputFile string,
   325  	logger zerolog.Logger,
   326  	nWorker uint,
   327  ) (
   328  	map[*node.Node]uint64, // node indices
   329  	uint64, // node count
   330  	[]uint32, //checksums
   331  	error, // any exception
   332  ) {
   333  	logger.Info().Msgf("storing %v subtrie groups with average node count %v for each subtrie", subtrieCount, estimatedSubtrieNodeCount)
   334  
   335  	if nWorker == 0 || nWorker > subtrieCount {
   336  		return nil, 0, nil, fmt.Errorf("invalid nWorker %v, the valid range is [1,%v]", nWorker, subtrieCount)
   337  	}
   338  
   339  	jobs := make(chan jobStoreSubTrie, len(subtrieRoots))
   340  	resultChs := make([]<-chan *resultStoringSubTrie, len(subtrieRoots))
   341  
   342  	// push all jobs into the channel
   343  	for i, roots := range subtrieRoots {
   344  		resultCh := make(chan *resultStoringSubTrie)
   345  		resultChs[i] = resultCh
   346  		jobs <- jobStoreSubTrie{
   347  			Index:  i,
   348  			Roots:  roots,
   349  			Result: resultCh,
   350  		}
   351  	}
   352  	close(jobs)
   353  
   354  	// start nWorker number of goroutine to take the job from the jobs channel concurrently
   355  	// and work on them, after finish, continue until the jobs channel is drained
   356  	for i := 0; i < int(nWorker); i++ {
   357  		go func() {
   358  			for job := range jobs {
   359  				roots, nodeCount, checksum, err := storeCheckpointSubTrie(
   360  					job.Index, job.Roots, estimatedSubtrieNodeCount, outputDir, outputFile, logger)
   361  
   362  				job.Result <- &resultStoringSubTrie{
   363  					Index:     job.Index,
   364  					Roots:     roots,
   365  					NodeCount: nodeCount,
   366  					Checksum:  checksum,
   367  					Err:       err,
   368  				}
   369  				close(job.Result)
   370  			}
   371  		}()
   372  	}
   373  
   374  	results := make(map[*node.Node]uint64, subAndTopNodeCount)
   375  	results[nil] = 0
   376  	nodeCounter := uint64(0)
   377  	checksums := make([]uint32, 0, len(subtrieRoots))
   378  
   379  	// reading job results in the same order as their indices
   380  	for _, resultCh := range resultChs {
   381  		result := <-resultCh
   382  
   383  		if result.Err != nil {
   384  			return nil, 0, nil, fmt.Errorf("fail to store %v-th subtrie, trie: %w", result.Index, result.Err)
   385  		}
   386  
   387  		for root, index := range result.Roots {
   388  			// nil is always 0
   389  			if root == nil {
   390  				results[root] = 0
   391  			} else {
   392  				// the original index is relative to the subtrie file itself.
   393  				// but we need a global index to be referenced by top level trie,
   394  				// therefore we need to add the nodeCounter
   395  				results[root] = index + nodeCounter
   396  			}
   397  		}
   398  		nodeCounter += result.NodeCount
   399  		checksums = append(checksums, result.Checksum)
   400  	}
   401  
   402  	return results, nodeCounter, checksums, nil
   403  }
   404  
   405  func createWriterForTopTries(dir string, file string, logger zerolog.Logger) (io.WriteCloser, error) {
   406  	_, topTriesFileName := filePathTopTries(dir, file)
   407  
   408  	return createClosableWriter(dir, topTriesFileName, logger)
   409  }
   410  
   411  func createWriterForSubtrie(dir string, file string, logger zerolog.Logger, index int) (io.WriteCloser, error) {
   412  	_, subTriesFileName, err := filePathSubTries(dir, file, index)
   413  	if err != nil {
   414  		return nil, err
   415  	}
   416  
   417  	return createClosableWriter(dir, subTriesFileName, logger)
   418  }
   419  
   420  func createClosableWriter(dir string, fileName string, logger zerolog.Logger) (io.WriteCloser, error) {
   421  	fullPath := path.Join(dir, fileName)
   422  	if utilsio.FileExists(fullPath) {
   423  		return nil, fmt.Errorf("checkpoint part file %v already exists", fullPath)
   424  	}
   425  
   426  	tmpFile, err := os.CreateTemp(dir, fmt.Sprintf("writing-%v-*", fileName))
   427  	if err != nil {
   428  		return nil, fmt.Errorf("could not create temporary file for checkpoint toptries: %w", err)
   429  	}
   430  
   431  	writer := bufio.NewWriterSize(tmpFile, defaultBufioWriteSize)
   432  	return &SyncOnCloseRenameFile{
   433  		logger:     logger,
   434  		file:       tmpFile,
   435  		targetName: fullPath,
   436  		Writer:     writer,
   437  	}, nil
   438  }
   439  
   440  // storeCheckpointSubTrie traverse each root node, and store the subtrie nodes into
   441  // the subtrie part file at index i
   442  // subtrie file contains:
   443  // 1. checkpoint version
   444  // 2. nodes
   445  // 3. node count
   446  // 4. checksum
   447  func storeCheckpointSubTrie(
   448  	i int,
   449  	roots []*node.Node,
   450  	estimatedSubtrieNodeCount int, // for estimate the amount of memory to be preallocated
   451  	outputDir string,
   452  	outputFile string,
   453  	logger zerolog.Logger,
   454  ) (
   455  	rootNodesOfAllSubtries map[*node.Node]uint64, // the stored position of each unique root node
   456  	totalSubtrieNodeCount uint64,
   457  	checksumOfSubtriePartfile uint32,
   458  	errToReturn error,
   459  ) {
   460  
   461  	closable, err := createWriterForSubtrie(outputDir, outputFile, logger, i)
   462  	if err != nil {
   463  		return nil, 0, 0, fmt.Errorf("could not create writer for sub trie: %w", err)
   464  	}
   465  
   466  	defer func() {
   467  		errToReturn = closeAndMergeError(closable, errToReturn)
   468  	}()
   469  
   470  	// create a CRC32 writer, so that any bytes passed to the writer will
   471  	// be used to calculate CRC32 checksum
   472  	writer := NewCRC32Writer(closable)
   473  
   474  	// write version
   475  	_, err = writer.Write(encodeVersion(MagicBytesCheckpointSubtrie, VersionV6))
   476  	if err != nil {
   477  		return nil, 0, 0, fmt.Errorf("cannot write version into checkpoint subtrie file: %w", err)
   478  	}
   479  
   480  	// subtrieRootNodes unique subtrie root nodes, the uint64 value is the index of each root node
   481  	// stored in the part file.
   482  	subtrieRootNodes := make(map[*node.Node]uint64, len(roots))
   483  
   484  	// nodeCounter is counter for all unique nodes.
   485  	// It starts from 1, as 0 marks nil node.
   486  	nodeCounter := uint64(1)
   487  
   488  	logging := logProgress(fmt.Sprintf("storing %v-th sub trie roots", i), estimatedSubtrieNodeCount, logger)
   489  
   490  	// traversedSubtrieNodes contains all unique nodes of subtries of the same path and their index.
   491  	traversedSubtrieNodes := make(map[*node.Node]uint64, estimatedSubtrieNodeCount)
   492  	// index 0 is nil, it can be used in a node's left child or right child to indicate
   493  	// a node's left child or right child is nil
   494  	traversedSubtrieNodes[nil] = 0
   495  
   496  	scratch := make([]byte, 1024*4)
   497  	for _, root := range roots {
   498  		// Note: nodeCounter is to assign an global index to each node in the order of it being seralized
   499  		// into the checkpoint file. Therefore, it has to be reused when iterating each subtrie.
   500  		// storeUniqueNodes will add the unique visited node into traversedSubtrieNodes with key as the node
   501  		// itself, and value as n-th node being seralized in the checkpoint file.
   502  		nodeCounter, err = storeUniqueNodes(root, traversedSubtrieNodes, nodeCounter, scratch, writer, logging)
   503  		if err != nil {
   504  			return nil, 0, 0, fmt.Errorf("fail to store nodes in step 1 for subtrie root %v: %w", root.Hash(), err)
   505  		}
   506  		// Save subtrie root node index in topLevelNodes,
   507  		// so when traversing top level tries
   508  		// (from level 0 to subtrieLevel) using topLevelNodes,
   509  		// node iterator skips subtrie as visited nodes.
   510  		subtrieRootNodes[root] = traversedSubtrieNodes[root]
   511  	}
   512  
   513  	// -1 to account for 0 node meaning nil
   514  	totalNodeCount := nodeCounter - 1
   515  
   516  	// write total number of node as footer
   517  	checksum, err := storeSubtrieFooter(totalNodeCount, writer)
   518  	if err != nil {
   519  		return nil, 0, 0, fmt.Errorf("could not store subtrie footer %w", err)
   520  	}
   521  
   522  	return subtrieRootNodes, totalNodeCount, checksum, nil
   523  }
   524  
   525  func storeTopLevelNodes(
   526  	scratch []byte,
   527  	tries []*trie.MTrie,
   528  	subTrieRootIndices map[*node.Node]uint64,
   529  	initNodeCounter uint64,
   530  	writer io.Writer) (
   531  	map[*node.Node]uint64,
   532  	uint64,
   533  	error) {
   534  	nodeCounter := initNodeCounter
   535  	var err error
   536  	for _, t := range tries {
   537  		root := t.RootNode()
   538  		if root == nil {
   539  			continue
   540  		}
   541  		// if we iterate through the root trie with an empty visited nodes map, then it will iterate through
   542  		// all nodes at all levels. In order to skip the nodes above subtrieLevel, since they have been seralized in step 1,
   543  		// we will need to pass in a visited nodes map that contains all the subtrie root nodes, which is the topLevelNodes.
   544  		// The topLevelNodes was built in step 1, when seralizing each subtrie root nodes.
   545  		nodeCounter, err = storeUniqueNodes(root, subTrieRootIndices, nodeCounter, scratch, writer, func(uint64) {})
   546  		if err != nil {
   547  			return nil, 0, fmt.Errorf("fail to store nodes in step 2 for root trie %v: %w", root.Hash(), err)
   548  		}
   549  	}
   550  
   551  	topLevelNodesCount := nodeCounter - initNodeCounter
   552  	return subTrieRootIndices, topLevelNodesCount, nil
   553  }
   554  
   555  func storeTries(
   556  	scratch []byte,
   557  	tries []*trie.MTrie,
   558  	topLevelNodes map[*node.Node]uint64,
   559  	writer io.Writer) error {
   560  	for _, t := range tries {
   561  		rootNode := t.RootNode()
   562  		if !t.IsEmpty() && rootNode.Height() != ledger.NodeMaxHeight {
   563  			return fmt.Errorf("height of root node must be %d, but is %d",
   564  				ledger.NodeMaxHeight, rootNode.Height())
   565  		}
   566  
   567  		// Get root node index
   568  		rootIndex, found := topLevelNodes[rootNode]
   569  		if !found {
   570  			rootHash := t.RootHash()
   571  			return fmt.Errorf("internal error: missing node with hash %s", hex.EncodeToString(rootHash[:]))
   572  		}
   573  
   574  		encTrie := flattener.EncodeTrie(t, rootIndex, scratch)
   575  		_, err := writer.Write(encTrie)
   576  		if err != nil {
   577  			return fmt.Errorf("cannot serialize trie: %w", err)
   578  		}
   579  	}
   580  
   581  	return nil
   582  }
   583  
   584  // deleteCheckpointFiles removes any checkpoint files with given checkpoint prefix in the outputDir.
   585  func deleteCheckpointFiles(outputDir string, outputFile string) error {
   586  	pattern := filePathPattern(outputDir, outputFile)
   587  	filesToRemove, err := filepath.Glob(pattern)
   588  	if err != nil {
   589  		return fmt.Errorf("could not glob checkpoint files to delete with pattern %v: %w",
   590  			pattern, err,
   591  		)
   592  	}
   593  
   594  	var merror *multierror.Error
   595  	for _, file := range filesToRemove {
   596  		err := os.Remove(file)
   597  		if err != nil {
   598  			merror = multierror.Append(merror, err)
   599  		}
   600  	}
   601  
   602  	return merror.ErrorOrNil()
   603  }
   604  
   605  func storeTopLevelTrieFooter(topLevelNodesCount uint64, rootTrieCount uint16, writer *Crc32Writer) (uint32, error) {
   606  	footer := encodeTopLevelNodesAndTriesFooter(topLevelNodesCount, rootTrieCount)
   607  	_, err := writer.Write(footer)
   608  	if err != nil {
   609  		return 0, fmt.Errorf("cannot write checkpoint footer: %w", err)
   610  	}
   611  
   612  	// write checksum to the end of the file
   613  	checksum := writer.Crc32()
   614  	_, err = writer.Write(encodeCRC32Sum(checksum))
   615  	if err != nil {
   616  		return 0, fmt.Errorf("cannot write CRC32 checksum to top level part file: %w", err)
   617  	}
   618  
   619  	return checksum, nil
   620  }
   621  
   622  func storeSubtrieFooter(nodeCount uint64, writer *Crc32Writer) (uint32, error) {
   623  	footer := encodeNodeCount(nodeCount)
   624  	_, err := writer.Write(footer)
   625  	if err != nil {
   626  		return 0, fmt.Errorf("cannot write checkpoint subtrie footer: %w", err)
   627  	}
   628  
   629  	// write checksum to the end of the file
   630  	crc32Sum := writer.Crc32()
   631  	_, err = writer.Write(encodeCRC32Sum(crc32Sum))
   632  	if err != nil {
   633  		return 0, fmt.Errorf("cannot write CRC32 checksum %v", err)
   634  	}
   635  	return crc32Sum, nil
   636  }
   637  
   638  func encodeTopLevelNodesAndTriesFooter(topLevelNodesCount uint64, rootTrieCount uint16) []byte {
   639  	footer := make([]byte, encNodeCountSize+encTrieCountSize)
   640  	binary.BigEndian.PutUint64(footer, topLevelNodesCount)
   641  	binary.BigEndian.PutUint16(footer[encNodeCountSize:], rootTrieCount)
   642  	return footer
   643  }
   644  
   645  func decodeTopLevelNodesAndTriesFooter(footer []byte) (uint64, uint16, error) {
   646  	const footerSize = encNodeCountSize + encTrieCountSize // footer doesn't include crc32 sum
   647  	if len(footer) != footerSize {
   648  		return 0, 0, fmt.Errorf("wrong footer size, expect %v, got %v", footerSize, len(footer))
   649  	}
   650  	nodesCount := binary.BigEndian.Uint64(footer)
   651  	triesCount := binary.BigEndian.Uint16(footer[encNodeCountSize:])
   652  	return nodesCount, triesCount, nil
   653  }
   654  
   655  func encodeNodeCount(nodeCount uint64) []byte {
   656  	buf := make([]byte, encNodeCountSize)
   657  	binary.BigEndian.PutUint64(buf, nodeCount)
   658  	return buf
   659  }
   660  
   661  func decodeNodeCount(encoded []byte) (uint64, error) {
   662  	if len(encoded) != encNodeCountSize {
   663  		return 0, fmt.Errorf("wrong subtrie node count size, expect %v, got %v", encNodeCountSize, len(encoded))
   664  	}
   665  	return binary.BigEndian.Uint64(encoded), nil
   666  }
   667  
   668  func encodeCRC32Sum(checksum uint32) []byte {
   669  	buf := make([]byte, crc32SumSize)
   670  	binary.BigEndian.PutUint32(buf, checksum)
   671  	return buf
   672  }
   673  
   674  func decodeCRC32Sum(encoded []byte) (uint32, error) {
   675  	if len(encoded) != crc32SumSize {
   676  		return 0, fmt.Errorf("wrong crc32sum size, expect %v, got %v", crc32SumSize, len(encoded))
   677  	}
   678  	return binary.BigEndian.Uint32(encoded), nil
   679  }
   680  
   681  func encodeVersion(magic uint16, version uint16) []byte {
   682  	// Write header: magic (2 bytes) + version (2 bytes)
   683  	header := make([]byte, encMagicSize+encVersionSize)
   684  	binary.BigEndian.PutUint16(header, magic)
   685  	binary.BigEndian.PutUint16(header[encMagicSize:], version)
   686  	return header
   687  }
   688  
   689  func decodeVersion(encoded []byte) (uint16, uint16, error) {
   690  	if len(encoded) != encMagicSize+encVersionSize {
   691  		return 0, 0, fmt.Errorf("wrong version size, expect %v, got %v", encMagicSize+encVersionSize, len(encoded))
   692  	}
   693  	magicBytes := binary.BigEndian.Uint16(encoded)
   694  	version := binary.BigEndian.Uint16(encoded[encMagicSize:])
   695  	return magicBytes, version, nil
   696  }
   697  
   698  func encodeSubtrieCount(level uint16) []byte {
   699  	bytes := make([]byte, encSubtrieCountSize)
   700  	binary.BigEndian.PutUint16(bytes, level)
   701  	return bytes
   702  }
   703  
   704  func decodeSubtrieCount(encoded []byte) (uint16, error) {
   705  	if len(encoded) != encSubtrieCountSize {
   706  		return 0, fmt.Errorf("wrong subtrie level size, expect %v, got %v", encSubtrieCountSize, len(encoded))
   707  	}
   708  	return binary.BigEndian.Uint16(encoded), nil
   709  }
   710  
   711  // closeAndMergeError close the closable and merge the closeErr with the given err into a multierror
   712  // Note: when using this function in a defer function, don't use as below:
   713  // func XXX() (
   714  //
   715  //	err error,
   716  //	) {
   717  //		def func() {
   718  //			// bad, because the definition of err might get overwritten
   719  //			err = closeAndMergeError(closable, err)
   720  //		}()
   721  //
   722  // Better to use as below:
   723  // func XXX() (
   724  //
   725  //	errToReturn error,
   726  //	) {
   727  //		def func() {
   728  //			// good, because the error to returned is only updated here, and guaranteed to be returned
   729  //			errToReturn = closeAndMergeError(closable, errToReturn)
   730  //		}()
   731  func closeAndMergeError(closable io.Closer, err error) error {
   732  	var merr *multierror.Error
   733  	if err != nil {
   734  		merr = multierror.Append(merr, err)
   735  	}
   736  
   737  	closeError := closable.Close()
   738  	if closeError != nil {
   739  		merr = multierror.Append(merr, closeError)
   740  	}
   741  
   742  	return merr.ErrorOrNil()
   743  }
   744  
   745  // withFile opens the file at the given path, and calls the given function with the opened file.
   746  // it handles closing the file and evicting the file from Linux page cache.
   747  func withFile(logger zerolog.Logger, filepath string, f func(file *os.File) error) (
   748  	errToReturn error,
   749  ) {
   750  
   751  	file, err := os.Open(filepath)
   752  	if err != nil {
   753  		return fmt.Errorf("could not open file %v: %w", filepath, err)
   754  	}
   755  	defer func(file *os.File) {
   756  		evictErr := evictFileFromLinuxPageCache(file, false, logger)
   757  		if evictErr != nil {
   758  			logger.Warn().Msgf("failed to evict top trie file %s from Linux page cache: %s", filepath, evictErr)
   759  			// No need to return this error because it's possible to continue normal operations.
   760  		}
   761  		errToReturn = closeAndMergeError(file, errToReturn)
   762  	}(file)
   763  
   764  	return f(file)
   765  }