diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index d9a31d98a3..9ac5e7572a 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -363,7 +363,7 @@ func initGenesis(ctx *cli.Context) error { log.Warn("Multi-database is an experimental feature") } - triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, ctx.Bool(utils.CachePreimagesFlag.Name), false, genesis.IsVerkle()) + triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, ctx.Bool(utils.CachePreimagesFlag.Name), false, genesis.IsVerkle(), false) defer triedb.Close() _, hash, compatErr, err := core.SetupGenesisBlockWithOverride(chaindb, triedb, genesis, &overrides) @@ -1091,7 +1091,7 @@ func dump(ctx *cli.Context) error { return err } defer db.Close() - triedb := utils.MakeTrieDatabase(ctx, stack, db, true, true, false) // always enable preimage lookup + triedb := utils.MakeTrieDatabase(ctx, stack, db, true, true, false, false) // always enable preimage lookup defer triedb.Close() state, err := state.New(root, state.NewDatabase(triedb, nil)) diff --git a/cmd/geth/dbcmd.go b/cmd/geth/dbcmd.go index 2ebc1c21b1..0b53233263 100644 --- a/cmd/geth/dbcmd.go +++ b/cmd/geth/dbcmd.go @@ -93,6 +93,7 @@ Remove blockchain and state databases`, dbTrieDeleteCmd, dbDeleteTrieStateCmd, ancientInspectCmd, + incrInspectCmd, }, } dbInspectCmd = &cli.Command{ @@ -275,6 +276,13 @@ of ancientStore, will also displays the reserved number of blocks in ancientStor }, utils.NetworkFlags, utils.DatabaseFlags), Description: "This command queries the history of the account or storage slot within the specified block range", } + incrInspectCmd = &cli.Command{ + Action: inspectIncrSnapshot, + Name: "inspect-incr-snapshot", + Flags: []cli.Flag{utils.IncrSnapshotPathFlag}, + Usage: "Inspect the incremental snapshot information", + Description: `This command reads and displays incremental store information`, + } ) func removeDB(ctx *cli.Context) error { @@ -945,7 +953,7 @@ func dbDumpTrie(ctx *cli.Context) error { db := utils.MakeChainDatabase(ctx, stack, true) defer db.Close() - triedb := utils.MakeTrieDatabase(ctx, stack, db, false, true, false) + triedb := utils.MakeTrieDatabase(ctx, stack, db, false, true, false, false) defer triedb.Close() var ( @@ -1279,7 +1287,7 @@ func inspectHistory(ctx *cli.Context) error { db := utils.MakeChainDatabase(ctx, stack, true) defer db.Close() - triedb := utils.MakeTrieDatabase(ctx, stack, db, false, false, false) + triedb := utils.MakeTrieDatabase(ctx, stack, db, false, false, false, false) defer triedb.Close() var ( @@ -1327,3 +1335,14 @@ func inspectHistory(ctx *cli.Context) error { } return inspectStorage(triedb, start, end, address, slot, ctx.Bool("raw")) } + +func inspectIncrSnapshot(ctx *cli.Context) error { + if !ctx.IsSet(utils.IncrSnapshotPathFlag.Name) { + return errors.New("increment snapshot path is not set") + } + baseDir := ctx.String(utils.IncrSnapshotPathFlag.Name) + if err := rawdb.InspectIncrStore(baseDir); err != nil { + return err + } + return nil +} diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 021322fd90..e5fd043c1d 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -183,6 +183,13 @@ var ( utils.LogDebugFlag, utils.LogBacktraceAtFlag, utils.BlobExtraReserveFlag, + utils.EnableIncrSnapshotFlag, + utils.IncrSnapshotPathFlag, + utils.IncrSnapshotBlockIntervalFlag, + utils.IncrSnapshotStateBufferFlag, + utils.IncrSnapshotKeptBlocksFlag, + utils.UseRemoteIncrSnapshotFlag, + utils.RemoteIncrSnapshotURLFlag, // utils.BeaconApiFlag, // utils.BeaconApiHeaderFlag, // utils.BeaconThresholdFlag, diff --git a/cmd/geth/snapshot.go b/cmd/geth/snapshot.go index e8d1440731..d41b529e44 100644 --- a/cmd/geth/snapshot.go +++ b/cmd/geth/snapshot.go @@ -27,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state/pruner" @@ -163,6 +164,15 @@ The export-preimages command exports hash preimages to a flat file, in exactly the expected order for the overlay tree migration. `, }, + { + Action: mergeIncrSnapshot, + Name: "merge-incr-snapshot", + Usage: "Merge the incremental snapshot into local data", + ArgsUsage: "", + Flags: slices.Concat([]cli.Flag{utils.IncrSnapshotPathFlag}, + utils.DatabaseFlags), + Description: `This command merges multiple incremental snapshots into local data`, + }, }, } ) @@ -220,7 +230,7 @@ func verifyState(ctx *cli.Context) error { log.Error("Failed to load head block") return errors.New("no head block") } - triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, false, true, false) + triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, false, true, false, false) defer triedb.Close() var ( @@ -285,7 +295,7 @@ func traverseState(ctx *cli.Context) error { chaindb := utils.MakeChainDatabase(ctx, stack, true) defer chaindb.Close() - triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, false, true, false) + triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, false, true, false, false) defer triedb.Close() headBlock := rawdb.ReadHeadBlock(chaindb) @@ -394,7 +404,7 @@ func traverseRawState(ctx *cli.Context) error { chaindb := utils.MakeChainDatabase(ctx, stack, true) defer chaindb.Close() - triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, false, true, false) + triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, false, true, false, false) defer triedb.Close() headBlock := rawdb.ReadHeadBlock(chaindb) @@ -562,7 +572,7 @@ func dumpState(ctx *cli.Context) error { return err } defer db.Close() - triedb := utils.MakeTrieDatabase(ctx, stack, db, false, true, false) + triedb := utils.MakeTrieDatabase(ctx, stack, db, false, true, false, false) defer triedb.Close() snapConfig := snapshot.Config{ @@ -645,7 +655,7 @@ func snapshotExportPreimages(ctx *cli.Context) error { chaindb := utils.MakeChainDatabase(ctx, stack, true) defer chaindb.Close() - triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, false, true, false) + triedb := utils.MakeTrieDatabase(ctx, stack, chaindb, false, true, false, false) defer triedb.Close() var root common.Hash @@ -707,3 +717,70 @@ func checkAccount(ctx *cli.Context) error { log.Info("Checked the snapshot journalled storage", "time", common.PrettyDuration(time.Since(start))) return nil } + +// mergeIncrSnapshot merges the incremental snapshot into local data. +func mergeIncrSnapshot(ctx *cli.Context) error { + stack, _ := makeConfigNode(ctx) + defer stack.Close() + + chainDB := utils.MakeChainDatabase(ctx, stack, false) + defer chainDB.Close() + + trieDB := utils.MakeTrieDatabase(ctx, stack, chainDB, false, false, false, true) + defer trieDB.Close() + + if !ctx.IsSet(utils.IncrSnapshotPathFlag.Name) { + return errors.New("incremental snapshot path is not set") + } + path := ctx.String(utils.IncrSnapshotPathFlag.Name) + + startBlock, err := trieDB.GetStartBlock() + if err != nil { + log.Error("Failed to get start block", "error", err) + return err + } + dirs, err := rawdb.GetAllIncrDirs(path) + if err != nil { + log.Error("Failed to get all incremental directories", "err", err) + return err + } + if startBlock < dirs[0].StartBlockNum { + return fmt.Errorf("local start block %d is lower than incr first start block %d", startBlock, dirs[0].StartBlockNum) + } + + for i := 1; i < len(dirs); i++ { + prevFile := dirs[i-1] + currFile := dirs[i] + + expectedStartBlock := prevFile.EndBlockNum + 1 + if currFile.StartBlockNum != expectedStartBlock { + return fmt.Errorf("file continuity broken: file %s ends at %d, but file %s starts at %d (expected %d)", + prevFile.Name, prevFile.EndBlockNum, currFile.Name, currFile.StartBlockNum, expectedStartBlock) + } + } + + log.Info("Start merging incremental snapshot", "path", path, "incremental snapshot number", len(dirs)) + for i, dir := range dirs { + if i == len(dirs)-1 { + complete, err := rawdb.CheckIncrSnapshotComplete(dir.Path) + if err != nil { + log.Error("Failed to check last incr snapshot complete", "err", err) + return err + } + if !complete { + log.Warn("Skip last incr snapshot due to data is incomplete") + continue + } + } + + if dir.StartBlockNum >= startBlock && dir.EndBlockNum > startBlock { + if err = core.MergeIncrSnapshot(chainDB, trieDB, dir.Path); err != nil { + log.Error("Failed to merge incremental snapshot", "err", err) + return err + } + } else { + log.Info("Skip merge incremental snapshot", "dir", dir.Name) + } + } + return nil +} diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 3727f4fdc3..c9841b4559 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1274,6 +1274,50 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server. Value: fakebeacon.DefaultPort, Category: flags.APICategory, } + + // incremental snapshot related flags + EnableIncrSnapshotFlag = &cli.BoolFlag{ + Name: "incr.enable", + Usage: "Enable incremental snapshot generation", + Value: false, + Category: flags.StateCategory, + } + IncrSnapshotPathFlag = &flags.DirectoryFlag{ + Name: "incr.datadir", + Usage: "Data directory for storing incremental snapshot data: can be used to store generated or downloaded incremental snapshot", + Value: "", + Category: flags.StateCategory, + } + IncrSnapshotBlockIntervalFlag = &cli.Uint64Flag{ + Name: "incr.block-interval", + Usage: "Set how many blocks interval are stored into one incremental snapshot", + Value: pathdb.DefaultBlockInterval, + Category: flags.StateCategory, + } + IncrSnapshotStateBufferFlag = &cli.Uint64Flag{ + Name: "incr.state-buffer", + Usage: "Set the incr state memory buffer to aggregate MPT trie nodes. The larger the setting, the smaller the incr snapshot size", + Value: pathdb.DefaultIncrStateBufferSize, + Category: flags.StateCategory, + } + IncrSnapshotKeptBlocksFlag = &cli.Uint64Flag{ + Name: "incr.kept-blocks", + Usage: "Set how many blocks are kept in incr snapshot. At least is 1024 blocks", + Value: pathdb.DefaultKeptBlocks, + Category: flags.StateCategory, + } + UseRemoteIncrSnapshotFlag = &cli.BoolFlag{ + Name: "incr.use-remote", + Usage: "Enable download and merge incremental snapshots into local data", + Value: false, + Category: flags.StateCategory, + } + RemoteIncrSnapshotURLFlag = &cli.StringFlag{ + Name: "incr.remote-url", + Usage: "Set from which remote url is used to download incremental snapshots", + Value: "", + Category: flags.StateCategory, + } ) var ( @@ -2318,6 +2362,38 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { cfg.VMTraceJsonConfig = ctx.String(VMTraceJsonConfigFlag.Name) } } + + // Download and merge incremental snapshot config + if ctx.IsSet(UseRemoteIncrSnapshotFlag.Name) { + cfg.UseRemoteIncrSnapshot = true + if !ctx.IsSet(RemoteIncrSnapshotURLFlag.Name) { + Fatalf("Must provide a remote increment snapshot URL") + } else { + cfg.RemoteIncrSnapshotURL = ctx.String(RemoteIncrSnapshotURLFlag.Name) + } + if ctx.IsSet(IncrSnapshotPathFlag.Name) { + cfg.IncrSnapshotPath = ctx.String(IncrSnapshotPathFlag.Name) + } else { + Fatalf("Must provide a path to store downloaded incr snapshot") + } + } + + // enable incremental snapshot generation config + if ctx.IsSet(EnableIncrSnapshotFlag.Name) { + cfg.EnableIncrSnapshots = true + if ctx.IsSet(IncrSnapshotPathFlag.Name) { + cfg.IncrSnapshotPath = ctx.String(IncrSnapshotPathFlag.Name) + } + if ctx.IsSet(IncrSnapshotBlockIntervalFlag.Name) { + cfg.IncrSnapshotBlockInterval = ctx.Uint64(IncrSnapshotBlockIntervalFlag.Name) + } + if ctx.IsSet(IncrSnapshotStateBufferFlag.Name) { + cfg.IncrSnapshotStateBuffer = ctx.Uint64(IncrSnapshotStateBufferFlag.Name) + } + if ctx.IsSet(IncrSnapshotKeptBlocksFlag.Name) { + cfg.IncrSnapshotKeptBlocks = ctx.Uint64(IncrSnapshotKeptBlocksFlag.Name) + } + } } // SetDNSDiscoveryDefaults configures DNS discovery with the given URL if @@ -2783,7 +2859,7 @@ func MakeConsolePreloads(ctx *cli.Context) []string { } // MakeTrieDatabase constructs a trie database based on the configured scheme. -func MakeTrieDatabase(ctx *cli.Context, stack *node.Node, disk ethdb.Database, preimage bool, readOnly bool, isVerkle bool) *triedb.Database { +func MakeTrieDatabase(ctx *cli.Context, stack *node.Node, disk ethdb.Database, preimage bool, readOnly bool, isVerkle bool, mergeIncr bool) *triedb.Database { config := &triedb.Config{ Preimages: preimage, IsVerkle: isVerkle, @@ -2803,6 +2879,9 @@ func MakeTrieDatabase(ctx *cli.Context, stack *node.Node, disk ethdb.Database, p config.PathDB = pathdb.ReadOnly } else { config.PathDB = pathdb.Defaults + if mergeIncr { + config.PathDB.MergeIncr = true + } } config.PathDB.JournalFilePath = fmt.Sprintf("%s/%s", stack.ResolvePath("chaindata"), eth.JournalFileName) return triedb.NewDatabase(disk, config) diff --git a/core/blockchain.go b/core/blockchain.go index 7c3e3bd49a..e062a5ddcb 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -178,11 +178,18 @@ const ( // BlockChainConfig contains the configuration of the BlockChain object. type BlockChainConfig struct { - TriesInMemory uint64 // How many tries keeps in memory - NoTries bool // Insecure settings. Do not have any tries in databases if enabled. - PathSyncFlush bool // Whether sync flush the trienodebuffer of pathdb to disk. - JournalFilePath string - JournalFile bool + TriesInMemory uint64 // How many tries keeps in memory + NoTries bool // Insecure settings. Do not have any tries in databases if enabled. + PathSyncFlush bool // Whether sync flush the trienodebuffer of pathdb to disk. + JournalFilePath string // The path to store journal file which is used in pathdb + JournalFile bool // Whether to use single file to store journal data in pathdb + EnableIncr bool // Flag whether the freezer db stores incremental block and state history + IncrHistoryPath string // The path to store incremental block and chain files + IncrHistory uint64 // Amount of block and state history stored in incremental freezer db + IncrStateBuffer uint64 // Maximum memory allowance (in bytes) for incr state buffer + IncrKeptBlocks uint64 // Amount of block kept in incr snapshot + UseRemoteIncrSnapshot bool // Whether to download and merge incremental snapshots + RemoteIncrURL string // The url to download incremental snapshots // Trie database related options TrieCleanLimit int // Memory allowance (MB) to use for caching trie nodes in memory @@ -272,6 +279,11 @@ func (cfg *BlockChainConfig) triedbConfig(isVerkle bool) *triedb.Config { config.PathDB = &pathdb.Config{ JournalFilePath: cfg.JournalFilePath, JournalFile: cfg.JournalFile, + EnableIncr: cfg.EnableIncr, + IncrHistoryPath: cfg.IncrHistoryPath, + IncrHistory: cfg.IncrHistory, + IncrStateBuffer: cfg.IncrStateBuffer, + IncrKeptBlocks: cfg.IncrKeptBlocks, StateHistory: cfg.StateHistory, EnableStateIndexing: cfg.ArchiveMode, @@ -415,7 +427,27 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, if err != nil { return nil, err } - triedb := triedb.NewDatabase(db, cfg.triedbConfig(enableVerkle)) + trieConfig := cfg.triedbConfig(enableVerkle) + if cfg.UseRemoteIncrSnapshot && cfg.StateScheme == rawdb.PathScheme { + trieConfig.PathDB.MergeIncr = true + } + triedb := triedb.NewDatabase(db, trieConfig) + + if cfg.UseRemoteIncrSnapshot { + log.Info("Download the incremental snapshot", "remote incr url", cfg.RemoteIncrURL) + startBlock, err := triedb.GetStartBlock() + if err != nil { + log.Error("Failed to get start block", "error", err) + return nil, err + } + downloader := NewIncrDownloader(db, triedb, cfg.RemoteIncrURL, cfg.IncrHistoryPath, startBlock) + if err = downloader.RunConcurrent(); err != nil { + log.Error("Failed to download and merge incremental snapshot", "error", err) + return nil, err + } + log.Info("Download and merge incr snapshots successfully") + triedb.SetStateGenerator() + } // Write the supplied genesis to the database if it has not been initialized // yet. The corresponding chain config will be returned, either from the diff --git a/core/incr_downloader.go b/core/incr_downloader.go new file mode 100644 index 0000000000..a55021c074 --- /dev/null +++ b/core/incr_downloader.go @@ -0,0 +1,1517 @@ +package core + +import ( + "archive/tar" + "context" + "crypto/md5" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/triedb" + "github.com/pierrec/lz4/v4" +) + +const ( + incrSnapshotNamePattern = `(.*)-incr-(\d+)-(\d+)\.tar\.lz4` + maxRetries = 5 + baseDelay = time.Second +) + +// Database keys for download status +var ( + incrDownloadedFilesKey = []byte("incr_downloaded_files") + incrToDownloadFilesKey = []byte("incr_to_download_files") + incrMergedFilesKey = []byte("incr_merged_files") + incrToMergeFilesKey = []byte("incr_to_merge_files") +) + +// metadata file contains many IncrMetadata, array +type IncrMetadata struct { + FileName string `json:"file_name"` + MD5Sum string `json:"md5_sum"` + Size uint64 `json:"size"` +} + +// IncrFileInfo represents parsed incremental file information +type IncrFileInfo struct { + Metadata IncrMetadata + StartBlock uint64 + EndBlock uint64 + LocalPath string + Downloaded bool + Verified bool + Extracted bool + Merged bool +} + +// DownloadProgress represents download progress for a file +type DownloadProgress struct { + FileName string + TotalSize uint64 + DownloadedSize uint64 + Progress float64 + Speed string + Status string + Error error +} + +// IncrDownloader handles incremental snapshot downloading and merging +type IncrDownloader struct { + db ethdb.Database + triedb *triedb.Database + remoteURL string + incrPath string + localBlockNum uint64 + + // Download management + downloadChan chan *IncrFileInfo + progressChan chan *DownloadProgress + errorChan chan error + + // State management + files []*IncrFileInfo + downloadWG sync.WaitGroup + mergeWG sync.WaitGroup + ctx context.Context + cancel context.CancelFunc + + // Statistics + totalFiles int + downloadedFiles int + mergedFiles int + mu sync.RWMutex + + // Merge order control + expectedNextBlockStart uint64 // Next expected start block for merge + downloadedFilesMap map[uint64]*IncrFileInfo // Downloaded files ready for merge, key is StartBlock + mergeMutex sync.Mutex // Protects merge-related state +} + +// NewIncrDownloader creates a new incremental downloader +func NewIncrDownloader(db ethdb.Database, triedb *triedb.Database, remoteURL, incrPath string, localBlockNum uint64) *IncrDownloader { + ctx, cancel := context.WithCancel(context.Background()) + // we don't validate the url and assume it's valid + newURL := strings.TrimSuffix(remoteURL, "/") + + downloader := &IncrDownloader{ + db: db, + triedb: triedb, + remoteURL: newURL, + incrPath: incrPath, + localBlockNum: localBlockNum, + downloadChan: make(chan *IncrFileInfo, 100), + progressChan: make(chan *DownloadProgress, 100), + errorChan: make(chan error, 10), + ctx: ctx, + cancel: cancel, + downloadedFilesMap: make(map[uint64]*IncrFileInfo), + } + + return downloader +} + +// saveDownloadedFiles saves list of downloaded files to db +func (d *IncrDownloader) saveDownloadedFiles(files []string) error { + data, err := json.Marshal(files) + if err != nil { + return fmt.Errorf("failed to marshal downloaded files: %v", err) + } + return d.db.Put(incrDownloadedFilesKey, data) +} + +// loadDownloadedFiles loads list of downloaded files from db +func (d *IncrDownloader) loadDownloadedFiles() ([]string, error) { + data, err := d.db.Get(incrDownloadedFilesKey) + if err != nil { + return nil, err + } + + var files []string + if err = json.Unmarshal(data, &files); err != nil { + return nil, fmt.Errorf("failed to unmarshal downloaded files: %v", err) + } + return files, nil +} + +// saveToDownloadFiles saves list of currently to download files to db +func (d *IncrDownloader) saveToDownloadFiles(files []string) error { + data, err := json.Marshal(files) + if err != nil { + return fmt.Errorf("failed to marshal to download files: %v", err) + } + return d.db.Put(incrToDownloadFilesKey, data) +} + +// loadToDownloadFiles loads list of currently to download files from db +func (d *IncrDownloader) loadToDownloadFiles() ([]string, error) { + data, err := d.db.Get(incrToDownloadFilesKey) + if err != nil { + return nil, err + } + + var files []string + if err = json.Unmarshal(data, &files); err != nil { + return nil, fmt.Errorf("failed to unmarshal to download files: %v", err) + } + return files, nil +} + +// saveDownloadedFiles saves list of downloaded files to db +func (d *IncrDownloader) saveMergedFiles(files []string) error { + data, err := json.Marshal(files) + if err != nil { + return fmt.Errorf("failed to marshal merged files: %v", err) + } + return d.db.Put(incrMergedFilesKey, data) +} + +// loadMergedFiles loads list of merged files from db +func (d *IncrDownloader) loadMergedFiles() ([]string, error) { + data, err := d.db.Get(incrMergedFilesKey) + if err != nil { + return nil, err + } + + var files []string + if err = json.Unmarshal(data, &files); err != nil { + return nil, fmt.Errorf("failed to unmarshal merged files: %v", err) + } + return files, nil +} + +// saveToMergeFiles saves list of currently to merge files to db +func (d *IncrDownloader) saveToMergeFiles(files []string) error { + data, err := json.Marshal(files) + if err != nil { + return fmt.Errorf("failed to marshal to merge files: %v", err) + } + return d.db.Put(incrToMergeFilesKey, data) +} + +// loadToMergeFiles loads list of currently to merge files from db +func (d *IncrDownloader) loadToMergeFiles() ([]string, error) { + data, err := d.db.Get(incrToMergeFilesKey) + if err != nil { + return nil, err + } + + var files []string + if err = json.Unmarshal(data, &files); err != nil { + return nil, fmt.Errorf("failed to unmarshal to merge files: %v", err) + } + return files, nil +} + +// Stage 1: Prepare - fetch metadata and validate +func (d *IncrDownloader) Prepare() error { + log.Info("Starting preparation phase", "remoteURL", d.remoteURL, "localBlockNum", d.localBlockNum) + + // Download metadata file + metadata, err := d.fetchMetadata() + if err != nil { + log.Error("Failed to fetch metadata", "error", err) + return err + } + + // Parse and filter file info + files, err := d.parseFileInfo(metadata) + if err != nil { + log.Error("Failed to parse file info", "error", err) + return err + } + + // Process file status and categorize files + if err = d.processFileStatus(files); err != nil { + return err + } + return nil +} + +// processFileStatus processes file status and categorizes files for download and merge +// toDownload -> downloaded -> toMerge -> merged +func (d *IncrDownloader) processFileStatus(files []*IncrFileInfo) error { + // Load existing file status from database + downloadedFiles, err := d.loadDownloadedFiles() + if err != nil { + log.Warn("Failed to load downloaded files list, starting fresh") + downloadedFiles = []string{} + } + + mergedFiles, err := d.loadMergedFiles() + if err != nil { + log.Warn("Failed to load merged files list, starting fresh") + mergedFiles = []string{} + } + + toDownloadFiles, err := d.loadToDownloadFiles() + if err != nil { + log.Warn("Failed to load to download files list, starting fresh") + toDownloadFiles = []string{} + } + + toMergeFiles, err := d.loadToMergeFiles() + if err != nil { + log.Warn("Failed to load to merge files list, starting fresh") + toMergeFiles = []string{} + } + + // Create sets for quick lookup + downloadedSet := make(map[string]bool) + for _, file := range downloadedFiles { + downloadedSet[file] = true + } + + mergedSet := make(map[string]bool) + for _, file := range mergedFiles { + mergedSet[file] = true + } + + toDownloadSet := make(map[string]bool) + for _, file := range toDownloadFiles { + toDownloadSet[file] = true + } + + toMergeSet := make(map[string]bool) + for _, file := range toMergeFiles { + toMergeSet[file] = true + } + + // Filter and categorize files based on their current status + var newToDownloadFiles []*IncrFileInfo + var newToDownloadFileNames []string + var newToMergeFiles []*IncrFileInfo + var newToMergeFileNames []string + + for _, file := range files { + fileName := file.Metadata.FileName + + // Skip already merged files + if mergedSet[fileName] { + log.Debug("Skipping already merged file", "fileName", fileName) + continue + } + + // Check if file is already downloaded + if downloadedSet[fileName] { + log.Debug("File already downloaded, checking merge status", "fileName", fileName) + d.downloadedFiles++ + + // If downloaded but not merged, add to merge queue + if !mergedSet[fileName] { + if !toMergeSet[fileName] { + newToMergeFiles = append(newToMergeFiles, file) + newToMergeFileNames = append(newToMergeFileNames, fileName) + log.Debug("Adding downloaded file to merge queue", "fileName", fileName) + } + } + continue + } + + // Check if file is currently being downloaded + if toDownloadSet[fileName] { + log.Debug("File is currently being downloaded, keeping in download queue", "fileName", fileName) + newToDownloadFiles = append(newToDownloadFiles, file) + newToDownloadFileNames = append(newToDownloadFileNames, fileName) + continue + } + + // Check if file is currently being merged + if toMergeSet[fileName] { + log.Debug("File is currently being merged, keeping in merge queue", "fileName", fileName) + newToMergeFiles = append(newToMergeFiles, file) + newToMergeFileNames = append(newToMergeFileNames, fileName) + continue + } + + // New file to download + log.Debug("Adding new file to download queue", "fileName", fileName) + newToDownloadFiles = append(newToDownloadFiles, file) + newToDownloadFileNames = append(newToDownloadFileNames, fileName) + } + + // Update file lists + d.files = newToDownloadFiles + d.totalFiles = len(d.files) + + if err = d.saveToDownloadFiles(newToDownloadFileNames); err != nil { + log.Error("Failed to save to download files", "error", err) + return err + } + + if err = d.saveToMergeFiles(newToMergeFileNames); err != nil { + log.Error("Failed to save to merge files", "error", err) + return err + } + + // Initialize downloaded files map with files that are ready for merge + for _, file := range newToMergeFiles { + d.downloadedFilesMap[file.StartBlock] = file + log.Debug("Added file to downloaded files map for merge", "fileName", file.Metadata.FileName, "startBlock", file.StartBlock) + } + + // Initialize expected next block start for merge ordering + if len(newToMergeFiles) > 0 { + // Find the earliest start block among files ready for merge + earliestBlock := newToMergeFiles[0].StartBlock + for _, file := range newToMergeFiles { + if file.StartBlock < earliestBlock { + earliestBlock = file.StartBlock + } + } + d.expectedNextBlockStart = earliestBlock + log.Info("Initialized expected next block start from existing merge queue", "expectedNextBlockStart", d.expectedNextBlockStart) + } else if len(d.files) > 0 { + d.expectedNextBlockStart = d.files[0].StartBlock + log.Info("Initialized expected next block start from new files", "expectedNextBlockStart", d.expectedNextBlockStart) + } + + log.Info("Preparation completed", "totalFiles", d.totalFiles, "downloadedFiles", len(downloadedFiles), + "mergedFiles", len(mergedFiles), "toDownloadFiles", len(newToDownloadFileNames), + "toMergeFiles", len(newToMergeFileNames), "firstBlock", d.getFirstBlockNum(), "lastBlock", d.getLastBlockNum()) + return nil +} + +// Download incremental files +func (d *IncrDownloader) Download() error { + log.Info("Starting download phase", "totalFiles", d.totalFiles) + + // Create download directory + if err := os.MkdirAll(d.incrPath, 0755); err != nil { + return fmt.Errorf("failed to create download directory: %v", err) + } + + // Start download workers + numWorkers := 4 + for i := 0; i < numWorkers; i++ { + d.downloadWG.Add(1) + go d.downloadWorker() + } + + // Start progress monitor + go d.progressMonitor() + + // Queue files for download + for _, file := range d.files { + select { + case d.downloadChan <- file: + case <-d.ctx.Done(): + return d.ctx.Err() + } + } + close(d.downloadChan) + + // Wait for all downloads to complete + d.downloadWG.Wait() + + log.Info("Download phase completed", "downloadedFiles", d.downloadedFiles) + return nil +} + +// RunConcurrent executes download and merge concurrently +func (d *IncrDownloader) RunConcurrent() error { + if err := d.Prepare(); err != nil { + return err + } + + // Start merge worker + d.mergeWG.Add(1) + go d.mergeWorker() + + // Start download and merge concurrently + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + if err := d.Download(); err != nil { + log.Error("Download failed", "error", err) + d.errorChan <- err + } + log.Debug("Download goroutine completed") + }() + + wg.Wait() + log.Info("All downloads completed, waiting for merge to complete") + + // Wait for merge worker to complete + d.mergeWG.Wait() + log.Info("All merges completed") + + return nil +} + +// fetchMetadata downloads and parses metadata file +func (d *IncrDownloader) fetchMetadata() ([]IncrMetadata, error) { + resp, err := http.Get(fmt.Sprintf("%s/incr_metadata.json", d.remoteURL)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP error: %d", resp.StatusCode) + } + + var metadata []IncrMetadata + if err = json.NewDecoder(resp.Body).Decode(&metadata); err != nil { + return nil, err + } + log.Info("Metadata fetched", "metadata", metadata) + + return metadata, nil +} + +// parseFileInfo parses file names to extract block information +func (d *IncrDownloader) parseFileInfo(metadata []IncrMetadata) ([]*IncrFileInfo, error) { + if len(metadata) == 0 { + return nil, fmt.Errorf("no metadata found") + } + + pattern := regexp.MustCompile(incrSnapshotNamePattern) + + var ( + files []*IncrFileInfo + filteredFiles []*IncrFileInfo + ) + + // Parse all files from metadata and separate by format + for _, meta := range metadata { + if matches := pattern.FindStringSubmatch(meta.FileName); len(matches) == 4 { + startBlock, err := strconv.ParseUint(matches[2], 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid start block in %s: %v", meta.FileName, err) + } + endBlock, err := strconv.ParseUint(matches[3], 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid end block in %s: %v", meta.FileName, err) + } + + files = append(files, &IncrFileInfo{ + Metadata: meta, + StartBlock: startBlock, + EndBlock: endBlock, + LocalPath: filepath.Join(d.incrPath, meta.FileName), + }) + } else { + log.Warn("Invalid file name format", "fileName", meta.FileName) + continue + } + } + + // Sort files by end block in ascending order + sort.Slice(files, func(i, j int) bool { + return files[i].EndBlock < files[j].EndBlock + }) + // Check continuity of all files before filtering + if err := d.checkFileContinuity(files); err != nil { + return nil, err + } + // filter the block number that matches local data + for index, file := range files { + // Check if local block number falls within this file's range + if file.StartBlock <= d.localBlockNum && d.localBlockNum <= file.EndBlock { + // Found the file containing local block number, add all remaining files from this point + filteredFiles = append(filteredFiles, files[index:]...) + break + } + } + + if len(filteredFiles) == 0 { + return nil, fmt.Errorf("remote incr snapshots don't match local data: %d", d.localBlockNum) + } + + log.Info("Filtered incremental files", "totalFiles", len(files), "keptFiles", len(filteredFiles), + "localBlockNum", d.localBlockNum) + return filteredFiles, nil +} + +// checkFileContinuity checks if all files have continuous block ranges +// startBlock = previousEndBlock + 1 (except for the first file) +func (d *IncrDownloader) checkFileContinuity(files []*IncrFileInfo) error { + if len(files) == 0 { + return nil + } + + // For the first file, we don't check continuity since we don't know the previous end block + // We'll check from the second file onwards + for i := 1; i < len(files); i++ { + prevFile := files[i-1] + currFile := files[i] + + expectedStartBlock := prevFile.EndBlock + 1 + if currFile.StartBlock != expectedStartBlock { + return fmt.Errorf("file continuity broken: file %s ends at %d, but file %s starts at %d (expected %d)", + prevFile.Metadata.FileName, prevFile.EndBlock, + currFile.Metadata.FileName, currFile.StartBlock, expectedStartBlock) + } + } + + log.Info("File continuity check completed successfully", "totalFiles", len(files)) + return nil +} + +// downloadWorker handles file downloads +func (d *IncrDownloader) downloadWorker() { + defer d.downloadWG.Done() + + for file := range d.downloadChan { + // Mark file as to download + d.markFileAsToDownload(file.Metadata.FileName) + + if err := d.downloadFile(file); err != nil { + log.Error("Failed to download file", "file", file.Metadata.FileName, "error", err) + d.errorChan <- err + continue + } + + if err := d.verifyAndExtract(file); err != nil { + log.Error("Failed to verify or extract failed", "file", file.Metadata.FileName, "error", err) + d.errorChan <- err + continue + } + + log.Info("Finished downloading and verifying file", "file", file.Metadata.FileName) + // Mark file as downloaded + d.markFileAsDownloaded(file.Metadata.FileName) + + d.mu.Lock() + d.downloadedFiles++ + d.mu.Unlock() + + log.Debug("File completed, queuing for merge", "file", file.Metadata.FileName, "downloadedFiles", d.downloadedFiles) + d.queueForMerge(file) + } +} + +// markFileAsToDownload marks a file as currently to download +func (d *IncrDownloader) markFileAsToDownload(fileName string) { + d.mu.Lock() + defer d.mu.Unlock() + + downloadingFiles, _ := d.loadToDownloadFiles() + if downloadingFiles == nil { + downloadingFiles = []string{} + } + + // Check if already in list + found := false + for _, file := range downloadingFiles { + if file == fileName { + found = true + break + } + } + + if !found { + downloadingFiles = append(downloadingFiles, fileName) + d.saveToDownloadFiles(downloadingFiles) + log.Debug("Marked file as downloading", "fileName", fileName) + } +} + +// removeFromToDownload removes a file from to download list +func (d *IncrDownloader) removeFromToDownload(fileName string) { + toDownloadFiles, err := d.loadToDownloadFiles() + if err != nil { + log.Error("Failed to load to download files", "error", err) + } + + if toDownloadFiles != nil { + var newDownloadingFiles []string + for _, file := range toDownloadFiles { + if file != fileName { + newDownloadingFiles = append(newDownloadingFiles, file) + } + } + d.saveToDownloadFiles(newDownloadingFiles) + log.Debug("Removed file from to download list", "fileName", fileName) + } +} + +// markFileAsDownloaded marks a file as downloaded and removes from downloading list +func (d *IncrDownloader) markFileAsDownloaded(fileName string) { + d.mu.Lock() + defer d.mu.Unlock() + + // Add to downloaded files + downloadedFiles, _ := d.loadDownloadedFiles() + if downloadedFiles == nil { + downloadedFiles = []string{} + } + // Check if already in list + found := false + for _, file := range downloadedFiles { + if file == fileName { + found = true + break + } + } + + if !found { + downloadedFiles = append(downloadedFiles, fileName) + d.saveDownloadedFiles(downloadedFiles) + log.Debug("Marked file as downloaded", "fileName", fileName) + } + + // Remove from to download files + d.removeFromToDownload(fileName) +} + +// markFileAsToMerge marks a file as currently to merge +func (d *IncrDownloader) markFileAsToMerge(fileName string) { + d.mu.Lock() + defer d.mu.Unlock() + + toMergeFiles, _ := d.loadToMergeFiles() + if toMergeFiles == nil { + toMergeFiles = []string{} + } + + // Check if already in list + found := false + for _, file := range toMergeFiles { + if file == fileName { + found = true + break + } + } + + if !found { + toMergeFiles = append(toMergeFiles, fileName) + d.saveToMergeFiles(toMergeFiles) + log.Debug("Marked file as to merge", "fileName", fileName) + } +} + +// removeFromToMerge removes a file from to merge list +func (d *IncrDownloader) removeFromToMerge(fileName string) { + toMergeFiles, err := d.loadToMergeFiles() + if err != nil { + log.Error("Failed to load to merge files", "error", err) + return + } + + if toMergeFiles != nil { + var newToMergeFiles []string + for _, file := range toMergeFiles { + if file != fileName { + newToMergeFiles = append(newToMergeFiles, file) + } + } + d.saveToMergeFiles(newToMergeFiles) + log.Debug("Removed file from to merge list", "fileName", fileName) + } +} + +// markFileAsMerged marks a file as merged and removes from to merge list +func (d *IncrDownloader) markFileAsMerged(fileName string) { + d.mu.Lock() + defer d.mu.Unlock() + + // Add to merged files + mergedFiles, _ := d.loadMergedFiles() + if mergedFiles == nil { + mergedFiles = []string{} + } + // Check if already in list + found := false + for _, file := range mergedFiles { + if file == fileName { + found = true + break + } + } + + if !found { + mergedFiles = append(mergedFiles, fileName) + d.saveMergedFiles(mergedFiles) + log.Debug("Marked file as merged", "fileName", fileName) + } + + // Remove from to merge files + d.removeFromToMerge(fileName) +} + +// queueForMerge queues a file for merge (non-blocking) +func (d *IncrDownloader) queueForMerge(file *IncrFileInfo) { + d.mergeMutex.Lock() + defer d.mergeMutex.Unlock() + + // Check if file is already in downloaded files map + if existingFile, exists := d.downloadedFilesMap[file.StartBlock]; exists { + if existingFile.Metadata.FileName == file.Metadata.FileName { + log.Debug("File already in downloaded files map, skipping", "file", file.Metadata.FileName, "startBlock", file.StartBlock) + return + } + } + + // Check if file has already been merged + if file.Merged { + log.Debug("File already merged, skipping queue", "file", file.Metadata.FileName, "startBlock", file.StartBlock) + return + } + + // Add to downloaded files map + d.downloadedFilesMap[file.StartBlock] = file + + // Mark file as to merge in database + d.markFileAsToMerge(file.Metadata.FileName) + + log.Debug("File queued for merge", "file", file.Metadata.FileName, "startBlock", file.StartBlock) +} + +// downloadFile downloads a single file with retry mechanism +func (d *IncrDownloader) downloadFile(file *IncrFileInfo) error { + for attempt := 1; attempt <= maxRetries; attempt++ { + err := d.downloadWithHTTP(file) + if err == nil { + return nil + } + + // Log the error for this attempt + log.Warn("Download attempt failed", "file", file.Metadata.FileName, "attempt", attempt, + "maxRetries", maxRetries, "error", err) + + // If this is the last attempt, return the error + if attempt == maxRetries { + return fmt.Errorf("download failed after %d attempts, last error: %w", maxRetries, err) + } + + // Calculate delay with exponential backoff + delay := baseDelay * time.Duration(attempt) + log.Info("Retrying download", "file", file.Metadata.FileName, "attempt", attempt+1, "delay", delay) + + // Wait before retrying + select { + case <-d.ctx.Done(): + return d.ctx.Err() + case <-time.After(delay): + continue + } + } + + return fmt.Errorf("failed to download file after %d attempts", maxRetries) +} + +// ChunkInfo represents a download chunk +type ChunkInfo struct { + Index int + Start int64 + End int64 + Downloaded int64 + TempFile string + Completed bool +} + +// ChunkProgress represents progress of a chunk download +type ChunkProgress struct { + ChunkIndex int + Downloaded int64 + Total int64 + FileName string +} + +// downloadWithHTTP downloads file using concurrent HTTP requests +func (d *IncrDownloader) downloadWithHTTP(file *IncrFileInfo) error { + url := fmt.Sprintf("%s/%s", d.remoteURL, file.Metadata.FileName) + log.Info("Start downloading incremental snapshot", "url", url) + + // Check if file already exists and has correct size + if info, err := os.Stat(file.LocalPath); err == nil { + if uint64(info.Size()) == file.Metadata.Size { + log.Info("File already exists with correct size, skipping download", "file", file.Metadata.FileName) + return nil + } + } + + // Check if server supports range requests + supportsRange, contentLength, err := d.checkRangeSupport(url) + if err != nil { + return fmt.Errorf("failed to check range support: %v", err) + } + + if !supportsRange { + log.Info("Server doesn't support range requests, using single-threaded download", "file", file.Metadata.FileName) + return d.downloadSingleThreaded(url, file) + } + + // Use expected size from metadata, fallback to content-length + totalSize := file.Metadata.Size + if totalSize == 0 { + totalSize = uint64(contentLength) + } + + // Calculate chunk size and number of chunks + numChunks := 8 + chunkSize := int64(totalSize) / int64(numChunks) + if chunkSize < 1024*1024 { // Minimum 1MB per chunk + chunkSize = 1024 * 1024 + numChunks = int(int64(totalSize) / chunkSize) + if numChunks < 1 { + numChunks = 1 + } + } + + // Create chunk info + chunks := make([]*ChunkInfo, numChunks) + for i := 0; i < numChunks; i++ { + start := int64(i) * chunkSize + end := start + chunkSize - 1 + if i == numChunks-1 { + end = int64(totalSize) - 1 + } + + chunks[i] = &ChunkInfo{ + Index: i, + Start: start, + End: end, + TempFile: fmt.Sprintf("%s.part%d", file.LocalPath, i), + } + } + + // Check for existing partial downloads + d.checkExistingChunks(chunks) + + // Download chunks concurrently + var wg sync.WaitGroup + progressChan := make(chan *ChunkProgress, numChunks) + errorChan := make(chan error, numChunks) + + // Download each chunk + for _, chunk := range chunks { + if chunk.Completed { + continue // Skip already completed chunks + } + + wg.Add(1) + go func(chunk *ChunkInfo) { + defer wg.Done() + if err = d.downloadChunk(url, chunk, progressChan); err != nil { + errorChan <- fmt.Errorf("failed to download chunk %d: %v", chunk.Index, err) + } + }(chunk) + } + + wg.Wait() + close(progressChan) + + // Check for errors + select { + case err = <-errorChan: + return err + default: + } + + // Merge chunks + log.Debug("Merging chunks", "file", file.Metadata.FileName, "chunks", numChunks) + if err = d.mergeChunks(chunks, file.LocalPath); err != nil { + return fmt.Errorf("failed to merge chunks: %v", err) + } + + // Clean up temporary files + d.cleanupTempFiles(chunks) + + // Verify final file size + if info, err := os.Stat(file.LocalPath); err != nil { + return fmt.Errorf("downloaded file not found: %v", err) + } else if uint64(info.Size()) != totalSize { + return fmt.Errorf("downloaded file size mismatch: expected %d, got %d", totalSize, info.Size()) + } + + log.Debug("Download completed successfully", "file", file.Metadata.FileName, "size", totalSize) + return nil +} + +// verifyAndExtract verifies MD5 hash and extracts file with retry mechanism +func (d *IncrDownloader) verifyAndExtract(file *IncrFileInfo) error { + for attempt := 1; attempt <= maxRetries; attempt++ { + // Verify MD5 + if err := d.verifyHash(file); err != nil { + log.Warn("Hash verification attempt failed", "file", file.Metadata.FileName, "attempt", attempt, + "maxRetries", maxRetries, "error", err) + + if attempt == maxRetries { + return fmt.Errorf("hash verification failed after %d attempts, last error: %w", maxRetries, err) + } + + // Wait before retrying + delay := baseDelay * time.Duration(attempt) + select { + case <-d.ctx.Done(): + return d.ctx.Err() + case <-time.After(delay): + continue + } + } + file.Verified = true + break + } + + // Extract file with retry mechanism + for attempt := 1; attempt <= maxRetries; attempt++ { + if err := d.extractFile(file); err != nil { + log.Warn("Failed to extract file", "file", file.Metadata.FileName, "attempt", attempt, + "maxRetries", maxRetries, "error", err) + + if attempt == maxRetries { + return fmt.Errorf("file extraction failed after %d attempts, last error: %w", maxRetries, err) + } + + // Wait before retrying + delay := baseDelay * time.Duration(attempt) + select { + case <-d.ctx.Done(): + return d.ctx.Err() + case <-time.After(delay): + continue + } + } + file.Extracted = true + break + } + + return nil +} + +// verifyHash verifies file MD5 hash +func (d *IncrDownloader) verifyHash(file *IncrFileInfo) error { + f, err := os.Open(file.LocalPath) + if err != nil { + return err + } + defer f.Close() + + h := md5.New() + if _, err = io.Copy(h, f); err != nil { + return err + } + + actualHash := hex.EncodeToString(h.Sum(nil)) + if actualHash != file.Metadata.MD5Sum { + return fmt.Errorf("hash mismatch for %s: expected %s, got %s", + file.Metadata.FileName, file.Metadata.MD5Sum, actualHash) + } + + log.Debug("Finished verifying md5 hash", "file", file.LocalPath) + return nil +} + +// extractFile extracts tar.lz4 file +func (d *IncrDownloader) extractFile(file *IncrFileInfo) error { + // Extract directory + extractDir := filepath.Join(d.incrPath) + if err := os.MkdirAll(extractDir, 0755); err != nil { + return err + } + log.Debug("Extracting file", "file", file.Metadata.FileName, "extractDir", extractDir) + + // Open the lz4 file + inputFile, err := os.Open(file.LocalPath) + if err != nil { + return fmt.Errorf("failed to open file %s: %v", file.LocalPath, err) + } + defer inputFile.Close() + + // Create lz4 reader + lz4Reader := lz4.NewReader(inputFile) + // Create tar reader from lz4 output + tarReader := tar.NewReader(lz4Reader) + + // Extract tar contents + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("failed to read tar header: %v", err) + } + + // Create the full path for the file + targetPath := filepath.Join(extractDir, header.Name) + + // Ensure the target directory exists + if err = os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %v", filepath.Dir(targetPath), err) + } + + switch header.Typeflag { + case tar.TypeDir: + // Create directory + if err = os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil { + return fmt.Errorf("failed to create directory %s: %v", targetPath, err) + } + case tar.TypeReg: + // Create regular file + outFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + if err != nil { + return fmt.Errorf("failed to create file %s: %v", targetPath, err) + } + + // Copy file content + if _, err = io.Copy(outFile, tarReader); err != nil { + outFile.Close() + return fmt.Errorf("failed to write file %s: %v", targetPath, err) + } + outFile.Close() + case tar.TypeSymlink: + // Create symbolic link + if err = os.Symlink(header.Linkname, targetPath); err != nil { + return fmt.Errorf("failed to create symlink %s: %v", targetPath, err) + } + default: + log.Warn("Unsupported file type in tar", "name", header.Name, "type", header.Typeflag) + } + } + + log.Debug("File extracted successfully", "file", file.Metadata.FileName, "extractDir", extractDir) + return nil +} + +// mergeWorker handles sequential merging of files +func (d *IncrDownloader) mergeWorker() { + defer d.mergeWG.Done() + + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + // Check if there are files ready for merge + d.mergeMutex.Lock() + currFile, exists := d.downloadedFilesMap[d.expectedNextBlockStart] + d.mergeMutex.Unlock() + + if exists { + // Process the next file in sequence + log.Info("Processing file for merge", "file", currFile.Metadata.FileName, + "startBlock", currFile.StartBlock, "endBlock", currFile.EndBlock) + + // Check if file has already been merged + if currFile.Merged { + log.Warn("File already merged, removing from map", "file", currFile.Metadata.FileName) + d.mergeMutex.Lock() + delete(d.downloadedFilesMap, d.expectedNextBlockStart) + d.mergeMutex.Unlock() + d.expectedNextBlockStart = currFile.EndBlock + 1 + continue + } + + // Remove from map before processing to prevent race conditions + d.mergeMutex.Lock() + delete(d.downloadedFilesMap, d.expectedNextBlockStart) + d.mergeMutex.Unlock() + + // Check if this is the last incr snapshot + d.mu.RLock() + isLastSnapshot := (d.mergedFiles + 1) == len(d.files) + d.mu.RUnlock() + + // Perform merge operation + path := filepath.Join(d.incrPath, fmt.Sprintf("incr-%d-%d", currFile.StartBlock, currFile.EndBlock)) + if isLastSnapshot { + log.Info("This is the last incremental snapshot, performing final cleanup and optimization") + complete, err := rawdb.CheckIncrSnapshotComplete(path) + if err != nil { + log.Error("Failed to check last incr snapshot complete", "err", err) + d.errorChan <- err + return + } + if !complete { + log.Warn("Skip last incr snapshot due to data is incomplete") + return + } + } + if err := MergeIncrSnapshot(d.db, d.triedb, path); err != nil { + log.Error("Failed to merge", "file", currFile.Metadata.FileName, "error", err) + d.errorChan <- err + return + } + + currFile.Merged = true + d.markFileAsMerged(currFile.Metadata.FileName) + + d.mu.Lock() + d.mergedFiles++ + d.mu.Unlock() + + // Update expected next start block + d.expectedNextBlockStart = currFile.EndBlock + 1 + log.Info("File merged successfully", "file", currFile.Metadata.FileName, + "progress", fmt.Sprintf("%d/%d", d.mergedFiles, d.totalFiles), + "startBlock", currFile.StartBlock, "endBlock", currFile.EndBlock) + } else { + // Check if all files have been processed + d.mu.RLock() + downloadedCount := d.downloadedFiles + mergedCount := d.mergedFiles + d.mu.RUnlock() + + if mergedCount == downloadedCount && downloadedCount == len(d.files) { + log.Info("All files processed, merge worker exiting") + return + } + } + + case <-d.ctx.Done(): + log.Info("Context cancelled, merge worker exiting") + return + } + } +} + +// progressMonitor monitors and reports download progress +func (d *IncrDownloader) progressMonitor() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + for { + select { + case progress := <-d.progressChan: + log.Info("Download progress", "file", progress.FileName, + "progress", fmt.Sprintf("%.1f%%", progress.Progress), + "speed", progress.Speed, "status", progress.Status) + case <-ticker.C: + d.mu.RLock() + stats := d.GetFileStatusStats() + log.Info("Overall progress", "downloaded", d.downloadedFiles, + "merged", d.mergedFiles, "total", d.totalFiles, "stats", stats) + d.mu.RUnlock() + case <-d.ctx.Done(): + return + } + } +} + +// progressReader tracks download progress +type progressReader struct { + reader io.Reader + total uint64 + downloaded uint64 + filename string + progress chan<- *DownloadProgress + lastUpdate time.Time +} + +func (pr *progressReader) Read(p []byte) (int, error) { + n, err := pr.reader.Read(p) + pr.downloaded += uint64(n) + + // Update progress every 30 seconds + if time.Since(pr.lastUpdate) > (time.Second * 30) { + progress := &DownloadProgress{ + FileName: pr.filename, + TotalSize: pr.total, + DownloadedSize: pr.downloaded, + Progress: float64(pr.downloaded) / float64(pr.total) * 100, + Status: "downloading", + } + + select { + case pr.progress <- progress: + default: + } + + pr.lastUpdate = time.Now() + } + + return n, err +} + +// Cancel cancels all operations +func (d *IncrDownloader) Cancel() { + d.cancel() +} + +// Close closes all channels and cleans up +func (d *IncrDownloader) Close() { + d.cancel() + + // Clean up pending merge files + d.mergeMutex.Lock() + d.downloadedFilesMap = make(map[uint64]*IncrFileInfo) + d.mergeMutex.Unlock() + + close(d.progressChan) + close(d.errorChan) +} + +// Helper methods +func (d *IncrDownloader) getFirstBlockNum() uint64 { + if len(d.files) == 0 { + return 0 + } + return d.files[0].StartBlock +} + +func (d *IncrDownloader) getLastBlockNum() uint64 { + if len(d.files) == 0 { + return 0 + } + return d.files[len(d.files)-1].EndBlock +} + +// checkRangeSupport checks if server supports range requests +func (d *IncrDownloader) checkRangeSupport(url string) (bool, int64, error) { + req, err := http.NewRequest("HEAD", url, nil) + if err != nil { + return false, 0, err + } + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return false, 0, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return false, 0, fmt.Errorf("HTTP error: %d", resp.StatusCode) + } + + acceptRanges := resp.Header.Get("Accept-Ranges") + contentLength := resp.ContentLength + + return acceptRanges == "bytes", contentLength, nil +} + +// downloadSingleThreaded downloads file without range requests +func (d *IncrDownloader) downloadSingleThreaded(url string, file *IncrFileInfo) error { + log.Info("Starting single-threaded download", "file", file.Metadata.FileName) + + client := &http.Client{} + resp, err := client.Get(url) + if err != nil { + return fmt.Errorf("HTTP GET failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("HTTP error: %d", resp.StatusCode) + } + + out, err := os.Create(file.LocalPath) + if err != nil { + return fmt.Errorf("failed to create file: %v", err) + } + defer out.Close() + + // Track progress + pr := &progressReader{ + reader: resp.Body, + total: uint64(resp.ContentLength), + filename: file.Metadata.FileName, + progress: d.progressChan, + } + + _, err = io.Copy(out, pr) + return err +} + +// checkExistingChunks checks for existing partial downloads +func (d *IncrDownloader) checkExistingChunks(chunks []*ChunkInfo) { + for _, chunk := range chunks { + if info, err := os.Stat(chunk.TempFile); err == nil { + downloaded := info.Size() + expectedSize := chunk.End - chunk.Start + 1 + + if downloaded == expectedSize { + chunk.Completed = true + chunk.Downloaded = downloaded + log.Debug("Found completed chunk", "chunk", chunk.Index, "size", downloaded) + } else if downloaded > 0 { + chunk.Downloaded = downloaded + // Update start position for resume + chunk.Start += downloaded + log.Debug("Found partial chunk", "chunk", chunk.Index, "downloaded", downloaded, "remaining", expectedSize-downloaded) + } + } + } +} + +// downloadChunk downloads a single chunk with retry mechanism +func (d *IncrDownloader) downloadChunk(url string, chunk *ChunkInfo, progressChan chan<- *ChunkProgress) error { + for attempt := 1; attempt <= maxRetries; attempt++ { + err := d.downloadChunkAttempt(url, chunk, progressChan) + if err == nil { + // Success, no need to retry + return nil + } + + // Log the error for this attempt + log.Warn("Failed to download chunk", "chunk", chunk.Index, "attempt", attempt, + "maxRetries", maxRetries, "error", err) + + // If this is the last attempt, return the error + if attempt == maxRetries { + return fmt.Errorf("failed to download chunk after %d attempts, last error: %w", maxRetries, err) + } + + // Calculate delay with exponential backoff + delay := baseDelay * time.Duration(attempt) + log.Info("Retrying chunk download", "chunk", chunk.Index, "attempt", attempt+1, "delay", delay) + + // Wait before retrying + select { + case <-d.ctx.Done(): + return d.ctx.Err() + case <-time.After(delay): + continue + } + } + + return fmt.Errorf("chunk download failed after %d attempts", maxRetries) +} + +// downloadChunkAttempt performs a single attempt to download a chunk +func (d *IncrDownloader) downloadChunkAttempt(url string, chunk *ChunkInfo, progressChan chan<- *ChunkProgress) error { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + + // Set range header + rangeHeader := fmt.Sprintf("bytes=%d-%d", chunk.Start, chunk.End) + req.Header.Set("Range", rangeHeader) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusPartialContent && resp.StatusCode != http.StatusOK { + return fmt.Errorf("HTTP error: %d", resp.StatusCode) + } + + // Open temp file for writing (append mode for resume) + var out *os.File + if chunk.Downloaded > 0 { + out, err = os.OpenFile(chunk.TempFile, os.O_WRONLY|os.O_APPEND, 0644) + } else { + out, err = os.Create(chunk.TempFile) + } + if err != nil { + return err + } + defer out.Close() + + // Track progress + chunkSize := chunk.End - chunk.Start + 1 + downloaded := chunk.Downloaded + + buffer := make([]byte, 32*1024) // 32KB buffer + lastProgress := time.Now() + + for { + select { + case <-d.ctx.Done(): + return d.ctx.Err() + default: + } + + n, err := resp.Body.Read(buffer) + if n > 0 { + if _, writeErr := out.Write(buffer[:n]); writeErr != nil { + log.Error("Failed to write to file", "error", writeErr) + return writeErr + } + downloaded += int64(n) + + // Send progress update every 100ms + if time.Since(lastProgress) > 100*time.Millisecond { + select { + case progressChan <- &ChunkProgress{ + ChunkIndex: chunk.Index, + Downloaded: downloaded, + Total: chunkSize, + FileName: "", + }: + default: + } + lastProgress = time.Now() + } + } + + if err == io.EOF { + break + } + if err != nil { + log.Error("Failed to read from response body", "error", err) + return err + } + } + + chunk.Downloaded = downloaded + chunk.Completed = true + return nil +} + +// mergeChunks merges all chunks into final file +func (d *IncrDownloader) mergeChunks(chunks []*ChunkInfo, outputPath string) error { + output, err := os.Create(outputPath) + if err != nil { + return err + } + defer output.Close() + + for _, chunk := range chunks { + chunkFile, err := os.Open(chunk.TempFile) + if err != nil { + return fmt.Errorf("failed to open chunk %d: %v", chunk.Index, err) + } + + _, err = io.Copy(output, chunkFile) + chunkFile.Close() + + if err != nil { + return fmt.Errorf("failed to copy chunk %d: %v", chunk.Index, err) + } + } + + return nil +} + +// cleanupTempFiles removes temporary chunk files +func (d *IncrDownloader) cleanupTempFiles(chunks []*ChunkInfo) { + for _, chunk := range chunks { + if err := os.Remove(chunk.TempFile); err != nil { + log.Warn("Failed to remove temp file", "file", chunk.TempFile, "error", err) + } + } +} + +// GetFileStatusStats returns current file status statistics +func (d *IncrDownloader) GetFileStatusStats() map[string]interface{} { + d.mu.RLock() + defer d.mu.RUnlock() + + downloadedFiles, _ := d.loadDownloadedFiles() + mergedFiles, _ := d.loadMergedFiles() + toDownloadFiles, _ := d.loadToDownloadFiles() + toMergeFiles, _ := d.loadToMergeFiles() + + return map[string]interface{}{ + "totalFiles": d.totalFiles, + "downloadedFiles": len(downloadedFiles), + "mergedFiles": len(mergedFiles), + "toDownloadFiles": len(toDownloadFiles), + "toMergeFiles": len(toMergeFiles), + "downloadedFilesMap": len(d.downloadedFilesMap), + "expectedNextBlockStart": d.expectedNextBlockStart, + } +} diff --git a/core/incr_downloader_test.go b/core/incr_downloader_test.go new file mode 100644 index 0000000000..ab0b1d2509 --- /dev/null +++ b/core/incr_downloader_test.go @@ -0,0 +1,1374 @@ +package core + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/triedb" +) + +const ( + testURL = "http://test.com" +) + +func createTestDB() ethdb.Database { + return rawdb.NewMemoryDatabase() +} + +// Create test HTTP server +func createTestHTTPServer() (*httptest.Server, string) { + metadata := []IncrMetadata{ + { + FileName: "test-incr-1000-1999.tar.lz4", + MD5Sum: "d41d8cd98f00b204e9800998ecf8427e", + Size: 1024, + }, + { + FileName: "test-incr-2000-2999.tar.lz4", + MD5Sum: "d41d8cd98f00b204e9800998ecf8427e", + Size: 2048, + }, + } + + testFileContent := "This is a test file content for incremental snapshot download testing." + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + + switch { + case path == "/incr_metadata.json": + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(metadata) + + case strings.HasSuffix(path, ".tar.lz4"): + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testFileContent))) + w.Header().Set("Accept-Ranges", "bytes") + w.Write([]byte(testFileContent)) + + default: + http.NotFound(w, r) + } + })) + + return server, server.URL +} + +func TestIncrDownloader_HTTPDownload(t *testing.T) { + server, serverURL := createTestHTTPServer() + defer server.Close() + + // Setup test environment + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, serverURL, tempDir, 1000) + + // Create test file info + file := &IncrFileInfo{ + Metadata: IncrMetadata{ + FileName: "test-incr-1000-1999.tar.lz4", + MD5Sum: "d41d8cd98f00b204e9800998ecf8427e", + Size: 70, + }, + StartBlock: 1000, + EndBlock: 1999, + LocalPath: filepath.Join(tempDir, "test-incr-1000-2000.tar.lz4"), + } + + // Test HTTP download + err := downloader.downloadWithHTTP(file) + require.NoError(t, err) + + // Verify file downloaded + _, err = os.Stat(file.LocalPath) + require.NoError(t, err) + + // Verify file content + content, err := os.ReadFile(file.LocalPath) + require.NoError(t, err) + assert.Contains(t, string(content), "test file content") +} + +func TestIncrDownloader_HTTPDownloadWithRangeSupport(t *testing.T) { + // Create test server with Range support + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + testContent := "This is a test file content for range download testing." + + if strings.HasSuffix(path, ".tar.lz4") { + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) + w.Header().Set("Accept-Ranges", "bytes") + + // Handle Range requests + if rangeHeader := r.Header.Get("Range"); rangeHeader != "" { + // Simple Range handling + w.WriteHeader(http.StatusPartialContent) + w.Write([]byte(testContent)) + } else { + w.Write([]byte(testContent)) + } + } else { + http.NotFound(w, r) + } + })) + defer server.Close() + + // Setup test environment + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, server.URL, tempDir, 1000) + + // Create test file info + file := &IncrFileInfo{ + Metadata: IncrMetadata{ + FileName: "test-incr-1000-1999.tar.lz4", + MD5Sum: "d41d8cd98f00b204e9800998ecf8427e", + Size: 55, + }, + StartBlock: 1000, + EndBlock: 1999, + LocalPath: filepath.Join(tempDir, "test-incr-1000-1999.tar.lz4"), + } + + // Test Range-supported download + err := downloader.downloadWithHTTP(file) + require.NoError(t, err) + + // Verify file downloaded + _, err = os.Stat(file.LocalPath) + require.NoError(t, err) +} + +func TestIncrDownloader_HTTPDownloadWithoutRangeSupport(t *testing.T) { + // Create test server without Range support + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + testContent := "This is a test file content for single-threaded download testing." + + if strings.HasSuffix(path, ".tar.lz4") { + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) + // Don't set Accept-Ranges header + w.Write([]byte(testContent)) + } else { + http.NotFound(w, r) + } + })) + defer server.Close() + + // Setup test environment + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, server.URL, tempDir, 1000) + + // Create test file info + file := &IncrFileInfo{ + Metadata: IncrMetadata{ + FileName: "test-incr-1000-1999.tar.lz4", + MD5Sum: "d41d8cd98f00b204e9800998ecf8427e", + Size: 1024, + }, + StartBlock: 1000, + EndBlock: 1999, + LocalPath: filepath.Join(tempDir, "test-incr-1000-1999.tar.lz4"), + } + + // Test single-threaded download + err := downloader.downloadWithHTTP(file) + require.NoError(t, err) + + // Verify file downloaded + _, err = os.Stat(file.LocalPath) + require.NoError(t, err) +} + +func TestIncrDownloader_HTTPDownloadError(t *testing.T) { + // Create test server that returns errors + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + if strings.HasSuffix(path, ".tar.lz4") { + // Return 404 error + http.NotFound(w, r) + } else { + http.NotFound(w, r) + } + })) + defer server.Close() + + // Setup test environment + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, server.URL, tempDir, 1000) + + // Create test file info + file := &IncrFileInfo{ + Metadata: IncrMetadata{ + FileName: "test-incr-1000-1999.tar.lz4", + MD5Sum: "d41d8cd98f00b204e9800998ecf8427e", + Size: 1024, + }, + StartBlock: 1000, + EndBlock: 1999, + LocalPath: filepath.Join(tempDir, "test-incr-1000-1999.tar.lz4"), + } + + // Test download error + err := downloader.downloadWithHTTP(file) + assert.Error(t, err) + assert.Contains(t, err.Error(), "HTTP error: 404") +} + +func TestIncrDownloader_FetchMetadata(t *testing.T) { + // Create test metadata + metadata := []IncrMetadata{ + { + FileName: "test-incr-1000-1999.tar.lz4", + MD5Sum: "d41d8cd98f00b204e9800998ecf8427e", + Size: 1024, + }, + { + FileName: "test-incr-2000-2999.tar.lz4", + MD5Sum: "d41d8cd98f00b204e9800998ecf8427e", + Size: 2048, + }, + } + + // Create test HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/incr_metadata.json" { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(metadata) + } else { + http.NotFound(w, r) + } + })) + defer server.Close() + + // Setup test environment + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, server.URL, tempDir, 1000) + + // Test metadata fetching + fetchedMetadata, err := downloader.fetchMetadata() + require.NoError(t, err) + assert.Len(t, fetchedMetadata, 2) + assert.Equal(t, "test-incr-1000-1999.tar.lz4", fetchedMetadata[0].FileName) + assert.Equal(t, uint64(1024), fetchedMetadata[0].Size) +} + +func TestIncrDownloader_FetchMetadataError(t *testing.T) { + // Create test server that returns errors + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + })) + defer server.Close() + + // Setup test environment + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, server.URL, tempDir, 1000) + + // Test metadata fetching error + _, err := downloader.fetchMetadata() + assert.Error(t, err) + assert.Contains(t, err.Error(), "HTTP error: 500") +} + +func TestIncrDownloader_CheckRangeSupport(t *testing.T) { + // Create test server with Range support + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "HEAD" { + w.Header().Set("Accept-Ranges", "bytes") + w.Header().Set("Content-Length", "1024") + } else { + w.Write([]byte("test content")) + } + })) + defer server.Close() + + // Setup test environment + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, server.URL, tempDir, 1000) + + // Test Range support check + supportsRange, contentLength, err := downloader.checkRangeSupport(server.URL + "/test-file.tar.lz4") + require.NoError(t, err) + assert.True(t, supportsRange) + assert.Equal(t, int64(1024), contentLength) +} + +func TestIncrDownloader_CheckRangeSupportNotSupported(t *testing.T) { + // Create test server without Range support + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == "HEAD" { + // Don't set Accept-Ranges header + w.Header().Set("Content-Length", "1024") + } else { + w.Write([]byte("test content")) + } + })) + defer server.Close() + + // Setup test environment + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, server.URL, tempDir, 1000) + + // Test Range support check + supportsRange, contentLength, err := downloader.checkRangeSupport(server.URL + "/test-file.tar.lz4") + require.NoError(t, err) + assert.False(t, supportsRange) + assert.Equal(t, int64(1024), contentLength) +} + +func TestIncrDownloader_DownloadChunk(t *testing.T) { + // Create test server + testContent := "This is test content for chunk download testing." + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rangeHeader := r.Header.Get("Range") + if rangeHeader != "" { + w.Header().Set("Content-Range", "bytes 0-1023/1024") + w.WriteHeader(http.StatusPartialContent) + } + w.Write([]byte(testContent)) + })) + defer server.Close() + + // Setup test environment + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, server.URL, tempDir, 1000) + + // Create test chunk + chunk := &ChunkInfo{ + Index: 0, + Start: 0, + End: 1023, + TempFile: filepath.Join(tempDir, "test-chunk.part0"), + } + progressChan := make(chan *ChunkProgress, 1) + + // Test chunk download + err := downloader.downloadChunk(server.URL+"/test-file.tar.lz4", chunk, progressChan) + require.NoError(t, err) + + // Verify chunk file created + _, err = os.Stat(chunk.TempFile) + require.NoError(t, err) + + // Verify chunk content + content, err := os.ReadFile(chunk.TempFile) + require.NoError(t, err) + assert.Contains(t, string(content), "test content") +} + +func TestIncrDownloader_MergeChunks(t *testing.T) { + // Setup test environment + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, testURL, tempDir, 1000) + + // Create test chunks + chunks := []*ChunkInfo{ + { + Index: 0, + Start: 0, + End: 1023, + TempFile: filepath.Join(tempDir, "chunk1.part0"), + }, + { + Index: 1, + Start: 1024, + End: 2047, + TempFile: filepath.Join(tempDir, "chunk2.part1"), + }, + } + + // Create test chunk files + err := os.WriteFile(chunks[0].TempFile, []byte("chunk1 content"), 0644) + require.NoError(t, err) + err = os.WriteFile(chunks[1].TempFile, []byte("chunk2 content"), 0644) + require.NoError(t, err) + + outputPath := filepath.Join(tempDir, "merged-file.tar.lz4") + + // Test chunk merging + err = downloader.mergeChunks(chunks, outputPath) + require.NoError(t, err) + + // Verify merged file + _, err = os.Stat(outputPath) + require.NoError(t, err) + + // Verify merged content + content, err := os.ReadFile(outputPath) + require.NoError(t, err) + assert.Contains(t, string(content), "chunk1 content") + assert.Contains(t, string(content), "chunk2 content") +} + +func TestIncrDownloader_VerifyAndExtract(t *testing.T) { + // Setup test environment + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + testContent := "test file content" + testFile := filepath.Join(tempDir, "test-file.tar.lz4") + err := os.WriteFile(testFile, []byte(testContent), 0644) + require.NoError(t, err) + + file := &IncrFileInfo{ + Metadata: IncrMetadata{ + FileName: "test-file.tar.lz4", + MD5Sum: "d41d8cd98f00b204e9800998ecf8427e", // Empty file MD5 + Size: uint64(len(testContent)), + }, + LocalPath: testFile, + } + + downloader := NewIncrDownloader(db, trieDB, testURL, tempDir, 1000) + err = downloader.verifyAndExtract(file) + assert.Error(t, err) +} + +func TestIncrDownloader_VerifyHash(t *testing.T) { + // Setup test environment + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, testURL, tempDir, 1000) + + // Create test file + testContent := "" + testFile := filepath.Join(tempDir, "test-file.tar.lz4") + err := os.WriteFile(testFile, []byte(testContent), 0644) + require.NoError(t, err) + + // Create test file info (empty file MD5) + file := &IncrFileInfo{ + Metadata: IncrMetadata{ + FileName: "test-file.tar.lz4", + MD5Sum: "d41d8cd98f00b204e9800998ecf8427e", // Empty file MD5 + Size: 0, + }, + LocalPath: testFile, + } + + // Test MD5 verification + err = downloader.verifyHash(file) + require.NoError(t, err) +} + +func TestIncrDownloader_VerifyHashMismatch(t *testing.T) { + // Setup test environment + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, testURL, tempDir, 1000) + + // Create test file + testContent := "test content" + testFile := filepath.Join(tempDir, "test-file.tar.lz4") + err := os.WriteFile(testFile, []byte(testContent), 0644) + require.NoError(t, err) + + // Create test file info (wrong MD5) + file := &IncrFileInfo{ + Metadata: IncrMetadata{ + FileName: "test-file.tar.lz4", + MD5Sum: "wrong-md5-hash", + Size: uint64(len(testContent)), + }, + LocalPath: testFile, + } + + // Test MD5 verification failure + err = downloader.verifyHash(file) + assert.Error(t, err) + assert.Contains(t, err.Error(), "hash mismatch for test-file.tar.lz4") +} + +func TestIncrDownloader_Prepare(t *testing.T) { + // Setup test environment + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, testURL, tempDir, 1000) + + // Create test metadata + metadata := []IncrMetadata{ + { + FileName: "test-incr-1000-1999.tar.lz4", + MD5Sum: "test-md5-1", + Size: 1024, + }, + { + FileName: "test-incr-2000-2999.tar.lz4", + MD5Sum: "test-md5-2", + Size: 2048, + }, + { + FileName: "test-incr-3000-3999.tar.lz4", + MD5Sum: "test-md5-3", + Size: 3072, + }, + } + + // Test preparation with empty state + // Since fetchMetadata is private, we directly test parseFileInfo + files, err := downloader.parseFileInfo(metadata) + require.NoError(t, err) + assert.Len(t, files, 3) + + // Verify file list + assert.Equal(t, "test-incr-1000-1999.tar.lz4", files[0].Metadata.FileName) + assert.Equal(t, uint64(1000), files[0].StartBlock) + assert.Equal(t, uint64(1999), files[0].EndBlock) +} + +func TestIncrDownloader_FileStatusManagement(t *testing.T) { + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, testURL, tempDir, 1000) + + // Test file status management methods + fileName := "test-file.tar.lz4" + + // Test mark as to download + downloader.markFileAsToDownload(fileName) + toDownloadFiles, err := downloader.loadToDownloadFiles() + require.NoError(t, err) + assert.Contains(t, toDownloadFiles, fileName) + + // Test mark as downloaded + downloader.markFileAsDownloaded(fileName) + downloadedFiles, err := downloader.loadDownloadedFiles() + require.NoError(t, err) + assert.Contains(t, downloadedFiles, fileName) + + // Verify removed from to-download list + toDownloadFiles, err = downloader.loadToDownloadFiles() + require.NoError(t, err) + assert.NotContains(t, toDownloadFiles, fileName) + + // Test mark as to merge + downloader.markFileAsToMerge(fileName) + toMergeFiles, err := downloader.loadToMergeFiles() + require.NoError(t, err) + assert.Contains(t, toMergeFiles, fileName) + + // Test mark as merged + downloader.markFileAsMerged(fileName) + mergedFiles, err := downloader.loadMergedFiles() + require.NoError(t, err) + assert.Contains(t, mergedFiles, fileName) + + // Verify removed from to-merge list + toMergeFiles, err = downloader.loadToMergeFiles() + require.NoError(t, err) + assert.NotContains(t, toMergeFiles, fileName) +} + +func TestIncrDownloader_PrepareWithExistingStatus(t *testing.T) { + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, testURL, tempDir, 1000) + + // Pre-set some file statuses + err := downloader.saveDownloadedFiles([]string{"test-incr-1000-1999.tar.lz4"}) + assert.NoError(t, err) + err = downloader.saveMergedFiles([]string{"test-incr-2000-2999.tar.lz4"}) + assert.NoError(t, err) + err = downloader.saveToDownloadFiles([]string{"test-incr-3000-3999.tar.lz4"}) + assert.NoError(t, err) + err = downloader.saveToMergeFiles([]string{"test-incr-4000-4999.tar.lz4"}) + assert.NoError(t, err) + + // Create test metadata + metadata := []IncrMetadata{ + { + FileName: "test-incr-1000-1999.tar.lz4", // Downloaded + MD5Sum: "test-md5-1", + Size: 1024, + }, + { + FileName: "test-incr-2000-2999.tar.lz4", // Merged + MD5Sum: "test-md5-2", + Size: 2048, + }, + { + FileName: "test-incr-3000-3999.tar.lz4", // Currently downloading + MD5Sum: "test-md5-3", + Size: 3072, + }, + { + FileName: "test-incr-4000-4999.tar.lz4", // Currently merging + MD5Sum: "test-md5-4", + Size: 4096, + }, + { + FileName: "test-incr-5000-5999.tar.lz4", // New file + MD5Sum: "test-md5-5", + Size: 5120, + }, + } + + // Directly test parseFileInfo and file status management logic + files, err := downloader.parseFileInfo(metadata) + require.NoError(t, err) + assert.Len(t, files, 5) + + // Manually test file status filtering logic + downloadedFiles, err := downloader.loadDownloadedFiles() + require.NoError(t, err) + mergedFiles, err := downloader.loadMergedFiles() + require.NoError(t, err) + toDownloadFiles, err := downloader.loadToDownloadFiles() + require.NoError(t, err) + toMergeFiles, err := downloader.loadToMergeFiles() + require.NoError(t, err) + + // Create lookup sets + downloadedSet := make(map[string]bool) + for _, file := range downloadedFiles { + downloadedSet[file] = true + } + mergedSet := make(map[string]bool) + for _, file := range mergedFiles { + mergedSet[file] = true + } + toDownloadSet := make(map[string]bool) + for _, file := range toDownloadFiles { + toDownloadSet[file] = true + } + toMergeSet := make(map[string]bool) + for _, file := range toMergeFiles { + toMergeSet[file] = true + } + + // Verify file status filtering + var newToDownloadFiles []*IncrFileInfo + for _, file := range files { + fileName := file.Metadata.FileName + if !downloadedSet[fileName] && !mergedSet[fileName] && !toDownloadSet[fileName] && !toMergeSet[fileName] { + newToDownloadFiles = append(newToDownloadFiles, file) + } + } + + // Only new files should be in download queue + assert.Len(t, newToDownloadFiles, 1) + assert.Equal(t, "test-incr-5000-5999.tar.lz4", newToDownloadFiles[0].Metadata.FileName) +} + +func TestIncrDownloader_QueueForMerge(t *testing.T) { + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, testURL, tempDir, 1000) + + // Create test file + file := &IncrFileInfo{ + Metadata: IncrMetadata{ + FileName: "test-incr-1000-1999.tar.lz4", + MD5Sum: "test-md5", + Size: 1024, + }, + StartBlock: 1000, + EndBlock: 1999, + LocalPath: filepath.Join(tempDir, "test-incr-1000-1999.tar.lz4"), + } + + // Test adding to merge queue + downloader.queueForMerge(file) + + // Verify file added to merge queue + toMergeFiles, err := downloader.loadToMergeFiles() + require.NoError(t, err) + assert.Contains(t, toMergeFiles, file.Metadata.FileName) + + // Verify file added to memory map + downloader.mergeMutex.Lock() + assert.Contains(t, downloader.downloadedFilesMap, uint64(1000)) + assert.Equal(t, file, downloader.downloadedFilesMap[1000]) + downloader.mergeMutex.Unlock() + + // Test duplicate addition (should be ignored) + downloader.queueForMerge(file) + toMergeFiles, err = downloader.loadToMergeFiles() + require.NoError(t, err) + // Should have only one entry + count := 0 + for _, f := range toMergeFiles { + if f == file.Metadata.FileName { + count++ + } + } + assert.Equal(t, 1, count) +} + +func TestIncrDownloader_GetFileStatusStats(t *testing.T) { + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, testURL, tempDir, 1000) + + // Set some test data + downloader.totalFiles = 5 + downloader.downloadedFiles = 2 + downloader.mergedFiles = 1 + downloader.expectedNextBlockStart = 1000 + + err := downloader.saveDownloadedFiles([]string{"file1.tar.lz4", "file2.tar.lz4"}) + assert.NoError(t, err) + err = downloader.saveMergedFiles([]string{"file3.tar.lz4"}) + assert.NoError(t, err) + err = downloader.saveToDownloadFiles([]string{"file4.tar.lz4"}) + assert.NoError(t, err) + err = downloader.saveToMergeFiles([]string{"file5.tar.lz4"}) + assert.NoError(t, err) + + // Get statistics + stats := downloader.GetFileStatusStats() + + // Verify statistics + assert.Equal(t, 5, stats["totalFiles"]) + assert.Equal(t, 2, stats["downloadedFiles"]) + assert.Equal(t, 1, stats["mergedFiles"]) + assert.Equal(t, 1, stats["toDownloadFiles"]) + assert.Equal(t, 1, stats["toMergeFiles"]) + assert.Equal(t, uint64(1000), stats["expectedNextBlockStart"]) +} + +func TestIncrDownloader_ParseFileInfo(t *testing.T) { + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, testURL, tempDir, 1000) + + // Test valid metadata + metadata := []IncrMetadata{ + { + FileName: "test-incr-1000-1999.tar.lz4", + MD5Sum: "test-md5-1", + Size: 1024, + }, + { + FileName: "test-incr-2000-2999.tar.lz4", + MD5Sum: "test-md5-2", + Size: 2048, + }, + { + FileName: "test-incr-3000-3999.tar.lz4", + MD5Sum: "test-md5-3", + Size: 3072, + }, + } + + files, err := downloader.parseFileInfo(metadata) + require.NoError(t, err) + assert.Len(t, files, 3) + + // Verify file info + assert.Equal(t, "test-incr-1000-1999.tar.lz4", files[0].Metadata.FileName) + assert.Equal(t, uint64(1000), files[0].StartBlock) + assert.Equal(t, uint64(1999), files[0].EndBlock) + assert.Equal(t, filepath.Join(tempDir, "test-incr-1000-1999.tar.lz4"), files[0].LocalPath) + + // Test invalid filename format + invalidMetadata := []IncrMetadata{ + { + FileName: "invalid-filename.txt", + MD5Sum: "test-md5", + Size: 1024, + }, + } + + _, err = downloader.parseFileInfo(invalidMetadata) + assert.Error(t, err) +} + +func TestIncrDownloader_CheckFileContinuity(t *testing.T) { + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, testURL, tempDir, 1000) + + // Test continuous files + continuousFiles := []*IncrFileInfo{ + { + Metadata: IncrMetadata{FileName: "test-incr-1000-1999.tar.lz4"}, + StartBlock: 1000, + EndBlock: 1999, + }, + { + Metadata: IncrMetadata{FileName: "test-incr-2000-2999.tar.lz4"}, + StartBlock: 2000, + EndBlock: 2999, + }, + { + Metadata: IncrMetadata{FileName: "test-incr-3000-3999.tar.lz4"}, + StartBlock: 3000, + EndBlock: 3999, + }, + } + + err := downloader.checkFileContinuity(continuousFiles) + assert.NoError(t, err) + + // Test discontinuous files + discontinuousFiles := []*IncrFileInfo{ + { + Metadata: IncrMetadata{FileName: "test-incr-1000-1999.tar.lz4"}, + StartBlock: 1000, + EndBlock: 1999, + }, + { + Metadata: IncrMetadata{FileName: "test-incr-2001-3000.tar.lz4"}, + StartBlock: 2001, // Should be 2001 + EndBlock: 3000, + }, + } + + err = downloader.checkFileContinuity(discontinuousFiles) + assert.Error(t, err) +} + +func TestIncrDownloader_DatabaseOperations(t *testing.T) { + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, testURL, tempDir, 1000) + + // Test saving and loading various file lists + testFiles := []string{"file1.tar.lz4", "file2.tar.lz4", "file3.tar.lz4"} + + // Test downloaded files list + err := downloader.saveDownloadedFiles(testFiles) + require.NoError(t, err) + + loadedFiles, err := downloader.loadDownloadedFiles() + require.NoError(t, err) + assert.Equal(t, testFiles, loadedFiles) + + // Test merged files list + err = downloader.saveMergedFiles(testFiles) + require.NoError(t, err) + + mergedFiles, err := downloader.loadMergedFiles() + require.NoError(t, err) + assert.Equal(t, testFiles, mergedFiles) + + // Test to-download files list + err = downloader.saveToDownloadFiles(testFiles) + require.NoError(t, err) + + toDownloadFiles, err := downloader.loadToDownloadFiles() + require.NoError(t, err) + assert.Equal(t, testFiles, toDownloadFiles) + + // Test to-merge files list + err = downloader.saveToMergeFiles(testFiles) + require.NoError(t, err) + + toMergeFiles, err := downloader.loadToMergeFiles() + require.NoError(t, err) + assert.Equal(t, testFiles, toMergeFiles) +} + +func TestIncrDownloader_ContextCancellation(t *testing.T) { + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, testURL, tempDir, 1000) + + // Test cancellation + downloader.Cancel() + + // Verify context is cancelled + select { + case <-downloader.ctx.Done(): + // Expected behavior + default: + t.Error("Context should be cancelled") + } +} + +func TestIncrDownloader_Close(t *testing.T) { + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, testURL, tempDir, 1000) + + // Add some test data to downloaded files map + file := &IncrFileInfo{ + Metadata: IncrMetadata{FileName: "test.tar.lz4"}, + StartBlock: 1000, + EndBlock: 1999, + } + downloader.downloadedFilesMap[1000] = file + + // Test close operation + downloader.Close() + + // Verify downloaded files map is cleared + assert.Empty(t, downloader.downloadedFilesMap) +} + +// func createTestMetadataFile(t *testing.T, tempDir string, metadata []IncrMetadata) string { +// metadataPath := filepath.Join(tempDir, "incr_metadata.json") +// data, err := json.Marshal(metadata) +// require.NoError(t, err) +// +// err = os.WriteFile(metadataPath, data, 0644) +// require.NoError(t, err) +// +// return metadataPath +// } + +func TestIncrDownloader_Integration(t *testing.T) { + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, testURL, tempDir, 1000) + + metadata := []IncrMetadata{ + { + FileName: "test-incr-1000-1999.tar.lz4", + MD5Sum: "test-md5-1", + Size: 1024, + }, + { + FileName: "test-incr-2000-2999.tar.lz4", + MD5Sum: "test-md5-2", + Size: 2048, + }, + } + + // Directly test parseFileInfo + files, err := downloader.parseFileInfo(metadata) + require.NoError(t, err) + assert.Len(t, files, 2) + + // Verify initial state + assert.Equal(t, uint64(1000), files[0].StartBlock) + + // Simulate file download completion + file := files[0] + downloader.markFileAsDownloaded(file.Metadata.FileName) + downloader.queueForMerge(file) + + // Verify status updates + downloadedFiles, err := downloader.loadDownloadedFiles() + require.NoError(t, err) + assert.Contains(t, downloadedFiles, file.Metadata.FileName) + + toMergeFiles, err := downloader.loadToMergeFiles() + require.NoError(t, err) + assert.Contains(t, toMergeFiles, file.Metadata.FileName) + + // Verify memory map + downloader.mergeMutex.Lock() + assert.Contains(t, downloader.downloadedFilesMap, file.StartBlock) + downloader.mergeMutex.Unlock() + + // Simulate merge completion + downloader.markFileAsMerged(file.Metadata.FileName) + + // Verify final state + mergedFiles, err := downloader.loadMergedFiles() + require.NoError(t, err) + assert.Contains(t, mergedFiles, file.Metadata.FileName) + + toMergeFiles, err = downloader.loadToMergeFiles() + require.NoError(t, err) + assert.NotContains(t, toMergeFiles, file.Metadata.FileName) +} + +func TestIncrDownloader_ProcessFileStatus(t *testing.T) { + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, testURL, tempDir, 1000) + + // Create test files + files := []*IncrFileInfo{ + { + Metadata: IncrMetadata{ + FileName: "test-incr-1000-1999.tar.lz4", + MD5Sum: "test-md5-1", + Size: 1024, + }, + StartBlock: 1000, + EndBlock: 1999, + }, + { + Metadata: IncrMetadata{ + FileName: "test-incr-2000-2999.tar.lz4", + MD5Sum: "test-md5-2", + Size: 2048, + }, + StartBlock: 2000, + EndBlock: 2999, + }, + { + Metadata: IncrMetadata{ + FileName: "test-incr-3000-3999.tar.lz4", + MD5Sum: "test-md5-3", + Size: 3072, + }, + StartBlock: 3000, + EndBlock: 3999, + }, + { + Metadata: IncrMetadata{ + FileName: "test-incr-4000-4999.tar.lz4", + MD5Sum: "test-md5-4", + Size: 4096, + }, + StartBlock: 4000, + EndBlock: 4999, + }, + } + + // Test different file status scenarios + testCases := []struct { + name string + downloadedFiles []string + mergedFiles []string + toDownloadFiles []string + toMergeFiles []string + expectedToDownloadCount int + expectedToMergeCount int + expectedTotalFiles int + expectedNextBlockStart uint64 + }{ + { + name: "All new files", + downloadedFiles: []string{}, + mergedFiles: []string{}, + toDownloadFiles: []string{}, + toMergeFiles: []string{}, + expectedToDownloadCount: 4, + expectedToMergeCount: 0, + expectedTotalFiles: 4, + expectedNextBlockStart: 1000, + }, + { + name: "Some files already downloaded", + downloadedFiles: []string{"test-incr-1000-1999.tar.lz4"}, + mergedFiles: []string{}, + toDownloadFiles: []string{}, + toMergeFiles: []string{}, + expectedToDownloadCount: 3, + expectedToMergeCount: 1, + expectedTotalFiles: 3, + expectedNextBlockStart: 1000, + }, + { + name: "Some files already merged", + downloadedFiles: []string{}, + mergedFiles: []string{"test-incr-1000-1999.tar.lz4"}, + toDownloadFiles: []string{}, + toMergeFiles: []string{}, + expectedToDownloadCount: 3, + expectedToMergeCount: 0, + expectedTotalFiles: 3, + expectedNextBlockStart: 2000, + }, + { + name: "Some files currently downloading", + downloadedFiles: []string{}, + mergedFiles: []string{}, + toDownloadFiles: []string{"test-incr-1000-1999.tar.lz4"}, + toMergeFiles: []string{}, + expectedToDownloadCount: 4, // Including the one currently downloading + expectedToMergeCount: 0, + expectedTotalFiles: 4, + expectedNextBlockStart: 1000, + }, + { + name: "Some files currently merging", + downloadedFiles: []string{}, + mergedFiles: []string{}, + toDownloadFiles: []string{}, + toMergeFiles: []string{"test-incr-1000-1999.tar.lz4"}, + expectedToDownloadCount: 3, + expectedToMergeCount: 1, // Including the one currently merging + expectedTotalFiles: 3, + expectedNextBlockStart: 1000, + }, + { + name: "Mixed statuses", + downloadedFiles: []string{"test-incr-2000-2999.tar.lz4"}, + mergedFiles: []string{"test-incr-1000-1999.tar.lz4"}, + toDownloadFiles: []string{"test-incr-3000-3999.tar.lz4"}, + toMergeFiles: []string{}, + expectedToDownloadCount: 2, + expectedToMergeCount: 1, + expectedTotalFiles: 2, + expectedNextBlockStart: 2000, + }, + { + name: "All files already merged", + downloadedFiles: []string{}, + mergedFiles: []string{"test-incr-1000-1999.tar.lz4", "test-incr-2000-2999.tar.lz4", "test-incr-3000-3999.tar.lz4", "test-incr-4000-4999.tar.lz4"}, + toDownloadFiles: []string{}, + toMergeFiles: []string{}, + expectedToDownloadCount: 0, + expectedToMergeCount: 0, + expectedTotalFiles: 0, + expectedNextBlockStart: 0, + }, + { + name: "All files currently downloading", + downloadedFiles: []string{}, + mergedFiles: []string{}, + toDownloadFiles: []string{"test-incr-1000-1999.tar.lz4", "test-incr-2000-2999.tar.lz4", "test-incr-3000-3999.tar.lz4", "test-incr-4000-4999.tar.lz4"}, + toMergeFiles: []string{}, + expectedToDownloadCount: 4, + expectedToMergeCount: 0, + expectedTotalFiles: 4, + expectedNextBlockStart: 1000, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Reset downloader state + downloader.downloadedFiles = 0 + downloader.files = nil + downloader.totalFiles = 0 + downloader.downloadedFilesMap = make(map[uint64]*IncrFileInfo) + downloader.expectedNextBlockStart = 0 + + // Pre-set database state for this test case + if len(tc.toDownloadFiles) > 0 { + err := downloader.saveToDownloadFiles(tc.toDownloadFiles) + require.NoError(t, err) + } + if len(tc.downloadedFiles) > 0 { + err := downloader.saveDownloadedFiles(tc.downloadedFiles) + require.NoError(t, err) + } + if len(tc.toMergeFiles) > 0 { + err := downloader.saveToMergeFiles(tc.toMergeFiles) + require.NoError(t, err) + } + if len(tc.mergedFiles) > 0 { + err := downloader.saveMergedFiles(tc.mergedFiles) + require.NoError(t, err) + } + + // Test processFileStatus + err := downloader.processFileStatus(files) + + require.NoError(t, err) + assert.Len(t, downloader.files, tc.expectedToDownloadCount) + assert.Equal(t, tc.expectedTotalFiles, downloader.totalFiles) + assert.Equal(t, tc.expectedNextBlockStart, downloader.expectedNextBlockStart) + + // Verify downloaded files map + assert.Len(t, downloader.downloadedFilesMap, tc.expectedToMergeCount) + + // Verify database state + toDownloadFiles, err := downloader.loadToDownloadFiles() + require.NoError(t, err) + assert.Len(t, toDownloadFiles, tc.expectedToDownloadCount) + + toMergeFiles, err := downloader.loadToMergeFiles() + require.NoError(t, err) + assert.Len(t, toMergeFiles, tc.expectedToMergeCount) + + // Verify specific file assignments + if tc.expectedToMergeCount > 0 { + // Check that files are properly assigned to merge queue + for _, file := range downloader.downloadedFilesMap { + assert.Contains(t, toMergeFiles, file.Metadata.FileName) + } + } + + // Clean up database state for next test + err = downloader.saveDownloadedFiles([]string{}) + assert.NoError(t, err) + err = downloader.saveMergedFiles([]string{}) + assert.NoError(t, err) + err = downloader.saveToDownloadFiles([]string{}) + assert.NoError(t, err) + err = downloader.saveToMergeFiles([]string{}) + assert.NoError(t, err) + }) + } +} + +func TestIncrDownloader_ProcessFileStatus_EmptyFiles(t *testing.T) { + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, testURL, tempDir, 1000) + + // Test with empty files list + files := []*IncrFileInfo{} + err := downloader.processFileStatus(files) + require.NoError(t, err) + assert.Len(t, downloader.files, 0) + assert.Equal(t, 0, downloader.totalFiles) + assert.Equal(t, uint64(0), downloader.expectedNextBlockStart) +} + +func TestIncrDownloader_ProcessFileStatus_BlockOrdering(t *testing.T) { + db := createTestDB() + defer db.Close() + trieDB := triedb.NewDatabase(db, nil) + defer trieDB.Close() + + tempDir := t.TempDir() + downloader := NewIncrDownloader(db, trieDB, testURL, tempDir, 1000) + + // Create test files with non-sequential block numbers + files := []*IncrFileInfo{ + { + Metadata: IncrMetadata{ + FileName: "test-incr-3000-3999.tar.lz4", + MD5Sum: "test-md5-3", + Size: 3072, + }, + StartBlock: 3000, + EndBlock: 3999, + }, + { + Metadata: IncrMetadata{ + FileName: "test-incr-1000-1999.tar.lz4", + MD5Sum: "test-md5-1", + Size: 1024, + }, + StartBlock: 1000, + EndBlock: 1999, + }, + { + Metadata: IncrMetadata{ + FileName: "test-incr-2000-2999.tar.lz4", + MD5Sum: "test-md5-2", + Size: 2048, + }, + StartBlock: 2000, + EndBlock: 2999, + }, + } + + // Set some files as downloaded + err := downloader.saveDownloadedFiles([]string{"test-incr-1000-1999.tar.lz4", "test-incr-2000-2999.tar.lz4"}) + assert.NoError(t, err) + + // Test processFileStatus + err = downloader.processFileStatus(files) + require.NoError(t, err) + + // Verify that the earliest block start is used for expectedNextBlockStart + assert.Equal(t, uint64(1000), downloader.expectedNextBlockStart) + + // Verify that both downloaded files are in the merge queue + assert.Len(t, downloader.downloadedFilesMap, 2) + assert.Contains(t, downloader.downloadedFilesMap, uint64(1000)) + assert.Contains(t, downloader.downloadedFilesMap, uint64(2000)) +} diff --git a/core/incr_merger.go b/core/incr_merger.go new file mode 100644 index 0000000000..baf903323c --- /dev/null +++ b/core/incr_merger.go @@ -0,0 +1,242 @@ +package core + +import ( + "errors" + "fmt" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/ethdb/pebble" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/triedb" +) + +// MergeIncrSnapshot merges the incremental snapshot into local data. +func MergeIncrSnapshot(chainDB ethdb.Database, trieDB *triedb.Database, incrPath string) error { + var wg sync.WaitGroup + errChan := make(chan error, 3) + + if err := updateGenesisMeta(chainDB); err != nil { + return err + } + + // merge incremental state data + wg.Add(1) + go func() { + defer wg.Done() + if err := trieDB.MergeIncrState(incrPath); err != nil { + log.Error("Failed to merge incremental state data", "path", incrPath, "err", err) + errChan <- fmt.Errorf("failed to merge incremental state data: %v", err) + } + }() + + // merge incremental block data + wg.Add(1) + go func() { + defer wg.Done() + if err := mergeIncrBlock(incrPath, chainDB); err != nil { + log.Error("Failed to merge incremental block data", "path", incrPath, "err", err) + errChan <- fmt.Errorf("failed to merge incremental block data: %v", err) + } + }() + + // merge contract codes + wg.Add(1) + go func() { + defer wg.Done() + if err := mergeIncrKV(incrPath, chainDB); err != nil { + log.Error("Failed to merge incremental contract codes", "path", incrPath, "err", err) + errChan <- fmt.Errorf("failed to merge incremental contract codes: %v", err) + } + }() + + go func() { + wg.Wait() + close(errChan) + }() + + var mergeErrors []error + for err := range errChan { + mergeErrors = append(mergeErrors, err) + } + if len(mergeErrors) > 0 { + errs := errors.Join(mergeErrors...) + log.Error("Parallel merge operations failed", "total_errors", len(mergeErrors), "path", incrPath) + return errs + } + + log.Info("All merge operations completed successfully", "path", incrPath) + return nil +} + +func mergeIncrBlock(incrDir string, chainDB ethdb.Database) error { + incrChainFreezer, err := rawdb.OpenIncrChainFreezer(incrDir, true) + if err != nil { + log.Error("Failed to open incremental chain freezer", "err", err) + return err + } + defer incrChainFreezer.Close() + + // Get chain config from database + chainConfig, err := rawdb.GetChainConfig(chainDB) + if err != nil { + log.Error("Failed to get chain config", "err", err) + return err + } + + incrAncients, _ := incrChainFreezer.Ancients() + tail, _ := incrChainFreezer.Tail() + + // delete old block data in pebble + if err = chainDB.CleanBlock(chainDB, tail); err != nil { + log.Error("Failed to force freeze to ancients", "err", err) + return err + } + + baseHead, _ := chainDB.Ancients() + if tail == baseHead && baseHead <= incrAncients { + for number := tail; number < incrAncients-1; number++ { + hashBytes, header, body, receipts, td, err := rawdb.ReadIncrBlock(incrChainFreezer, number) + if err != nil { + log.Error("Failed to read incremental block", "block", number, "err", err) + return err + } + + var h types.Header + if err = rlp.DecodeBytes(header, &h); err != nil { + log.Error("Failed to decode header", "block", number, "err", err) + return err + } + // Check if Cancun hardfork is active for this block + isCancunActive := chainConfig.IsCancun(h.Number, h.Time) + var sidecars rlp.RawValue + if isCancunActive { + sidecars, err = rawdb.ReadIncrChainBlobSideCars(incrChainFreezer, number) + if err != nil { + log.Error("Failed to read increment chain blob side car", "block", number, "err", err) + return err + } + } + + blockBatch := chainDB.NewBatch() + hash := common.BytesToHash(hashBytes) + rawdb.WriteCanonicalHash(blockBatch, hash, number) + rawdb.WriteTdRLP(blockBatch, hash, number, td) + rawdb.WriteBodyRLP(blockBatch, hash, number, body) + rawdb.WriteHeaderRLP(blockBatch, hash, number, header) + rawdb.WriteRawReceipts(blockBatch, hash, number, receipts) + if isCancunActive { + rawdb.WriteBlobSidecarsRLP(blockBatch, hash, number, sidecars) + } + if err = blockBatch.Write(); err != nil { + log.Error("Failed to batch commit block data", "err", err) + return err + } + } + } else { + log.Crit("There are block data gap", "tail", tail, "baseHead", baseHead) + } + + if err = rawdb.FinalizeIncrementalMerge(chainDB, incrChainFreezer, chainConfig, incrAncients-1); err != nil { + log.Error("Failed to finalize incremental data merge", "err", err) + return err + } + + log.Info("Finished merging incremental block data", "merged_number", incrAncients-baseHead) + return nil +} + +// mergeIncrKV merges incr kv: contract codes, parlia snapshot, chain config and genesis state spec.. +func mergeIncrKV(incrDir string, chainDB ethdb.Database) error { + newDB, err := pebble.New(incrDir, 10, 10, "incremental", true) + if err != nil { + log.Error("Failed to open pebble to read incremental data", "err", err) + return err + } + defer newDB.Close() + + it := newDB.NewIterator(rawdb.CodePrefix, nil) + defer it.Release() + + codeCount := 0 + for it.Next() { + key := it.Key() + value := it.Value() + + isCode, hashBytes := rawdb.IsCodeKey(key) + if !isCode { + log.Warn("Invalid code key found", "key", fmt.Sprintf("%x", key)) + continue + } + + codeHash := common.BytesToHash(hashBytes) + if rawdb.HasCodeWithPrefix(chainDB, codeHash) { + log.Debug("Code already exists, skipping", "hash", codeHash.Hex()) + continue + } + rawdb.WriteCode(chainDB, codeHash, value) + codeCount++ + } + + if err = it.Error(); err != nil { + log.Error("Iterator error while reading contract codes", "err", err) + return err + } + + // Merge Parlia snapshots from incremental snapshot to local data + if err = mergeParliaSnapshots(chainDB, newDB); err != nil { + log.Error("Failed to merge Parlia snapshots", "err", err) + return err + } + + if err = updateGenesisMeta(chainDB); err != nil { + log.Error("Failed to merge genesis meta data", "err", err) + return err + } + + log.Info("Complete merging contract codes", "total", codeCount) + return nil +} + +// mergeParliaSnapshots merges Parlia consensus snapshots from incremental snapshot into local data +func mergeParliaSnapshots(chainDB ethdb.Database, incrKV *pebble.Database) error { + log.Info("Starting Parlia snapshots import from incremental snapshot") + iter := incrKV.NewIterator(rawdb.ParliaSnapshotPrefix, nil) + defer iter.Release() + + count := 0 + for iter.Next() { + key := iter.Key() + value := iter.Value() + if err := chainDB.Put(key, value); err != nil { + log.Error("Failed to store Parlia snapshot in main DB", "key", common.Bytes2Hex(key), "err", err) + return err + } + count++ + } + + if iter.Error() != nil { + log.Error("Failed to iterate Parlia snapshot", "error", iter.Error()) + return iter.Error() + } + + log.Info("Completed Parlia snapshots merging", "total_snapshots", count) + return nil +} + +// updateGenesisMeta updates base snapshot chain config +func updateGenesisMeta(chainDB ethdb.Database) error { + stored := rawdb.ReadCanonicalHash(chainDB, 0) + if (stored == common.Hash{}) { + return fmt.Errorf("invalid genesis hash in database: %x", stored) + } + + builtInConf := params.GetBuiltInChainConfig(stored) + rawdb.WriteChainConfig(chainDB, stored, builtInConf) + return nil +} diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index 758275adc8..e303f62ea2 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -418,6 +418,18 @@ func WriteHeader(db ethdb.KeyValueWriter, header *types.Header) { } } +// WriteHeaderRLP stores a RLP encoded block header into the database and also stores the +// hash-to-number mapping. +func WriteHeaderRLP(db ethdb.KeyValueWriter, hash common.Hash, number uint64, header rlp.RawValue) { + // Write the hash -> number mapping + WriteHeaderNumber(db, hash, number) + + key := headerKey(number, hash) + if err := db.Put(key, header); err != nil { + log.Crit("Failed to store header", "err", err) + } +} + // DeleteHeader removes all block header data associated with a hash. func DeleteHeader(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { deleteHeaderWithoutNumber(db, hash, number) @@ -576,6 +588,13 @@ func WriteTd(db ethdb.KeyValueWriter, hash common.Hash, number uint64, td *big.I } } +// WriteTd stores the rlp encoded total difficulty of a block into the database. +func WriteTdRLP(db ethdb.KeyValueWriter, hash common.Hash, number uint64, td rlp.RawValue) { + if err := db.Put(headerTDKey(number, hash), td); err != nil { + log.Crit("Failed to store block total difficulty", "err", err) + } +} + // DeleteTd removes all block total difficulty data associated with a hash. func DeleteTd(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { if err := db.Delete(headerTDKey(number, hash)); err != nil { @@ -913,6 +932,14 @@ func ReadBlobSidecars(db ethdb.Reader, hash common.Hash, number uint64) types.Bl return ret } +// WriteBlobSidecarsRLP stores all the RLP encoded transaction blobs belonging to a block. +// It could input nil for empty blobs. +func WriteBlobSidecarsRLP(db ethdb.KeyValueWriter, hash common.Hash, number uint64, blobs rlp.RawValue) { + if err := db.Put(blockBlobSidecarsKey(number, hash), blobs); err != nil { + log.Crit("Failed to store block blobs", "err", err) + } +} + // WriteBlobSidecars stores all the transaction blobs belonging to a block. // It could input nil for empty blobs. func WriteBlobSidecars(db ethdb.KeyValueWriter, hash common.Hash, number uint64, blobs types.BlobSidecars) { diff --git a/core/rawdb/accessors_increment.go b/core/rawdb/accessors_increment.go new file mode 100644 index 0000000000..a75926a509 --- /dev/null +++ b/core/rawdb/accessors_increment.go @@ -0,0 +1,435 @@ +package rawdb + +import ( + "encoding/binary" + "errors" + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" +) + +// ContractCode represents a contract code with associated metadata. +type ContractCode struct { + Hash common.Hash // hash is the cryptographic hash of the contract code. + Blob []byte // blob is the binary representation of the contract code. +} + +// IncrStateMetadata represents metadata for incremental state data. +type IncrStateMetadata struct { + Root common.Hash + HasStates bool + NodeCount uint64 + StateCount uint64 + Layers uint64 + StateIDArray [2]uint64 + BlockNumberArray [2]uint64 +} + +// WriteIncrState writes the provided state data into the database. +// Compute the position of state history in freezer by minus one since the id of first state +// history starts from one(zero for initial state). +func WriteIncrTrieNodes(db ethdb.AncientWriter, id uint64, meta, trieNodes []byte) error { + _, err := db.ModifyAncients(func(op ethdb.AncientWriteOp) error { + if err := op.AppendRaw(incrStateHistoryMeta, id-1, meta); err != nil { + return err + } + if err := op.AppendRaw(incrStateHistoryTrieNodesData, id-1, trieNodes); err != nil { + return err + } + if err := op.AppendRaw(incrStateHistoryStatesData, id-1, []byte{}); err != nil { + return err + } + return nil + }) + return err +} + +// WriteIncrState writes the provided state data into the database. +// Compute the position of state history in freezer by minus one since the id of first state +// history starts from one(zero for initial state). +func WriteIncrState(db ethdb.AncientWriter, id uint64, meta, states []byte) error { + _, err := db.ModifyAncients(func(op ethdb.AncientWriteOp) error { + if err := op.AppendRaw(incrStateHistoryMeta, id-1, meta); err != nil { + return err + } + if err := op.AppendRaw(incrStateHistoryTrieNodesData, id-1, []byte{}); err != nil { + return err + } + if err := op.AppendRaw(incrStateHistoryStatesData, id-1, states); err != nil { + return err + } + return nil + }) + return err +} + +// ReadIncrStateTrieNodes retrieves the trie nodes corresponding to the specified +// state history. Compute the position of state history in freezer by minus one +// since the id of first state history starts from one(zero for initial state). +func ReadIncrStateTrieNodes(db ethdb.AncientReaderOp, id uint64) ([]byte, error) { + blob, err := db.Ancient(incrStateHistoryTrieNodesData, id-1) + if err != nil { + return nil, err + } + return blob, nil +} + +// ReadIncrStatesData retrieves the states corresponding to the specified +// state history. Compute the position of state history in freezer by minus one +// since the id of first state history starts from one(zero for initial state). +func ReadIncrStatesData(db ethdb.AncientReaderOp, id uint64) ([]byte, error) { + blob, err := db.Ancient(incrStateHistoryStatesData, id-1) + if err != nil { + return nil, err + } + return blob, nil +} + +// WriteIncrBlockData writes the provided block data to the database. +func WriteIncrBlockData(db ethdb.AncientWriter, number, stateID uint64, hash, header, body, receipts, td, sidecars []byte, + isEmptyBlock, isCancun bool) error { + _, err := db.ModifyAncients(func(op ethdb.AncientWriteOp) error { + if err := op.AppendRaw(ChainFreezerHashTable, number, hash); err != nil { + return err + } + if err := op.AppendRaw(ChainFreezerHeaderTable, number, header); err != nil { + return err + } + if err := op.AppendRaw(ChainFreezerBodiesTable, number, body); err != nil { + return err + } + if err := op.AppendRaw(ChainFreezerReceiptTable, number, receipts); err != nil { + return err + } + if err := op.AppendRaw(ChainFreezerDifficultyTable, number, td); err != nil { + return err + } + if err := op.AppendRaw(IncrBlockStateIDMappingTable, number, encodeBlockNumber(stateID)); err != nil { + return err + } + if err := op.AppendRaw(IncrEmptyBlockTable, number, boolToBytes(isEmptyBlock)); err != nil { + return err + } + if isCancun { + if err := op.AppendRaw(ChainFreezerBlobSidecarTable, number, sidecars); err != nil { + return err + } + } + return nil + }) + return err +} + +// ReadIncrBlock read the block data with the provided block number. +func ReadIncrBlock(db ethdb.AncientReaderOp, number uint64) ([]byte, []byte, []byte, []byte, []byte, error) { + hashBytes, err := ReadIncrChainHash(db, number) + if err != nil { + return nil, nil, nil, nil, nil, err + } + header, err := ReadIncrChainHeader(db, number) + if err != nil { + return nil, nil, nil, nil, nil, err + } + body, err := ReadIncrChainBodies(db, number) + if err != nil { + return nil, nil, nil, nil, nil, err + } + receipts, err := ReadIncrChainReceipts(db, number) + if err != nil { + return nil, nil, nil, nil, nil, err + } + td, err := ReadIncrChainDifficulty(db, number) + if err != nil { + return nil, nil, nil, nil, nil, err + } + return hashBytes, header, body, receipts, td, nil +} + +// FinalizeIncrementalMerge is ued to write last block data from incremental db into blockchain db. +// Blockchain metadata: head block, head hash, canonical hash, etc. +func FinalizeIncrementalMerge(db ethdb.Database, incrChainFreezer ethdb.AncientReaderOp, chainConfig *params.ChainConfig, + number uint64) error { + hashBytes, header, body, receipts, td, err := ReadIncrBlock(incrChainFreezer, number) + if err != nil { + log.Error("Failed to read incremental block", "block", number, "error", err) + return err + } + hash := common.BytesToHash(hashBytes) + + var h types.Header + if err = rlp.DecodeBytes(header, &h); err != nil { + log.Error("Failed to decode header", "block", number, "error", err) + return err + } + isCancunActive := chainConfig.IsCancun(h.Number, h.Time) + + var sidecars rlp.RawValue + if isCancunActive { + sidecars, err = ReadIncrChainBlobSideCars(incrChainFreezer, number) + if err != nil { + log.Error("Failed to read increment chain blob side car", "block", number, "error", err) + return err + } + } + + blockBatch := db.NewBatch() + + // write block data + WriteTdRLP(blockBatch, hash, number, td) + WriteBodyRLP(blockBatch, hash, number, body) + WriteHeaderRLP(blockBatch, hash, number, header) + WriteRawReceipts(blockBatch, hash, number, receipts) + if isCancunActive { + WriteBlobSidecarsRLP(blockBatch, hash, number, sidecars) + } + + // update blockchain metadata + WriteCanonicalHash(blockBatch, hash, number) + WriteHeadBlockHash(blockBatch, hash) + WriteHeadHeaderHash(blockBatch, hash) + WriteHeadFastBlockHash(blockBatch, hash) + WriteFinalizedBlockHash(blockBatch, hash) + if err = blockBatch.Write(); err != nil { + log.Error("Failed to update block metadata into disk", "error", err) + return err + } + + return nil +} + +// ReadIncrChainHash retrieves the incremental hash history from the database with the provided block number. +func ReadIncrChainHash(db ethdb.AncientReaderOp, number uint64) ([]byte, error) { + blob, err := db.Ancient(ChainFreezerHashTable, number) + if err != nil { + return nil, err + } + return blob, nil +} + +// ReadIncrChainHeader retrieves the incremental header history from the database with the provided block number. +func ReadIncrChainHeader(db ethdb.AncientReaderOp, number uint64) ([]byte, error) { + blob, err := db.Ancient(ChainFreezerHeaderTable, number) + if err != nil { + return nil, err + } + return blob, nil +} + +// ReadIncrChainBodies retrieves the incremental bodies history from the database with the provided block number. +func ReadIncrChainBodies(db ethdb.AncientReaderOp, number uint64) ([]byte, error) { + blob, err := db.Ancient(ChainFreezerBodiesTable, number) + if err != nil { + return nil, err + } + return blob, nil +} + +// ReadIncrChainReceipts retrieves the incremental receipts history from the database with the provided block number. +func ReadIncrChainReceipts(db ethdb.AncientReaderOp, number uint64) ([]byte, error) { + blob, err := db.Ancient(ChainFreezerReceiptTable, number) + if err != nil { + return nil, err + } + return blob, nil +} + +// ReadIncrChainDifficulty retrieves the incremental difficulty history from the database with the provided block number. +func ReadIncrChainDifficulty(db ethdb.AncientReaderOp, number uint64) ([]byte, error) { + blob, err := db.Ancient(ChainFreezerDifficultyTable, number) + if err != nil { + return nil, err + } + return blob, nil +} + +// ReadIncrChainBlobSideCars retrieves the incremental blob history from the database with the provided block number. +func ReadIncrChainBlobSideCars(db ethdb.AncientReaderOp, number uint64) ([]byte, error) { + blobs, err := db.Ancient(ChainFreezerBlobSidecarTable, number) + if err != nil { + return nil, err + } + return blobs, nil +} + +// ReadIncrChainMapping retrieves the state id from the incremental database with the provided block number. +func ReadIncrChainMapping(db ethdb.AncientReaderOp, number uint64) (uint64, error) { + blob, err := db.Ancient(IncrBlockStateIDMappingTable, number) + if err != nil { + return 0, err + } + id := binary.BigEndian.Uint64(blob) + return id, nil +} + +// ReadIncrStateHistoryMeta retrieves the incremental metadata corresponding to the +// specified state history. Compute the position of state history in freezer by minus +// one since the id of first state history starts from one(zero for initial state). +func ReadIncrStateHistoryMeta(db ethdb.AncientReaderOp, id uint64) *IncrStateMetadata { + blob, err := db.Ancient(incrStateHistoryMeta, id-1) + if err != nil { + return nil + } + m := new(IncrStateMetadata) + if err = rlp.DecodeBytes(blob, m); err != nil { + log.Error("Failed to decode incr state history", "error", err) + return nil + } + return m +} + +// ResetEmptyIncrChainTable resets the empty incremental chain table to the new start point. +func ResetEmptyIncrChainTable(db ethdb.AncientWriter, next uint64, isCancun bool) error { + if err := db.ResetTable(ChainFreezerHeaderTable, next, true); err != nil { + return err + } + if err := db.ResetTable(ChainFreezerHashTable, next, true); err != nil { + return err + } + if err := db.ResetTable(ChainFreezerBodiesTable, next, true); err != nil { + return err + } + if err := db.ResetTable(ChainFreezerReceiptTable, next, true); err != nil { + return err + } + if err := db.ResetTable(ChainFreezerDifficultyTable, next, true); err != nil { + return err + } + if err := db.ResetTable(IncrBlockStateIDMappingTable, next, true); err != nil { + return err + } + if err := db.ResetTable(IncrEmptyBlockTable, next, true); err != nil { + return err + } + if isCancun { + if err := db.ResetTable(ChainFreezerBlobSidecarTable, next, true); err != nil { + return err + } + } + return nil +} + +// ResetChainTable resets the chain table to the new start point. +// It's used in merging the incremental snapshot case. +func ResetChainTable(db ethdb.AncientWriter, next uint64, isCancun bool) error { + if err := db.ResetTableForIncr(ChainFreezerHeaderTable, next, true); err != nil { + return err + } + if err := db.ResetTableForIncr(ChainFreezerHashTable, next, true); err != nil { + return err + } + if err := db.ResetTableForIncr(ChainFreezerBodiesTable, next, true); err != nil { + return err + } + if err := db.ResetTableForIncr(ChainFreezerReceiptTable, next, true); err != nil { + return err + } + if err := db.ResetTableForIncr(ChainFreezerDifficultyTable, next, true); err != nil { + return err + } + if isCancun { + if err := db.ResetTableForIncr(ChainFreezerBlobSidecarTable, next, true); err != nil { + return err + } + } + return nil +} + +// ResetStateTableToNewStartPoint resets the entire state tables and sets a new start point for an empty state freezer. +func ResetStateTableToNewStartPoint(db ethdb.ResettableAncientStore, startPoint uint64) error { + if err := db.Reset(); err != nil { + log.Error("Failed to reset state freezer", "error", err) + return err + } + + if err := db.ResetTableForIncr(stateHistoryMeta, startPoint, true); err != nil { + return err + } + if err := db.ResetTableForIncr(stateHistoryAccountIndex, startPoint, true); err != nil { + return err + } + if err := db.ResetTableForIncr(stateHistoryStorageIndex, startPoint, true); err != nil { + return err + } + if err := db.ResetTableForIncr(stateHistoryAccountData, startPoint, true); err != nil { + return err + } + if err := db.ResetTableForIncr(stateHistoryStorageData, startPoint, true); err != nil { + return err + } + + log.Info("Successfully set state freezer start point", "startPoint", startPoint) + return nil +} + +// GetChainConfig reads chain config from db. +func GetChainConfig(db ethdb.Reader) (*params.ChainConfig, error) { + genesisHash := ReadCanonicalHash(db, 0) + if genesisHash == (common.Hash{}) { + return nil, errors.New("genesis hash not found") + } + + chainConfig := ReadChainConfig(db, genesisHash) + if chainConfig == nil { + return nil, errors.New("chain config not found") + } + + return chainConfig, nil +} + +// CheckIncrSnapshotComplete check the incr snapshot is complete for force kill or graceful kill +// True is graceful kill, false is force kill. +func CheckIncrSnapshotComplete(incrDir string) (bool, error) { + cf, err := OpenIncrChainFreezer(incrDir, true) + if err != nil { + if strings.Contains(err.Error(), "garbage data bytes") { + return false, nil + } + return false, err + } + defer cf.Close() + sf, err := OpenIncrStateFreezer(incrDir, true) + if err != nil { + if strings.Contains(err.Error(), "garbage data bytes") { + return false, nil + } + return false, err + } + defer sf.Close() + + chainAncients, err := cf.Ancients() + if err != nil { + return false, err + } + stateAncients, err := sf.Ancients() + if err != nil { + return false, err + } + if chainAncients == 0 || stateAncients == 0 { + return false, nil + } + + // Read last state metadata + m := ReadIncrStateHistoryMeta(sf, stateAncients) + if m == nil { + return false, fmt.Errorf("last incr state history not found: %d", stateAncients) + } + + if chainAncients-1 != m.BlockNumberArray[1] { + return false, nil + } + return true, nil +} + +func boolToBytes(b bool) []byte { + buf := make([]byte, 1) + if b { + buf[0] = 1 + } + return buf +} diff --git a/core/rawdb/ancient_scheme.go b/core/rawdb/ancient_scheme.go index 6620e9bd4c..2218836058 100644 --- a/core/rawdb/ancient_scheme.go +++ b/core/rawdb/ancient_scheme.go @@ -17,6 +17,7 @@ package rawdb import ( + "errors" "path/filepath" "github.com/ethereum/go-ethereum/ethdb" @@ -41,6 +42,12 @@ const ( // ChainFreezerBlobSidecarTable indicates the name of the freezer total blob table. ChainFreezerBlobSidecarTable = "blobs" + + // IncrBlockStateIDMappingTable indicates the mapping table between block numbers and state IDs. + IncrBlockStateIDMappingTable = "mapping" + + // IncrEmptyBlockTable indicates the block has a state transition. + IncrEmptyBlockTable = "empty" ) // chainFreezerTableConfigs configures the settings for tables in the chain freezer. @@ -55,6 +62,19 @@ var chainFreezerTableConfigs = map[string]freezerTableConfig{ } var additionTables = []string{ChainFreezerBlobSidecarTable} +// incrChainFreezerTableConfigs configures the settings for tables in the incr chain freezer. +// Hashes and difficulties don't compress well. +var incrChainFreezerTableConfigs = map[string]freezerTableConfig{ + ChainFreezerHeaderTable: {noSnappy: false, prunable: true}, + ChainFreezerHashTable: {noSnappy: true, prunable: true}, + ChainFreezerBodiesTable: {noSnappy: false, prunable: true}, + ChainFreezerReceiptTable: {noSnappy: false, prunable: true}, + ChainFreezerDifficultyTable: {noSnappy: true, prunable: true}, + ChainFreezerBlobSidecarTable: {noSnappy: false, prunable: true}, + IncrBlockStateIDMappingTable: {noSnappy: false, prunable: true}, // block number -> state id + IncrEmptyBlockTable: {noSnappy: false, prunable: true}, +} + // freezerTableConfig contains the settings for a freezer table. type freezerTableConfig struct { noSnappy bool // disables item compression @@ -71,6 +91,11 @@ const ( stateHistoryStorageIndex = "storage.index" stateHistoryAccountData = "account.data" stateHistoryStorageData = "storage.data" + + // indicates the name of the freezer incremental state history table. + incrStateHistoryMeta = "incrhistory.meta" + incrStateHistoryTrieNodesData = "trienodes.data" + incrStateHistoryStatesData = "states.data" ) // stateFreezerTableConfigs configures the settings for tables in the state freezer. @@ -82,11 +107,23 @@ var stateFreezerTableConfigs = map[string]freezerTableConfig{ stateHistoryStorageData: {noSnappy: false, prunable: true}, } +var additionIncrTables = []string{ChainFreezerHeaderTable, ChainFreezerHashTable, ChainFreezerBodiesTable, ChainFreezerReceiptTable, + ChainFreezerDifficultyTable, IncrBlockStateIDMappingTable, IncrEmptyBlockTable} + +// incrStateFreezerTableConfigs configures the settings for tables in the incr state freezer. +var incrStateFreezerTableConfigs = map[string]freezerTableConfig{ + incrStateHistoryMeta: {noSnappy: true, prunable: true}, + incrStateHistoryTrieNodesData: {noSnappy: false, prunable: true}, + incrStateHistoryStatesData: {noSnappy: false, prunable: true}, +} + // The list of identifiers of ancient stores. var ( ChainFreezerName = "chain" // the folder name of chain segment ancient store. MerkleStateFreezerName = "state" // the folder name of state history ancient store. VerkleStateFreezerName = "state_verkle" // the folder name of state history ancient store. + + IncrementalPath = "incremental" // the folder name of incremental ancient store ) // freezers the collections of all builtin freezers. @@ -108,5 +145,25 @@ func NewStateFreezer(ancientDir string, verkle bool, readOnly bool) (ethdb.Reset } else { name = filepath.Join(ancientDir, MerkleStateFreezerName) } - return newResettableFreezer(name, "eth/db/state", readOnly, stateHistoryTableSize, stateFreezerTableConfigs) + return newResettableFreezer(name, "eth/db/state", readOnly, stateHistoryTableSize, stateFreezerTableConfigs, false) +} + +// OpenIncrStateFreezer opens the incremental state freezer. +func OpenIncrStateFreezer(incrStateDir string, readOnly bool) (ethdb.ResettableAncientStore, error) { + if incrStateDir == "" { + return nil, errors.New("empty incr state directory") + } + + name := filepath.Join(incrStateDir, MerkleStateFreezerName) + return newResettableFreezer(name, "eth/db/incr/state", readOnly, stateHistoryTableSize, incrStateFreezerTableConfigs, true) +} + +// OpenIncrChainFreezer opens the incremental chain freezer. +func OpenIncrChainFreezer(incrChainDir string, readOnly bool) (ethdb.ResettableAncientStore, error) { + if incrChainDir == "" { + return nil, errors.New("empty incr chain directory") + } + + name := filepath.Join(incrChainDir, ChainFreezerName) + return newResettableFreezer(name, "eth/db/incr/chain", readOnly, stateHistoryTableSize, incrChainFreezerTableConfigs, true) } diff --git a/core/rawdb/ancient_utils.go b/core/rawdb/ancient_utils.go index 422c869d8d..36df5f9f1b 100644 --- a/core/rawdb/ancient_utils.go +++ b/core/rawdb/ancient_utils.go @@ -129,6 +129,33 @@ func inspectFreezers(db ethdb.Database) ([]freezerInfo, error) { return infos, nil } +func inspectIncrFreezers(db *snapDBWrapper) ([]freezerInfo, error) { + var infos []freezerInfo + for _, freezer := range freezers { + switch freezer { + case ChainFreezerName: + info, err := inspect(ChainFreezerName, incrChainFreezerTableConfigs, db.chainFreezer) + if err != nil { + return nil, err + } + infos = append(infos, info) + + case MerkleStateFreezerName: + info, err := inspect(freezer, incrStateFreezerTableConfigs, db.stateFreezer) + if err != nil { + return nil, err + } + infos = append(infos, info) + case VerkleStateFreezerName: + continue + + default: + return nil, fmt.Errorf("unknown freezer, supported ones: %v", freezers) + } + } + return infos, nil +} + // InspectFreezerTable dumps out the index of a specific freezer table. The passed // ancient indicates the path of root ancient directory where the chain freezer can // be opened. Start and end specify the range for dumping out indexes. diff --git a/core/rawdb/chain_freezer.go b/core/rawdb/chain_freezer.go index 8fe8e5c115..6e02d63ba2 100644 --- a/core/rawdb/chain_freezer.go +++ b/core/rawdb/chain_freezer.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "math/big" + "os" "sync" "sync/atomic" "time" @@ -73,6 +74,10 @@ type chainFreezer struct { freezeEnv atomic.Value blockHistory atomic.Uint64 waitEnvTimes int + + // used to reset chain freezer in the incremental case + datadir string + opener freezerOpenFunc } // newChainFreezer initializes the freezer for ancient chain segment. @@ -85,11 +90,15 @@ func newChainFreezer(datadir string, eraDir string, namespace string, readonly b var ( err error freezer ethdb.AncientStore + opener freezerOpenFunc ) if datadir == "" { freezer = NewMemoryFreezer(readonly, chainFreezerTableConfigs) } else { - freezer, err = NewFreezer(datadir, namespace, readonly, freezerTableSize, chainFreezerTableConfigs) + freezer, err = NewFreezer(datadir, namespace, readonly, freezerTableSize, chainFreezerTableConfigs, false) + opener = func() (*Freezer, error) { + return NewFreezer(datadir, namespace, readonly, freezerTableSize, chainFreezerTableConfigs, false) + } } if err != nil { return nil, err @@ -103,6 +112,10 @@ func newChainFreezer(datadir string, eraDir string, namespace string, readonly b eradb: edb, quit: make(chan struct{}), trigger: make(chan chan struct{}), + datadir: datadir, + } + if opener != nil { + cf.opener = opener } // After enabling pruneAncient, the ancient data is not retained. In some specific scenarios where it is // necessary to roll back to blocks prior to the finalized block, it is mandatory to keep the most recent 90,000 blocks in the database to ensure proper functionality and rollback capability. @@ -116,7 +129,7 @@ func resetFreezerMeta(datadir string, namespace string, legacyOffset uint64) err return nil } - freezer, err := NewFreezer(datadir, namespace, false, freezerTableSize, chainFreezerTableConfigs) + freezer, err := NewFreezer(datadir, namespace, false, freezerTableSize, chainFreezerTableConfigs, false) if err != nil { return err } @@ -747,3 +760,89 @@ func trySlowdownFreeze(head *types.Header) { log.Info("Freezer need to slow down", "number", head.Number, "time", head.Time, "new", SlowFreezerBatchLimit) freezerBatchLimit = SlowFreezerBatchLimit } + +func (f *chainFreezer) getAllHashes(nfdb *nofreezedb, number, limit uint64) ([]common.Hash, error) { + lastHash := ReadCanonicalHash(nfdb, limit) + if lastHash == (common.Hash{}) { + return nil, fmt.Errorf("canonical hash missing, can't freeze block %d", limit) + } + + hashes := make([]common.Hash, 0, limit-number+1) + for ; number <= limit; number++ { + // Retrieve all the components of the canonical block. + hash := ReadCanonicalHash(nfdb, number) + if hash == (common.Hash{}) { + return nil, fmt.Errorf("canonical hash missing, can't freeze block %d", number) + } + hashes = append(hashes, hash) + } + return hashes, nil +} + +// CleanBlock clean block data in pebble and chain freezer, except genesis block. +func (f *chainFreezer) CleanBlock(kvStore ethdb.KeyValueStore, start uint64) error { + log.Info("Start cleaning old blocks") + nfdb := &nofreezedb{KeyValueStore: kvStore} + frozen, _ := f.Ancients() // no error will occur, safe to ignore + head := f.readHeadNumber(nfdb) + + first := frozen + last := head + hashes, err := f.getAllHashes(nfdb, first, last) + if err != nil { + log.Error("Failed to freeze block forcefully", "error", err) + return err + } + // Wipe out all data from the active database + batch := kvStore.NewBatch() + for i := 0; i < len(hashes); i++ { + // Always keep the genesis block in the active database + if first+uint64(i) != 0 { + DeleteBlockWithoutNumber(batch, hashes[i], first+uint64(i)) + DeleteCanonicalHash(batch, first+uint64(i)) + } + } + if err = batch.Write(); err != nil { + log.Crit("Failed to delete frozen canonical blocks", "error", err) + } + batch.Reset() + + if err = f.resetToNewStartPoint(start); err != nil { + log.Error("Failed to reset frozen blocks", "error", err) + return err + } + + log.Info("Finished cleaning blocks", "num", len(hashes)) + return nil +} + +func (f *chainFreezer) resetToNewStartPoint(start uint64) error { + if err := cleanup(f.datadir); err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + tmp := tmpName(f.datadir) + if err := os.Rename(f.datadir, tmp); err != nil { + return err + } + if err := os.RemoveAll(tmp); err != nil { + return err + } + freezer, err := f.opener() + if err != nil { + return err + } + f.ancients = freezer + + if err = ResetChainTable(f, start, false); err != nil { + log.Error("Failed to reset chain freezer to the start point", "error", err, "start", start) + return err + } + return nil +} + +func (f *chainFreezer) ResetTableForIncr(kind string, startAt uint64, onlyEmpty bool) error { + return f.ancients.ResetTableForIncr(kind, startAt, onlyEmpty) +} diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 41bf1671a3..9af890216e 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -178,6 +178,10 @@ func (db *nofreezedb) ResetTable(kind string, startAt uint64, onlyEmpty bool) er return errNotSupported } +func (db *nofreezedb) ResetTableForIncr(kind string, startAt uint64, onlyEmpty bool) error { + return errNotSupported +} + // SyncAncient returns an error as we don't have a backing chain freezer. func (db *nofreezedb) SyncAncient() error { return errNotSupported @@ -229,6 +233,9 @@ func (db *nofreezedb) AncientDatadir() (string, error) { func (db *nofreezedb) SetupFreezerEnv(env *ethdb.FreezerEnv, blockHistory uint64) error { return nil } +func (db *nofreezedb) CleanBlock(ethdb.KeyValueStore, uint64) error { + return nil +} // NewDatabase creates a high level database on top of a given key-value data // store without a freezer moving immutable chain segments into cold storage. @@ -290,6 +297,10 @@ func (db *emptyfreezedb) ResetTable(kind string, startAt uint64, onlyEmpty bool) return nil } +func (db *emptyfreezedb) ResetTableForIncr(kind string, startAt uint64, onlyEmpty bool) error { + return nil +} + // SyncAncient returns nil for pruned db that we don't have a backing chain freezer. func (db *emptyfreezedb) SyncAncient() error { return nil @@ -310,6 +321,9 @@ func (db *emptyfreezedb) AncientDatadir() (string, error) { func (db *emptyfreezedb) SetupFreezerEnv(env *ethdb.FreezerEnv, blockHistory uint64) error { return nil } +func (db *emptyfreezedb) CleanBlock(ethdb.KeyValueStore, uint64) error { + return nil +} // NewEmptyFreezeDB is used for CLI such as `geth db inspect` in pruned db that we don't // have a backing chain freezer. @@ -907,6 +921,128 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { return nil } +// InspectAncients +func InspectAncients(db ethdb.Database) error { + // Totals + var ( + total common.StorageSize + stats [][]string + ) + ancients, err := inspectFreezers(db) + if err != nil { + return err + } + for _, ancient := range ancients { + for _, t := range ancient.sizes { + stats = append(stats, []string{ + fmt.Sprintf("Ancient store (%s)", strings.Title(ancient.name)), + strings.Title(t.name), + t.size.String(), + fmt.Sprintf("%d", ancient.count()), + }) + } + total += ancient.size() + } + t := tablewriter.NewWriter(os.Stdout) + t.SetHeader([]string{"Database", "Category", "Size", "Items"}) + t.SetFooter([]string{"", "Total", total.String(), " "}) + t.AppendBulk(stats) + t.Render() + + return nil +} + +// InspectIncrStore traverses the entire incr db and checks the size +// of all different categories of data. +func InspectIncrStore(baseDir string) error { + dirs, err := GetAllIncrDirs(baseDir) + if err != nil { + return err + } + fmt.Println(dirs) + + var ( + total common.StorageSize + stats [][]string + unaccounted stat + info = incrSnapDBInfo{ + readonly: true, + namespace: "eth/db/incremental/", + offset: 0, + maxTableSize: stateHistoryTableSize, + chainTables: incrChainFreezerTableConfigs, + stateTables: incrStateFreezerTableConfigs, + blockInterval: 0, + } + ) + + complete, err := CheckIncrSnapshotComplete(dirs[len(dirs)-1].Path) + if err != nil { + return err + } + if !complete { + log.Info("Skip last incremental directory", "dir", dirs[len(dirs)-1].Path) + dirs = dirs[:len(dirs)-1] + } + + for _, dir := range dirs { + db, err := newSnapDBWrapper(dir.Path, &info) + if err != nil { + return err + } + var ( + codes, parliaSnaps stat + ) + it := db.kvDB.NewIterator(nil, nil) + for it.Next() { + var ( + key = it.Key() + size = common.StorageSize(len(key) + len(it.Value())) + ) + switch { + case bytes.HasPrefix(key, ParliaSnapshotPrefix) && len(key) == 7+common.HashLength: + parliaSnaps.Add(size) + case bytes.HasPrefix(key, CodePrefix) && len(key) == len(CodePrefix)+common.HashLength: + codes.Add(size) + default: + unaccounted.Add(size) + } + } + title := fmt.Sprintf("%s/KV store", dir.Name) + stats = append(stats, [][]string{ + {title, "Contract codes", codes.Size(), codes.Count()}, + {title, "Parlia snapshots", parliaSnaps.Size(), parliaSnaps.Count()}, + }...) + + ancients, err := inspectIncrFreezers(db) + if err != nil { + return err + } + for _, ancient := range ancients { + for _, table := range ancient.sizes { + stats = append(stats, []string{ + fmt.Sprintf("%s/%s", dir.Name, strings.Title(ancient.name)), + strings.Title(table.name), + table.size.String(), + fmt.Sprintf("%d", ancient.count()), + }) + } + total += ancient.size() + } + } + + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"Database", "Category", "Size", "Items"}) + table.SetFooter([]string{"", "Total", total.String(), " "}) + table.AppendBulk(stats) + table.Render() + + if unaccounted.size > 0 { + log.Error("Database contains unaccounted data", "size", unaccounted.size, "count", unaccounted.count) + } + return nil +} + func DeleteTrieState(db ethdb.Database) error { var ( it ethdb.Iterator diff --git a/core/rawdb/freezer.go b/core/rawdb/freezer.go index 778d9b8cbe..a644759a6d 100644 --- a/core/rawdb/freezer.go +++ b/core/rawdb/freezer.go @@ -62,6 +62,7 @@ type Freezer struct { datadir string frozen atomic.Uint64 // Number of items already frozen tail atomic.Uint64 // Number of the first stored item in the freezer + isIncr bool // This lock synchronizes writers and the truncate operation, as well as // the "atomic" (batched) read operations. @@ -80,7 +81,7 @@ type Freezer struct { // The 'tables' argument defines the data tables. If the value of a map // entry is true, snappy compression is disabled for the table. // additionTables indicates the new add tables for freezerDB, it has some special rules. -func NewFreezer(datadir string, namespace string, readonly bool, maxTableSize uint32, tables map[string]freezerTableConfig) (*Freezer, error) { +func NewFreezer(datadir string, namespace string, readonly bool, maxTableSize uint32, tables map[string]freezerTableConfig, isIncr bool) (*Freezer, error) { // Create the initial freezer object var ( readMeter = metrics.NewRegisteredMeter(namespace+"ancient/read", nil) @@ -120,6 +121,7 @@ func NewFreezer(datadir string, namespace string, readonly bool, maxTableSize ui readonly: readonly, tables: make(map[string]*freezerTable), instanceLock: lock, + isIncr: isIncr, } // Create the tables. @@ -162,7 +164,8 @@ func NewFreezer(datadir string, namespace string, readonly bool, maxTableSize ui // Create the write batch. freezer.writeBatch = newFreezerBatch(freezer) - log.Info("Opened ancient database", "database", datadir, "readonly", readonly, "tail", freezer.tail.Load(), "frozen", freezer.frozen.Load()) + log.Info("Opened ancient database", "database", datadir, "readonly", readonly, "tail", freezer.tail.Load(), + "frozen", freezer.frozen.Load(), "isIncr", isIncr) return freezer, nil } @@ -316,7 +319,7 @@ func (f *Freezer) TruncateHead(items uint64) (uint64, error) { // This often happens in chain rewinds, but the blob table is special. // It has the same head, but a different tail from other tables (like bodies, receipts). // So if the chain is rewound to head below the blob's tail, it needs to reset again. - if kind != ChainFreezerBlobSidecarTable { + if kind != ChainFreezerBlobSidecarTable || (f.isIncr && slices.Contains(additionIncrTables, kind)) { return 0, err } nt, err := table.resetItems(items) @@ -457,20 +460,38 @@ func (f *Freezer) repair() error { head = min(head, table.items.Load()) continue } + // addition incremental tables only align head + if f.isIncr && slices.Contains(additionIncrTables, kind) { + if EmptyTable(table) { + continue + } + head = min(head, table.items.Load()) + prunedTail = max(prunedTail, table.itemHidden.Load()) + continue + } + head = min(head, table.items.Load()) prunedTail = max(prunedTail, table.itemHidden.Load()) } + if f.isIncr && (head == math.MaxUint64) { + head = 0 + } for kind, table := range f.tables { // try to align with exist tables, skip empty table if slices.Contains(additionTables, kind) && EmptyTable(table) { continue } + // try to align with exist tables, skip empty table + if f.isIncr && slices.Contains(additionIncrTables, kind) && EmptyTable(table) { + continue + } + err := table.truncateHead(head) if err == errTruncationBelowTail { // This often happens in chain rewinds, but the blob table is special. // It has the same head, but a different tail from other tables (like bodies, receipts). // So if the chain is rewound to head below the blob's tail, it needs to reset again. - if kind != ChainFreezerBlobSidecarTable { + if (kind != ChainFreezerBlobSidecarTable) || (f.isIncr && slices.Contains(additionIncrTables, kind)) { return err } nt, err := table.resetItems(head) @@ -566,6 +587,127 @@ func (f *Freezer) ResetTable(kind string, startAt uint64, onlyEmpty bool) error return nil } +func (f *Freezer) ResetTableForIncr(kind string, startAt uint64, onlyEmpty bool) error { + if f.readonly { + return errReadOnly + } + + f.writeLock.Lock() + defer f.writeLock.Unlock() + + t, exist := f.tables[kind] + if !exist { + return errors.New("you reset a non-exist table") + } + + // if you reset a non empty table just skip + if onlyEmpty && !EmptyTable(t) { + return nil + } + + if err := f.SyncAncient(); err != nil { + return err + } + nt, err := t.resetItems(startAt) + if err != nil { + return err + } + f.tables[kind] = nt + + // repair all tables with same tail & head + if err = f.repairForIncr(); err != nil { + for _, t = range f.tables { + t.Close() + } + return err + } + f.writeBatch = newFreezerBatch(f) + log.Debug("Reset Table for incremental snapshot merge", "kind", kind, "tail", f.tables[kind].itemHidden.Load(), "frozen", f.tables[kind].items.Load()) + return nil +} + +func (f *Freezer) repairForIncr() error { + var ( + head = uint64(math.MaxUint64) + tail = uint64(0) + ) + for kind, table := range f.tables { + // addition tables only align head + if slices.Contains(additionTables, kind) { + if EmptyTable(table) { + continue + } + head = min(head, table.items.Load()) + continue + } + + // addition incremental tables only align head + if _, ok := stateFreezerTableConfigs[kind]; ok { + if EmptyTable(table) { + continue + } + head = min(head, table.items.Load()) + tail = max(tail, table.itemHidden.Load()) + continue + } + + if slices.Contains(additionIncrTables, kind) { + if EmptyTable(table) { + continue + } + head = min(head, table.items.Load()) + tail = max(tail, table.itemHidden.Load()) + continue + } + + head = min(head, table.items.Load()) + tail = max(tail, table.itemHidden.Load()) + } + if head == math.MaxUint64 { + head = 0 + } + for kind, table := range f.tables { + // try to align with exist tables, skip empty table + if slices.Contains(additionTables, kind) && EmptyTable(table) { + continue + } + // try to align with exist tables, skip empty table + _, ok := stateFreezerTableConfigs[kind] + if ok && EmptyTable(table) { + continue + } + if slices.Contains(additionIncrTables, kind) && EmptyTable(table) { + continue + } + + err := table.truncateHead(head) + if err == errTruncationBelowTail { + // This often happens in chain rewinds, but the blob table is special. + // It has the same head, but a different tail from other tables (like bodies, receipts). + // So if the chain is rewound to head below the blob's tail, it needs to reset again. + _, ok = stateFreezerTableConfigs[kind] + if (kind != ChainFreezerBlobSidecarTable) || slices.Contains(additionIncrTables, kind) || ok { + return err + } + nt, err := table.resetItems(head) + if err != nil { + return err + } + f.tables[kind] = nt + continue + } + if err != nil { + return err + } + if err := table.truncateTail(tail); err != nil { + return err + } + } + f.frozen.Store(head) + f.tail.Store(tail) + return nil +} + // resetTailMeta will reset tail meta with legacyOffset // Caution: the freezer cannot be used anymore, it will sync/close all data files func (f *Freezer) resetTailMeta(legacyOffset uint64) error { diff --git a/core/rawdb/freezer_memory.go b/core/rawdb/freezer_memory.go index f71d4c4eea..cc4921cb66 100644 --- a/core/rawdb/freezer_memory.go +++ b/core/rawdb/freezer_memory.go @@ -416,12 +416,16 @@ func (f *MemoryFreezer) Reset() error { } func (f *MemoryFreezer) TruncateTableTail(kind string, tail uint64) (uint64, error) { - //TODO implement me + // TODO implement me panic("implement me") } func (f *MemoryFreezer) ResetTable(kind string, startAt uint64, onlyEmpty bool) error { - //TODO implement me + // TODO implement me + panic("not supported") +} + +func (f *MemoryFreezer) ResetTableForIncr(kind string, startAt uint64, onlyEmpty bool) error { panic("not supported") } diff --git a/core/rawdb/freezer_resettable.go b/core/rawdb/freezer_resettable.go index 8017900a47..b88ba76c06 100644 --- a/core/rawdb/freezer_resettable.go +++ b/core/rawdb/freezer_resettable.go @@ -49,12 +49,12 @@ type resettableFreezer struct { // // The reset function will delete directory atomically and re-create the // freezer from scratch. -func newResettableFreezer(datadir string, namespace string, readonly bool, maxTableSize uint32, tables map[string]freezerTableConfig) (*resettableFreezer, error) { +func newResettableFreezer(datadir string, namespace string, readonly bool, maxTableSize uint32, tables map[string]freezerTableConfig, isIncr bool) (*resettableFreezer, error) { if err := cleanup(datadir); err != nil { return nil, err } opener := func() (*Freezer, error) { - return NewFreezer(datadir, namespace, readonly, maxTableSize, tables) + return NewFreezer(datadir, namespace, readonly, maxTableSize, tables, isIncr) } freezer, err := opener() if err != nil { @@ -201,6 +201,13 @@ func (f *resettableFreezer) ResetTable(kind string, startAt uint64, onlyEmpty bo return f.freezer.ResetTable(kind, startAt, onlyEmpty) } +func (f *resettableFreezer) ResetTableForIncr(kind string, startAt uint64, onlyEmpty bool) error { + f.lock.RLock() + defer f.lock.RUnlock() + + return f.freezer.ResetTableForIncr(kind, startAt, onlyEmpty) +} + // SyncAncient flushes all data tables to disk. func (f *resettableFreezer) SyncAncient() error { f.lock.RLock() diff --git a/core/rawdb/freezer_resettable_test.go b/core/rawdb/freezer_resettable_test.go index 61dc23d798..38e4ce7dee 100644 --- a/core/rawdb/freezer_resettable_test.go +++ b/core/rawdb/freezer_resettable_test.go @@ -33,7 +33,7 @@ func TestResetFreezer(t *testing.T) { {1, bytes.Repeat([]byte{1}, 2048)}, {2, bytes.Repeat([]byte{2}, 2048)}, } - f, _ := newResettableFreezer(t.TempDir(), "", false, 2048, freezerTestTableDef) + f, _ := newResettableFreezer(t.TempDir(), "", false, 2048, freezerTestTableDef, false) defer f.Close() f.ModifyAncients(func(op ethdb.AncientWriteOp) error { @@ -87,7 +87,7 @@ func TestFreezerCleanup(t *testing.T) { {2, bytes.Repeat([]byte{2}, 2048)}, } datadir := t.TempDir() - f, _ := newResettableFreezer(datadir, "", false, 2048, freezerTestTableDef) + f, _ := newResettableFreezer(datadir, "", false, 2048, freezerTestTableDef, false) f.ModifyAncients(func(op ethdb.AncientWriteOp) error { for _, item := range items { op.AppendRaw("test", item.id, item.blob) @@ -98,7 +98,7 @@ func TestFreezerCleanup(t *testing.T) { os.Rename(datadir, tmpName(datadir)) // Open the freezer again, trigger cleanup operation - f, _ = newResettableFreezer(datadir, "", false, 2048, freezerTestTableDef) + f, _ = newResettableFreezer(datadir, "", false, 2048, freezerTestTableDef, false) f.Close() if _, err := os.Lstat(tmpName(datadir)); !os.IsNotExist(err) { diff --git a/core/rawdb/freezer_table_test.go b/core/rawdb/freezer_table_test.go index 9cb46333c4..64180ef67b 100644 --- a/core/rawdb/freezer_table_test.go +++ b/core/rawdb/freezer_table_test.go @@ -51,13 +51,13 @@ func TestFreezerBasics(t *testing.T) { // Write 15 bytes 255 times, results in 85 files writeChunks(t, f, 255, 15) - //print(t, f, 0) - //print(t, f, 1) - //print(t, f, 2) + // print(t, f, 0) + // print(t, f, 1) + // print(t, f, 2) // - //db[0] = 000000000000000000000000000000 - //db[1] = 010101010101010101010101010101 - //db[2] = 020202020202020202020202020202 + // db[0] = 000000000000000000000000000000 + // db[1] = 010101010101010101010101010101 + // db[2] = 020202020202020202020202020202 for y := 0; y < 255; y++ { exp := getChunk(15, y) diff --git a/core/rawdb/freezer_test.go b/core/rawdb/freezer_test.go index 7a2360ff3a..546dcc3931 100644 --- a/core/rawdb/freezer_test.go +++ b/core/rawdb/freezer_test.go @@ -116,7 +116,7 @@ func TestFreezerModifyRollback(t *testing.T) { // Reopen and check that the rolled-back data doesn't reappear. tables := map[string]freezerTableConfig{"test": {noSnappy: true}} - f2, err := NewFreezer(dir, "", false, 2049, tables) + f2, err := NewFreezer(dir, "", false, 2049, tables, false) if err != nil { t.Fatalf("can't reopen freezer after failed ModifyAncients: %v", err) } @@ -257,7 +257,7 @@ func TestFreezerReadonlyValidate(t *testing.T) { dir := t.TempDir() // Open non-readonly freezer and fill individual tables // with different amount of data. - f, err := NewFreezer(dir, "", false, 2049, tables) + f, err := NewFreezer(dir, "", false, 2049, tables, false) if err != nil { t.Fatal("can't open freezer", err) } @@ -280,7 +280,7 @@ func TestFreezerReadonlyValidate(t *testing.T) { // Re-opening as readonly should fail when validating // table lengths. - _, err = NewFreezer(dir, "", true, 2049, tables) + _, err = NewFreezer(dir, "", true, 2049, tables, false) if err == nil { t.Fatal("readonly freezer should fail with differing table lengths") } @@ -292,7 +292,7 @@ func TestFreezerConcurrentReadonly(t *testing.T) { tables := map[string]freezerTableConfig{"a": {noSnappy: true}} dir := t.TempDir() - f, err := NewFreezer(dir, "", false, 2049, tables) + f, err := NewFreezer(dir, "", false, 2049, tables, false) if err != nil { t.Fatal("can't open freezer", err) } @@ -318,7 +318,7 @@ func TestFreezerConcurrentReadonly(t *testing.T) { go func(i int) { defer wg.Done() - f, err := NewFreezer(dir, "", true, 2049, tables) + f, err := NewFreezer(dir, "", true, 2049, tables, false) if err == nil { fs[i] = f } else { @@ -341,7 +341,7 @@ func TestFreezer_AdditionTables(t *testing.T) { dir := t.TempDir() // Open non-readonly freezer and fill individual tables // with different amount of data. - f, err := NewFreezer(dir, "", false, 2049, o1o2TableDef) + f, err := NewFreezer(dir, "", false, 2049, o1o2TableDef, false) if err != nil { t.Fatal("can't open freezer", err) } @@ -367,11 +367,11 @@ func TestFreezer_AdditionTables(t *testing.T) { // check read only additionTables = []string{"a1"} - f, err = NewFreezer(dir, "", true, 2049, o1o2a1TableDef) + f, err = NewFreezer(dir, "", true, 2049, o1o2a1TableDef, false) require.NoError(t, err) require.NoError(t, f.Close()) - f, err = NewFreezer(dir, "", false, 2049, o1o2a1TableDef) + f, err = NewFreezer(dir, "", false, 2049, o1o2a1TableDef, false) require.NoError(t, err) frozen, _ := f.Ancients() require.NoError(t, f.ResetTable("a1", frozen, true)) @@ -414,7 +414,7 @@ func TestFreezer_AdditionTables(t *testing.T) { require.NoError(t, f.Close()) // reopen and read - f, err = NewFreezer(dir, "", true, 2049, o1o2a1TableDef) + f, err = NewFreezer(dir, "", true, 2049, o1o2a1TableDef, false) require.NoError(t, err) // recheck additional table boundary @@ -431,7 +431,7 @@ func TestFreezer_AdditionTables(t *testing.T) { func TestFreezer_ResetTailMeta_WithAdditionTable(t *testing.T) { dir := t.TempDir() - f, err := NewFreezer(dir, "", false, 2049, o1o2TableDef) + f, err := NewFreezer(dir, "", false, 2049, o1o2TableDef, false) if err != nil { t.Fatal("can't open freezer", err) } @@ -456,7 +456,7 @@ func TestFreezer_ResetTailMeta_WithAdditionTable(t *testing.T) { require.NoError(t, f.Close()) additionTables = []string{"a1"} - f, err = NewFreezer(dir, "", false, 2049, o1o2a1TableDef) + f, err = NewFreezer(dir, "", false, 2049, o1o2a1TableDef, false) require.NoError(t, err) frozen, _ := f.Ancients() require.NoError(t, f.ResetTable("a1", frozen, true)) @@ -480,7 +480,7 @@ func TestFreezer_ResetTailMeta_WithAdditionTable(t *testing.T) { f.Close() // check items - f, err = NewFreezer(dir, "", false, 2049, o1o2a1TableDef) + f, err = NewFreezer(dir, "", false, 2049, o1o2a1TableDef, false) require.NoError(t, err) _, err = f.Ancient("o1", 0) require.Error(t, err) @@ -506,7 +506,7 @@ func TestFreezer_ResetTailMeta_WithAdditionTable(t *testing.T) { func TestFreezer_ResetTailMeta_EmptyTable(t *testing.T) { dir := t.TempDir() - f, err := NewFreezer(dir, "", false, 2049, o1o2TableDef) + f, err := NewFreezer(dir, "", false, 2049, o1o2TableDef, false) if err != nil { t.Fatal("can't open freezer", err) } @@ -516,7 +516,7 @@ func TestFreezer_ResetTailMeta_EmptyTable(t *testing.T) { // try to append the ancient additionTables = []string{"a1"} - f, err = NewFreezer(dir, "", false, 2049, o1o2a1TableDef) + f, err = NewFreezer(dir, "", false, 2049, o1o2a1TableDef, false) require.NoError(t, err) var item = make([]byte, 1024) _, err = f.ModifyAncients(func(op ethdb.AncientWriteOp) error { @@ -537,7 +537,7 @@ func TestFreezer_ResetTailMeta_EmptyTable(t *testing.T) { require.NoError(t, err) require.NoError(t, f.Close()) - f, err = NewFreezer(dir, "", false, 2049, o1o2a1TableDef) + f, err = NewFreezer(dir, "", false, 2049, o1o2a1TableDef, false) require.NoError(t, err) frozen, _ := f.Ancients() require.NoError(t, f.ResetTable("a1", frozen, true)) @@ -582,7 +582,7 @@ func newFreezerForTesting(t *testing.T, tables map[string]freezerTableConfig) (* dir := t.TempDir() // note: using low max table size here to ensure the tests actually // switch between multiple files. - f, err := NewFreezer(dir, "", false, 2049, tables) + f, err := NewFreezer(dir, "", false, 2049, tables, false) if err != nil { t.Fatal("can't open freezer", err) } @@ -656,7 +656,7 @@ func TestFreezerSuite(t *testing.T) { prunable: true, } } - f, _ := newResettableFreezer(t.TempDir(), "", false, 2048, tables) + f, _ := newResettableFreezer(t.TempDir(), "", false, 2048, tables, false) return f }) } diff --git a/core/rawdb/incr_snap_db.go b/core/rawdb/incr_snap_db.go new file mode 100644 index 0000000000..c0a94b3071 --- /dev/null +++ b/core/rawdb/incr_snap_db.go @@ -0,0 +1,605 @@ +package rawdb + +import ( + "encoding/binary" + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + "sort" + "strconv" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/ethdb/pebble" + "github.com/ethereum/go-ethereum/log" +) + +// AsyncWriteManager defines the interface for async write manager +type AsyncWriteManager interface { + // ForceFlushStateBuffer forces all buffered incr state data to be flushed before directory switch + ForceFlushStateBuffer() error +} + +const ( + incrDirNameRegexPattern = `^incr-(\d+)-(\d+)$` + incrDirNamePattern = "incr-%d-%d" +) + +// FirstStateID is used to record the first state id in each incr snapshot +var FirstStateID = []byte("firstStateID") + +type IncrSnapDB struct { + currSnapDB *snapDBWrapper + info incrSnapDBInfo + baseDir string + currentDir string + lastBlock uint64 // last updated block number + blockCount uint64 // record the stored number of blocks in current snap db + lock sync.RWMutex + + // directory switching control + switching bool + switchCond *sync.Cond + switchMutex sync.Mutex +} + +type snapDBWrapper struct { + chainFreezer ethdb.ResettableAncientStore + stateFreezer ethdb.ResettableAncientStore + kvDB ethdb.KeyValueStore +} + +type incrSnapDBInfo struct { + readonly bool + namespace string + offset uint64 + maxTableSize uint32 + chainTables map[string]freezerTableConfig + stateTables map[string]freezerTableConfig + blockInterval uint64 // write needs to set it; 0 is used in reading data from incr db +} + +// IncrDirInfo holds information about an incremental directory +type IncrDirInfo struct { + Name string + Path string + StartBlockNum uint64 + EndBlockNum uint64 +} + +// NewIncrSnapDB creates a new incremental snap database +func NewIncrSnapDB(baseDir string, readonly bool, startBlock, blockInterval uint64) (*IncrSnapDB, error) { + info := incrSnapDBInfo{ + readonly: readonly, + namespace: "eth/db/incremental/", + maxTableSize: stateHistoryTableSize, + chainTables: incrChainFreezerTableConfigs, + stateTables: incrStateFreezerTableConfigs, + blockInterval: blockInterval, + } + + // Find the latest directory or create the first one + currentDir, err := findLatestIncrDir(baseDir, startBlock, blockInterval) + if err != nil { + return nil, err + } + + db, err := newSnapDBWrapper(currentDir, &info) + if err != nil { + return nil, err + } + + incrDB := &IncrSnapDB{ + currSnapDB: db, + info: info, + baseDir: baseDir, + currentDir: currentDir, + switching: false, + } + incrDB.switchCond = sync.NewCond(&incrDB.switchMutex) + + log.Info("New incr snap db", "baseDir", baseDir, "currentDir", currentDir, "blockInterval", blockInterval, + "startBlock", startBlock) + return incrDB, nil +} + +func newSnapDBWrapper(incrDir string, info *incrSnapDBInfo) (*snapDBWrapper, error) { + if incrDir == "" { + return &snapDBWrapper{ + chainFreezer: NewMemoryFreezer(info.readonly, incrChainFreezerTableConfigs), + stateFreezer: NewMemoryFreezer(info.readonly, incrStateFreezerTableConfigs), + kvDB: NewMemoryDatabase(), + }, nil + } + + chainPath := filepath.Join(incrDir, ChainFreezerName) + chainNamespace := fmt.Sprintf("%s%s", info.namespace, "ChainFreezer") + cFreezer, err := newResettableFreezer(chainPath, chainNamespace, info.readonly, info.maxTableSize, + info.chainTables, true) + if err != nil { + return nil, fmt.Errorf("failed to create incr chain freezer: %w", err) + } + + statePath := filepath.Join(incrDir, MerkleStateFreezerName) + stateNamespace := fmt.Sprintf("%s%s", info.namespace, "MerkleStateFreezer") + sFreezer, err := newResettableFreezer(statePath, stateNamespace, info.readonly, info.maxTableSize, + info.stateTables, true) + if err != nil { + return nil, fmt.Errorf("failed to create incr state freezer: %w", err) + } + + kvNamespace := fmt.Sprintf("%s%s", info.namespace, "kv") + db, err := pebble.New(incrDir, 10, 10, kvNamespace, info.readonly) + if err != nil { + return nil, fmt.Errorf("failed to create incr KV database: %w", err) + } + + return &snapDBWrapper{ + chainFreezer: cFreezer, + stateFreezer: sFreezer, + kvDB: NewDatabase(db), + }, nil +} + +// SetBlockCount sets the block count +func (idb *IncrSnapDB) SetBlockCount(blockCount uint64) { + idb.blockCount = blockCount +} + +// waitForSwitchComplete waits until directory switching is complete +func (idb *IncrSnapDB) waitForSwitchComplete() { + const ( + pollInterval = 100 * time.Millisecond + timeout = 60 * time.Second + ) + + deadline := time.Now().Add(timeout) + ticker := time.NewTicker(pollInterval) + defer ticker.Stop() + + for time.Now().Before(deadline) { + idb.switchMutex.Lock() + if !idb.switching { + idb.switchMutex.Unlock() + return + } + idb.switchMutex.Unlock() + + <-ticker.C + log.Debug("Waiting for directory switch to complete") + } + + // Timeout occurred + log.Error("Timeout waiting for directory switch to complete") +} + +// WriteIncrBlockData writes incremental block data and checks if directory switch is needed +func (idb *IncrSnapDB) WriteIncrBlockData(number, id uint64, hash, header, body, receipts, td, sidecars []byte, + isEmptyBlock, isCancun bool) error { + idb.lock.Lock() + defer idb.lock.Unlock() + + err := WriteIncrBlockData(idb.currSnapDB.chainFreezer, number, id, hash, header, body, receipts, td, sidecars, isEmptyBlock, isCancun) + if err != nil { + return fmt.Errorf("failed to write block %d: %w", number, err) + } + idb.blockCount++ + log.Debug("Block written to IncrDB", "blockNum", number, "currentDir", idb.currentDir, "blockCount", idb.blockCount) + + return nil +} + +// WriteIncrState writes incremental state data +func (idb *IncrSnapDB) WriteIncrTrieNodes(id uint64, meta, trieNodes []byte) error { + idb.lock.Lock() + defer idb.lock.Unlock() + + if err := WriteIncrTrieNodes(idb.currSnapDB.stateFreezer, id, meta, trieNodes); err != nil { + return fmt.Errorf("failed to write incr trie nodes: %w", err) + } + return nil +} + +// WriteIncrState writes incremental state data +func (idb *IncrSnapDB) WriteIncrState(id uint64, meta, states []byte) error { + idb.lock.Lock() + defer idb.lock.Unlock() + + if err := WriteIncrState(idb.currSnapDB.stateFreezer, id, meta, states); err != nil { + return fmt.Errorf("failed to write incr state: %w", err) + } + return nil +} + +// WriteIncrContractCodes writes contract codes +func (idb *IncrSnapDB) WriteIncrContractCodes(codes map[common.Address]ContractCode) error { + idb.waitForSwitchComplete() + + idb.lock.Lock() + defer idb.lock.Unlock() + + batch := idb.currSnapDB.kvDB.NewBatch() + for _, code := range codes { + WriteCode(batch, code.Hash, code.Blob) + } + if err := batch.Write(); err != nil { + return fmt.Errorf("failed to write incr contract codes: %w", err) + } + return nil +} + +// WriteParliaSnapshot stores parlia snapshot into pebble. +func (idb *IncrSnapDB) WriteParliaSnapshot(hash common.Hash, blob []byte) { + idb.lock.Lock() + defer idb.lock.Unlock() + + if err := idb.currSnapDB.kvDB.Put(append(ParliaSnapshotPrefix, hash[:]...), blob); err != nil { + log.Crit("Failed to write parlia snapshot", "error", err) + } +} + +func (idb *IncrSnapDB) NewIterator(prefix []byte, start []byte) ethdb.Iterator { + idb.lock.Lock() + defer idb.lock.Unlock() + + return idb.currSnapDB.kvDB.NewIterator(prefix, start) +} + +func (idb *IncrSnapDB) WriteFirstStateID(id uint64) { + idb.lock.Lock() + defer idb.lock.Unlock() + + enc := make([]byte, 8) + binary.BigEndian.PutUint64(enc, id) + if err := idb.currSnapDB.kvDB.Put(FirstStateID, enc); err != nil { + log.Crit("Failed to write first state ID", "id", id, "error", err) + } +} + +// switchToNewDirectoryWithAsyncManager performs directory switch with async write manager coordination +func (idb *IncrSnapDB) switchToNewDirectoryWithAsyncManager(blockNum uint64, asyncManager AsyncWriteManager) error { + log.Info("Starting coordinated directory switch", "blockNum", blockNum) + + // Set switching flag to block new writes + idb.switchMutex.Lock() + idb.switching = true + idb.switchMutex.Unlock() + + defer func() { + // Clear switching flag and notify waiting writers + idb.switchMutex.Lock() + idb.switching = false + idb.switchCond.Broadcast() + idb.switchMutex.Unlock() + }() + + log.Info("Force flushing all incr state data before directory switch") + if err := asyncManager.ForceFlushStateBuffer(); err != nil { + return err + } + + idb.lock.Lock() + defer idb.lock.Unlock() + + // Record the last block in old chain freezer before switching + // It's used to detect and handle empty blocks that might be skipped + if idb.currSnapDB != nil && idb.currSnapDB.chainFreezer != nil { + ancients, err := idb.currSnapDB.chainFreezer.Ancients() + if err != nil { + log.Error("Failed to get ancients from old chain freezer", "err", err) + return err + } + tail, err := idb.currSnapDB.chainFreezer.Tail() + if err != nil { + log.Error("Failed to get tail from old chain freezer", "err", err) + return err + } + + // Record the last block that was actually written to the old chain freezer + // ancients represents the count of blocks, so the last written block is ancients - 1 + idb.lastBlock = ancients - 1 + log.Info("Recorded old chain freezer state", "lastBlock", idb.lastBlock, "ancients", ancients, + "tail", tail, "switchTriggerBlock", blockNum) + } + + if err := idb.closeCurrentDatabases(); err != nil { + return err + } + + newDir := filepath.Join(idb.baseDir, fmt.Sprintf(incrDirNamePattern, blockNum, blockNum+idb.info.blockInterval-1)) + db, err := newSnapDBWrapper(newDir, &idb.info) + if err != nil { + return fmt.Errorf("failed to create new snap db wrapper in directory %s: %v", newDir, err) + } + + idb.currSnapDB = db + idb.currentDir = newDir + idb.blockCount = 0 + log.Info("Successfully completed coordinated directory switch", "newDir", newDir, + "oldLastBlock", idb.lastBlock, "startBlock", blockNum) + + return nil +} + +// closeCurrentDatabases safely closes all current databases +func (idb *IncrSnapDB) closeCurrentDatabases() error { + if idb.currSnapDB == nil { + return nil + } + + var errs []error + + // Close chain freezer + if idb.currSnapDB.chainFreezer != nil { + if err := idb.currSnapDB.chainFreezer.Close(); err != nil { + log.Error("Failed to close chain freezer", "err", err) + errs = append(errs, fmt.Errorf("chain freezer: %v", err)) + } + } + + // Close state freezer + if idb.currSnapDB.stateFreezer != nil { + if err := idb.currSnapDB.stateFreezer.Close(); err != nil { + log.Error("Failed to close state freezer", "err", err) + errs = append(errs, fmt.Errorf("state freezer: %v", err)) + } + } + + // Close KV database + if idb.currSnapDB.kvDB != nil { + if err := idb.currSnapDB.kvDB.Close(); err != nil { + log.Error("Failed to close kv db", "err", err) + errs = append(errs, fmt.Errorf("kv database: %v", err)) + } + } + + if len(errs) > 0 { + return errors.Join(errs...) + } + + log.Info("All databases closed successfully") + return nil +} + +// GetChainFreezer returns the current chain freezer +func (idb *IncrSnapDB) GetChainFreezer() ethdb.ResettableAncientStore { + idb.lock.RLock() + defer idb.lock.RUnlock() + + if idb.currSnapDB != nil { + return idb.currSnapDB.chainFreezer + } + return nil +} + +// GetStateFreezer returns the current state freezer +func (idb *IncrSnapDB) GetStateFreezer() ethdb.ResettableAncientStore { + idb.lock.RLock() + defer idb.lock.RUnlock() + + if idb.currSnapDB != nil { + return idb.currSnapDB.stateFreezer + } + return nil +} + +// GetKVDB returns the current kv db +func (idb *IncrSnapDB) GetKVDB() ethdb.KeyValueStore { + idb.lock.RLock() + defer idb.lock.RUnlock() + + if idb.currSnapDB != nil { + return idb.currSnapDB.kvDB + } + return nil +} + +// GetLastBlock returns the last block number +func (idb *IncrSnapDB) GetLastBlock() uint64 { + idb.lock.RLock() + defer idb.lock.RUnlock() + + return idb.lastBlock +} + +// Full returns the last block number +func (idb *IncrSnapDB) Full() bool { + idb.lock.RLock() + defer idb.lock.RUnlock() + + return idb.info.blockInterval > 0 && idb.blockCount >= idb.info.blockInterval +} + +// Close closes the IncrDB and all underlying databases +func (idb *IncrSnapDB) Close() error { + idb.lock.Lock() + defer idb.lock.Unlock() + + log.Info("Closing IncrDB", "currentDir", idb.currentDir) + return idb.closeCurrentDatabases() +} + +// IsSwitching returns true if directory switching is in progress +func (idb *IncrSnapDB) IsSwitching() bool { + idb.switchMutex.Lock() + defer idb.switchMutex.Unlock() + + return idb.switching +} + +// CheckAndInitiateSwitch safely checks if directory switch is needed and initiates it +// Returns true if switch was initiated, false if not needed or already in progress +func (idb *IncrSnapDB) CheckAndInitiateSwitch(blockNum uint64, asyncManager AsyncWriteManager) (bool, error) { + // First check without lock (fast path) + if idb.IsSwitching() { + return false, nil + } + + // Double-checked locking to prevent race conditions + idb.switchMutex.Lock() + if idb.switching { + // Another goroutine already initiated the switch + idb.switchMutex.Unlock() + return false, nil + } + + // Check limit again with proper lock to ensure consistency + idb.lock.RLock() + limitReached := idb.info.blockInterval > 0 && idb.blockCount >= idb.info.blockInterval + idb.lock.RUnlock() + + if !limitReached { + idb.switchMutex.Unlock() + return false, nil + } + + // We need to switch and we're the first to acquire the switch lock + idb.switchMutex.Unlock() + + log.Info("Initiating directory switch", "blockNum", blockNum) + err := idb.switchToNewDirectoryWithAsyncManager(blockNum, asyncManager) + return true, err +} + +// Reset all incremental directories. +func (idb *IncrSnapDB) ResetAllIncr(block uint64) error { + idb.lock.RLock() + defer idb.lock.RUnlock() + + if err := idb.reset(block); err != nil { + return err + } + return nil +} + +// reset the specified directory and create a new snap db. +func (idb *IncrSnapDB) reset(block uint64) error { + if err := idb.closeCurrentDatabases(); err != nil { + return err + } + if err := os.RemoveAll(idb.currentDir); err != nil { + return err + } + if err := os.MkdirAll(idb.baseDir, 0755); err != nil { + return fmt.Errorf("failed to create base directory %s: %v", idb.baseDir, err) + } + + newDir := filepath.Join(idb.baseDir, fmt.Sprintf(incrDirNamePattern, block, block+idb.info.blockInterval-1)) + db, err := newSnapDBWrapper(newDir, &idb.info) + if err != nil { + return fmt.Errorf("failed to create new snap db wrapper in directory %s: %v", newDir, err) + } + + idb.currSnapDB = db + idb.currentDir = newDir + idb.blockCount = 0 + return nil +} + +func (idb *IncrSnapDB) ParseCurrDirBlockNumber() (uint64, uint64, error) { + return parseDirBlockNumber(idb.currentDir) +} + +// parseDirBlockNumber parses the start and end block number from directory path +func parseDirBlockNumber(dirPath string) (uint64, uint64, error) { + path := filepath.Base(dirPath) + pattern := regexp.MustCompile(incrDirNameRegexPattern) + matches := pattern.FindStringSubmatch(path) + if len(matches) != 3 { + return 0, 0, fmt.Errorf("invalid directory name format: %s", path) + } + + startBlock, err := strconv.ParseUint(matches[1], 10, 64) + if err != nil { + return 0, 0, fmt.Errorf("failed to parse start block from directory name %s: %v", path, err) + } + endBlock, err := strconv.ParseUint(matches[2], 10, 64) + if err != nil { + return 0, 0, fmt.Errorf("failed to parse end block from directory name %s: %v", path, err) + } + + return startBlock, endBlock, nil +} + +// findLatestIncrDir finds the latest incremental directory or creates the first one +func findLatestIncrDir(baseDir string, startBlock, blockLimit uint64) (string, error) { + if err := os.MkdirAll(baseDir, 0755); err != nil { + return "", fmt.Errorf("failed to create base directory %s: %v", baseDir, err) + } + + entries, err := os.ReadDir(baseDir) + if err != nil { + return "", fmt.Errorf("failed to read base directory %s: %v", baseDir, err) + } + + var incrDirs []IncrDirInfo + for _, entry := range entries { + if !entry.IsDir() { + continue + } + start, end, err := parseDirBlockNumber(entry.Name()) + if err != nil { + log.Warn("Invalid incremental directory name", "dir", entry.Name(), "err", err) + continue + } + incrDirs = append(incrDirs, IncrDirInfo{ + Name: entry.Name(), + Path: filepath.Join(baseDir, entry.Name()), + StartBlockNum: start, + EndBlockNum: end, + }) + } + + // If no existing directories found, create the first one + if len(incrDirs) == 0 { + firstDir := filepath.Join(baseDir, fmt.Sprintf(incrDirNamePattern, startBlock, startBlock+blockLimit-1)) + log.Info("No existing incremental directories found, creating first one", "dir", firstDir) + return firstDir, nil + } + + // Sort by block number and return the latest one + sort.Slice(incrDirs, func(i, j int) bool { + return incrDirs[i].StartBlockNum < incrDirs[j].StartBlockNum + }) + + latestDir := incrDirs[len(incrDirs)-1] + log.Info("Found latest incremental directory", "dir", latestDir.Path, "startBlockNum", latestDir.StartBlockNum, + "endBlockNum", latestDir.EndBlockNum) + return latestDir.Path, nil +} + +// GetAllIncrDirs returns all incremental directories sorted by block number +func GetAllIncrDirs(baseDir string) ([]IncrDirInfo, error) { + entries, err := os.ReadDir(baseDir) + if err != nil { + return nil, fmt.Errorf("failed to read base directory %s: %v", baseDir, err) + } + + var incrDirs []IncrDirInfo + for _, entry := range entries { + if !entry.IsDir() { + continue + } + start, end, err := parseDirBlockNumber(entry.Name()) + if err != nil { + log.Warn("Invalid incremental directory name", "dir", entry.Name(), "err", err) + continue + } + incrDirs = append(incrDirs, IncrDirInfo{ + Name: entry.Name(), + Path: filepath.Join(baseDir, entry.Name()), + StartBlockNum: start, + EndBlockNum: end, + }) + } + + // Sort by block number + sort.Slice(incrDirs, func(i, j int) bool { + return incrDirs[i].StartBlockNum < incrDirs[j].StartBlockNum + }) + return incrDirs, nil +} diff --git a/core/rawdb/incr_snap_db_test.go b/core/rawdb/incr_snap_db_test.go new file mode 100644 index 0000000000..0a38c6eaf1 --- /dev/null +++ b/core/rawdb/incr_snap_db_test.go @@ -0,0 +1,435 @@ +package rawdb + +import ( + "os" + "path/filepath" + "testing" +) + +func TestParseDirBlockNumber(t *testing.T) { + tests := []struct { + name string + dirPath string + wantStart uint64 + wantEnd uint64 + wantErr bool + expectedErr string + }{ + { + name: "valid directory path", + dirPath: "/path/to/incr-1000-1999", + wantStart: 1000, + wantEnd: 1999, + wantErr: false, + }, + { + name: "valid directory path with different numbers", + dirPath: "/some/path/incr-5000-5999", + wantStart: 5000, + wantEnd: 5999, + wantErr: false, + }, + { + name: "valid directory path with single digit", + dirPath: "/test/incr-1-9", + wantStart: 1, + wantEnd: 9, + wantErr: false, + }, + { + name: "invalid directory name format", + dirPath: "/path/to/invalid_dir", + wantErr: true, + expectedErr: "invalid directory name format: invalid_dir", + }, + { + name: "invalid directory name with wrong pattern", + dirPath: "/path/to/incr-abc-def", + wantErr: true, + expectedErr: "invalid directory name format: incr-abc-def", + }, + { + name: "invalid directory name with missing parts", + dirPath: "/path/to/incr_1000", + wantErr: true, + expectedErr: "invalid directory name format: incr_1000", + }, + { + name: "invalid start block number", + dirPath: "/path/to/incr_abc-1999", + wantErr: true, + expectedErr: "invalid directory name format: incr_abc-1999", + }, + { + name: "invalid end block number", + dirPath: "/path/to/incr-1000_def", + wantErr: true, + expectedErr: "invalid directory name format: incr-1000_def", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + start, end, err := parseDirBlockNumber(tt.dirPath) + + if tt.wantErr { + if err == nil { + t.Fatalf("parseDirBlockNumber() expected error but got none") + } + if tt.expectedErr != "" && err.Error() != tt.expectedErr { + t.Fatalf("parseDirBlockNumber() error = %v, expected %v", err.Error(), tt.expectedErr) + } + } + + if start != tt.wantStart { + t.Fatalf("parseDirBlockNumber() start = %v, want %v", start, tt.wantStart) + } + if end != tt.wantEnd { + t.Fatalf("parseDirBlockNumber() end = %v, want %v", end, tt.wantEnd) + } + }) + } +} + +func TestFindLatestIncrDir(t *testing.T) { + tests := []struct { + name string + setupDirs []string + startBlock uint64 + blockLimit uint64 + expectedDir string + wantErr bool + }{ + { + name: "no existing directories - should create first one", + setupDirs: []string{}, + startBlock: 1000, + blockLimit: 1000, + expectedDir: "incr-1000-1999", + wantErr: false, + }, + { + name: "existing directories - should return latest", + setupDirs: []string{ + "incr-1000-1999", + "incr-2000-2999", + "incr-3000-3999", + }, + startBlock: 1000, + blockLimit: 1000, + expectedDir: "incr-3000-3999", + wantErr: false, + }, + { + name: "existing directories with gaps - should return latest", + setupDirs: []string{ + "incr-1000-1999", + "incr-3000-3999", + "incr-5000-5999", + }, + startBlock: 1000, + blockLimit: 1000, + expectedDir: "incr-5000-5999", + wantErr: false, + }, + { + name: "existing directories with invalid names - should ignore invalid ones", + setupDirs: []string{ + "incr-1000-1999", + "invalid_dir", + "incr-2000-2999", + "another_invalid", + }, + startBlock: 1000, + blockLimit: 1000, + expectedDir: "incr-2000-2999", + wantErr: false, + }, + { + name: "existing directories with invalid block numbers - should ignore invalid ones", + setupDirs: []string{ + "incr-1000-1999", + "incr_abc-def", + "incr-2000-2999", + }, + startBlock: 1000, + blockLimit: 1000, + expectedDir: "incr-2000-2999", + wantErr: false, + }, + { + name: "single existing directory", + setupDirs: []string{ + "incr-1000-1999", + }, + startBlock: 1000, + blockLimit: 1000, + expectedDir: "incr-1000-1999", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create independent test directory for each test case + testDir := t.TempDir() + + // Create setup directories + for _, dirName := range tt.setupDirs { + dirPath := filepath.Join(testDir, dirName) + if err := os.MkdirAll(dirPath, 0755); err != nil { + t.Fatalf("Failed to create setup directory %s: %v", dirPath, err) + } + } + + // Test the function + gotDir, err := findLatestIncrDir(testDir, tt.startBlock, tt.blockLimit) + + if tt.wantErr { + if err == nil { + t.Fatalf("findLatestIncrDir() expected error but got none") + } + return + } + + if err != nil { + t.Fatalf("findLatestIncrDir() unexpected error = %v", err) + } + + // Verify the result + expectedDir := filepath.Join(testDir, tt.expectedDir) + if gotDir != expectedDir { + t.Fatalf("findLatestIncrDir() = %v, want %v", gotDir, expectedDir) + } + }) + } +} + +func TestFindLatestIncrDirWithFiles(t *testing.T) { + // Test with files in directory (should be ignored) + tempDir := t.TempDir() + + // Create a file in the directory + filePath := filepath.Join(tempDir, "test_file.txt") + if err := os.WriteFile(filePath, []byte("test"), 0644); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + // Create a valid directory + validDir := filepath.Join(tempDir, "incr-1000-1999") + if err := os.MkdirAll(validDir, 0755); err != nil { + t.Fatalf("Failed to create valid directory: %v", err) + } + + // Test that files are ignored and only directories are considered + gotDir, err := findLatestIncrDir(tempDir, 1000, 1000) + if err != nil { + t.Errorf("findLatestIncrDir() unexpected error = %v", err) + return + } + + expectedDir := filepath.Join(tempDir, "incr-1000-1999") + if gotDir != expectedDir { + t.Errorf("findLatestIncrDir() = %v, want %v", gotDir, expectedDir) + } +} + +func TestGetAllIncrDirs(t *testing.T) { + tests := []struct { + name string + setupDirs []string + expectedDirs []IncrDirInfo + wantErr bool + }{ + { + name: "no directories", + setupDirs: []string{}, + expectedDirs: []IncrDirInfo{}, + wantErr: false, + }, + { + name: "valid incremental directories", + setupDirs: []string{ + "incr-1000-1999", + "incr-2000-2999", + "incr-3000-3999", + }, + expectedDirs: []IncrDirInfo{ + {Name: "incr-1000-1999", StartBlockNum: 1000, EndBlockNum: 1999}, + {Name: "incr-2000-2999", StartBlockNum: 2000, EndBlockNum: 2999}, + {Name: "incr-3000-3999", StartBlockNum: 3000, EndBlockNum: 3999}, + }, + wantErr: false, + }, + { + name: "mixed valid and invalid directories", + setupDirs: []string{ + "incr-1000-1999", + "invalid_dir", + "incr-2000-2999", + "another_invalid", + "incr-3000-3999", + }, + expectedDirs: []IncrDirInfo{ + {Name: "incr-1000-1999", StartBlockNum: 1000, EndBlockNum: 1999}, + {Name: "incr-2000-2999", StartBlockNum: 2000, EndBlockNum: 2999}, + {Name: "incr-3000-3999", StartBlockNum: 3000, EndBlockNum: 3999}, + }, + wantErr: false, + }, + { + name: "directories with invalid block numbers", + setupDirs: []string{ + "incr-1000-1999", + "incr_abc_def", + "incr-2000-2999", + "incr-xyz-123", + }, + expectedDirs: []IncrDirInfo{ + {Name: "incr-1000-1999", StartBlockNum: 1000, EndBlockNum: 1999}, + {Name: "incr-2000-2999", StartBlockNum: 2000, EndBlockNum: 2999}, + }, + wantErr: false, + }, + { + name: "unsorted directories - should return sorted", + setupDirs: []string{ + "incr-3000-3999", + "incr-1000-1999", + "incr-2000-2999", + }, + expectedDirs: []IncrDirInfo{ + {Name: "incr-1000-1999", StartBlockNum: 1000, EndBlockNum: 1999}, + {Name: "incr-2000-2999", StartBlockNum: 2000, EndBlockNum: 2999}, + {Name: "incr-3000-3999", StartBlockNum: 3000, EndBlockNum: 3999}, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create independent test directory for each test case + testDir := t.TempDir() + + // Create setup directories + for _, dirName := range tt.setupDirs { + dirPath := filepath.Join(testDir, dirName) + if err := os.MkdirAll(dirPath, 0755); err != nil { + t.Fatalf("Failed to create setup directory %s: %v", dirPath, err) + } + } + + // Test the function + gotDirs, err := GetAllIncrDirs(testDir) + + if tt.wantErr { + if err == nil { + t.Fatalf("GetAllIncrDirs() expected error but got none") + } + return + } + if err != nil { + t.Fatalf("GetAllIncrDirs() unexpected error = %v", err) + return + } + // Verify the result + if len(gotDirs) != len(tt.expectedDirs) { + t.Fatalf("GetAllIncrDirs() returned %d directories, want %d", len(gotDirs), len(tt.expectedDirs)) + } + + // Build expected directories with full paths + expectedDirs := make([]IncrDirInfo, len(tt.expectedDirs)) + for i, expected := range tt.expectedDirs { + expectedDirs[i] = IncrDirInfo{ + Name: expected.Name, + Path: filepath.Join(testDir, expected.Name), + StartBlockNum: expected.StartBlockNum, + EndBlockNum: expected.EndBlockNum, + } + } + + verifyDirs(t, gotDirs, expectedDirs) + }) + } +} + +// verifyDirs is a helper function to verify directory information +func verifyDirs(t *testing.T, gotDirs, expectedDirs []IncrDirInfo) { + for i, gotDir := range gotDirs { + expectedDir := expectedDirs[i] + if gotDir.Name != expectedDir.Name { + t.Fatalf("GetAllIncrDirs()[%d].Name = %v, want %v", i, gotDir.Name, expectedDir.Name) + } + if gotDir.Path != expectedDir.Path { + t.Fatalf("GetAllIncrDirs()[%d].Path = %v, want %v", i, gotDir.Path, expectedDir.Path) + } + if gotDir.StartBlockNum != expectedDir.StartBlockNum { + t.Fatalf("GetAllIncrDirs()[%d].StartBlockNum = %v, want %v", i, gotDir.StartBlockNum, expectedDir.StartBlockNum) + } + if gotDir.EndBlockNum != expectedDir.EndBlockNum { + t.Fatalf("GetAllIncrDirs()[%d].EndBlockNum = %v, want %v", i, gotDir.EndBlockNum, expectedDir.EndBlockNum) + } + } +} + +func TestGetAllIncrDirsWithFiles(t *testing.T) { + // Test with files in directory (should be ignored) + tempDir := t.TempDir() + + // Create files in the directory + files := []string{"test1.txt", "test2.dat", "incr_1000_1999.txt"} + for _, fileName := range files { + filePath := filepath.Join(tempDir, fileName) + if err := os.WriteFile(filePath, []byte("test"), 0644); err != nil { + t.Fatalf("Failed to create test file %s: %v", fileName, err) + } + } + + // Create a valid directory + validDir := filepath.Join(tempDir, "incr-1000-1999") + if err := os.MkdirAll(validDir, 0755); err != nil { + t.Fatalf("Failed to create valid directory: %v", err) + } + + // Test that files are ignored and only directories are returned + gotDirs, err := GetAllIncrDirs(tempDir) + if err != nil { + t.Errorf("GetAllIncrDirs() unexpected error = %v", err) + return + } + + if len(gotDirs) != 1 { + t.Errorf("GetAllIncrDirs() returned %d directories, want 1", len(gotDirs)) + return + } + + expectedDir := IncrDirInfo{ + Name: "incr-1000-1999", + Path: filepath.Join(tempDir, "incr-1000-1999"), + StartBlockNum: 1000, + EndBlockNum: 1999, + } + + if gotDirs[0].Name != expectedDir.Name { + t.Errorf("GetAllIncrDirs()[0].Name = %v, want %v", gotDirs[0].Name, expectedDir.Name) + } + if gotDirs[0].Path != expectedDir.Path { + t.Errorf("GetAllIncrDirs()[0].Path = %v, want %v", gotDirs[0].Path, expectedDir.Path) + } + if gotDirs[0].StartBlockNum != expectedDir.StartBlockNum { + t.Errorf("GetAllIncrDirs()[0].StartBlockNum = %v, want %v", gotDirs[0].StartBlockNum, expectedDir.StartBlockNum) + } + if gotDirs[0].EndBlockNum != expectedDir.EndBlockNum { + t.Errorf("GetAllIncrDirs()[0].EndBlockNum = %v, want %v", gotDirs[0].EndBlockNum, expectedDir.EndBlockNum) + } +} + +func TestGetAllIncrDirsError(t *testing.T) { + // Test with non-existent directory + nonExistentDir := "/non/existent/directory" + _, err := GetAllIncrDirs(nonExistentDir) + if err == nil { + t.Errorf("GetAllIncrDirs() expected error for non-existent directory but got none") + } +} diff --git a/core/rawdb/table.go b/core/rawdb/table.go index 5d8006ea70..f0bd02a7a6 100644 --- a/core/rawdb/table.go +++ b/core/rawdb/table.go @@ -95,6 +95,10 @@ func (t *table) ResetTable(kind string, startAt uint64, onlyEmpty bool) error { return t.db.ResetTable(kind, startAt, onlyEmpty) } +func (t *table) ResetTableForIncr(kind string, startAt uint64, onlyEmpty bool) error { + return t.db.ResetTableForIncr(kind, startAt, onlyEmpty) +} + func (t *table) ReadAncients(fn func(reader ethdb.AncientReaderOp) error) (err error) { return t.db.ReadAncients(fn) } @@ -235,6 +239,10 @@ func (t *table) SetupFreezerEnv(env *ethdb.FreezerEnv, blockHistory uint64) erro return nil } +func (t *table) CleanBlock(kvStore ethdb.KeyValueStore, start uint64) error { + return nil +} + // tableBatch is a wrapper around a database batch that prefixes each key access // with a pre-configured string. type tableBatch struct { diff --git a/core/state/statedb.go b/core/state/statedb.go index 419d2df074..66847236fa 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -1479,6 +1479,20 @@ func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorag return nil, err } } + // Write dirty contract code into incremental db if any exists and incr is enabled + if db := s.db.TrieDB(); db != nil && len(ret.codes) > 0 && db.IsIncrEnabled() { + codes := make(map[common.Address]rawdb.ContractCode) + for hash, code := range ret.codes { + codes[hash] = rawdb.ContractCode{ + Hash: code.hash, + Blob: code.blob, + } + } + if err = db.WriteContractCodes(codes); err != nil { + return nil, err + } + } + if !ret.empty() { // If snapshotting is enabled, update the snapshot tree with this new version if snap := s.db.Snapshot(); snap != nil && snap.Snapshot(ret.originRoot) != nil { diff --git a/core/types/tx_blob.go b/core/types/tx_blob.go index 2a31e5ec2d..57124d0faa 100644 --- a/core/types/tx_blob.go +++ b/core/types/tx_blob.go @@ -60,11 +60,10 @@ type BlobTxSidecar struct { // NOTE(BSC): PeerDAS support (EIP-7594) is disabled. // Only sidecar Version = 0 (EIP-4844 legacy proofs) is supported for now. // See upstream PR: https://github.com/ethereum/go-ethereum/pull/31791 - Version byte `json:"version" rlp:"-"` // Sidecar version - - Blobs []kzg4844.Blob `json:"blobs"` // Blobs needed by the blob pool - Commitments []kzg4844.Commitment `json:"commitments"` // Commitments needed by the blob pool - Proofs []kzg4844.Proof `json:"proofs"` // Proofs needed by the blob pool + Version byte `json:"version" rlp:"-"` // Sidecar version + Blobs []kzg4844.Blob `json:"blobs"` // Blobs needed by the blob pool + Commitments []kzg4844.Commitment `json:"commitments"` // Commitments needed by the blob pool + Proofs []kzg4844.Proof `json:"proofs"` // Proofs needed by the blob pool } // BlobHashes computes the blob hashes of the given blobs. diff --git a/eth/backend.go b/eth/backend.go index 2f2bed4f49..97bb4d00e2 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -322,23 +322,30 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { journalFilePath := stack.ResolvePath(path) + "/" + JournalFileName var ( options = &core.BlockChainConfig{ - TrieCleanLimit: config.TrieCleanCache, - NoPrefetch: config.NoPrefetch, - EnableBAL: config.EnableBAL, - TrieDirtyLimit: config.TrieDirtyCache, - ArchiveMode: config.NoPruning, - TrieTimeLimit: config.TrieTimeout, - NoTries: noTries, - SnapshotLimit: config.SnapshotCache, - TriesInMemory: config.TriesInMemory, - Preimages: config.Preimages, - StateHistory: config.StateHistory, - StateScheme: config.StateScheme, - PathSyncFlush: config.PathSyncFlush, - JournalFilePath: journalFilePath, - JournalFile: config.JournalFileEnabled, - ChainHistoryMode: config.HistoryMode, - TxLookupLimit: int64(min(config.TransactionHistory, math.MaxInt64)), + TrieCleanLimit: config.TrieCleanCache, + NoPrefetch: config.NoPrefetch, + EnableBAL: config.EnableBAL, + TrieDirtyLimit: config.TrieDirtyCache, + ArchiveMode: config.NoPruning, + TrieTimeLimit: config.TrieTimeout, + NoTries: noTries, + SnapshotLimit: config.SnapshotCache, + TriesInMemory: config.TriesInMemory, + Preimages: config.Preimages, + StateHistory: config.StateHistory, + StateScheme: config.StateScheme, + PathSyncFlush: config.PathSyncFlush, + JournalFilePath: journalFilePath, + JournalFile: config.JournalFileEnabled, + EnableIncr: config.EnableIncrSnapshots, + IncrHistoryPath: config.IncrSnapshotPath, + IncrHistory: config.IncrSnapshotBlockInterval, + IncrStateBuffer: config.IncrSnapshotStateBuffer, + IncrKeptBlocks: config.IncrSnapshotKeptBlocks, + UseRemoteIncrSnapshot: config.UseRemoteIncrSnapshot, + RemoteIncrURL: config.RemoteIncrSnapshotURL, + ChainHistoryMode: config.HistoryMode, + TxLookupLimit: int64(min(config.TransactionHistory, math.MaxInt64)), VmConfig: vm.Config{ EnablePreimageRecording: config.EnablePreimageRecording, }, diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 86c57071a4..07347e10c1 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -214,6 +214,15 @@ type Config struct { // blob setting BlobExtraReserve uint64 + + // incremental snapshot config + EnableIncrSnapshots bool + IncrSnapshotPath string + IncrSnapshotBlockInterval uint64 + IncrSnapshotStateBuffer uint64 + IncrSnapshotKeptBlocks uint64 + UseRemoteIncrSnapshot bool + RemoteIncrSnapshotURL string } // CreateConsensusEngine creates a consensus engine for the given chain config. diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go index a784f2dabc..b4c3c63314 100644 --- a/eth/ethconfig/gen_config.go +++ b/eth/ethconfig/gen_config.go @@ -18,65 +18,72 @@ import ( // MarshalTOML marshals as TOML. func (c Config) MarshalTOML() (interface{}, error) { type Config struct { - Genesis *core.Genesis `toml:",omitempty"` - NetworkId uint64 - SyncMode SyncMode - DisablePeerTxBroadcast bool - EVNNodeIDsToAdd []enode.ID - EVNNodeIDsToRemove []enode.ID - HistoryMode history.HistoryMode - EthDiscoveryURLs []string - SnapDiscoveryURLs []string - BscDiscoveryURLs []string - NoPruning bool - NoPrefetch bool - EnableBAL bool - DirectBroadcast bool - DisableSnapProtocol bool - RangeLimit bool - TxLookupLimit uint64 `toml:",omitempty"` - TransactionHistory uint64 `toml:",omitempty"` - BlockHistory uint64 `toml:",omitempty"` - LogHistory uint64 `toml:",omitempty"` - LogNoHistory bool `toml:",omitempty"` - LogExportCheckpoints string - StateHistory uint64 `toml:",omitempty"` - StateScheme string `toml:",omitempty"` - PathSyncFlush bool `toml:",omitempty"` - JournalFileEnabled bool - DisableTxIndexer bool `toml:",omitempty"` - RequiredBlocks map[uint64]common.Hash `toml:"-"` - SkipBcVersionCheck bool `toml:"-"` - DatabaseHandles int `toml:"-"` - DatabaseCache int - DatabaseFreezer string - DatabaseEra string - PruneAncientData bool - TrieCleanCache int - TrieDirtyCache int - TrieTimeout time.Duration - SnapshotCache int - TriesInMemory uint64 - TriesVerifyMode core.VerifyMode - Preimages bool - FilterLogCacheSize int - Miner minerconfig.Config - TxPool legacypool.Config - BlobPool blobpool.Config - GPO gasprice.Config - EnablePreimageRecording bool - VMTrace string - VMTraceJsonConfig string - RPCGasCap uint64 - RPCEVMTimeout time.Duration - RPCTxFeeCap float64 - OverridePassedForkTime *uint64 `toml:",omitempty"` - OverrideLorentz *uint64 `toml:",omitempty"` - OverrideMaxwell *uint64 `toml:",omitempty"` - OverrideFermi *uint64 `toml:",omitempty"` - OverrideOsaka *uint64 `toml:",omitempty"` - OverrideVerkle *uint64 `toml:",omitempty"` - BlobExtraReserve uint64 + Genesis *core.Genesis `toml:",omitempty"` + NetworkId uint64 + SyncMode SyncMode + DisablePeerTxBroadcast bool + EVNNodeIDsToAdd []enode.ID + EVNNodeIDsToRemove []enode.ID + HistoryMode history.HistoryMode + EthDiscoveryURLs []string + SnapDiscoveryURLs []string + BscDiscoveryURLs []string + NoPruning bool + NoPrefetch bool + EnableBAL bool + DirectBroadcast bool + DisableSnapProtocol bool + RangeLimit bool + TxLookupLimit uint64 `toml:",omitempty"` + TransactionHistory uint64 `toml:",omitempty"` + BlockHistory uint64 `toml:",omitempty"` + LogHistory uint64 `toml:",omitempty"` + LogNoHistory bool `toml:",omitempty"` + LogExportCheckpoints string + StateHistory uint64 `toml:",omitempty"` + StateScheme string `toml:",omitempty"` + PathSyncFlush bool `toml:",omitempty"` + JournalFileEnabled bool + DisableTxIndexer bool `toml:",omitempty"` + RequiredBlocks map[uint64]common.Hash `toml:"-"` + SkipBcVersionCheck bool `toml:"-"` + DatabaseHandles int `toml:"-"` + DatabaseCache int + DatabaseFreezer string + DatabaseEra string + PruneAncientData bool + TrieCleanCache int + TrieDirtyCache int + TrieTimeout time.Duration + SnapshotCache int + TriesInMemory uint64 + TriesVerifyMode core.VerifyMode + Preimages bool + FilterLogCacheSize int + Miner minerconfig.Config + TxPool legacypool.Config + BlobPool blobpool.Config + GPO gasprice.Config + EnablePreimageRecording bool + VMTrace string + VMTraceJsonConfig string + RPCGasCap uint64 + RPCEVMTimeout time.Duration + RPCTxFeeCap float64 + OverridePassedForkTime *uint64 `toml:",omitempty"` + OverrideLorentz *uint64 `toml:",omitempty"` + OverrideMaxwell *uint64 `toml:",omitempty"` + OverrideFermi *uint64 `toml:",omitempty"` + OverrideOsaka *uint64 `toml:",omitempty"` + OverrideVerkle *uint64 `toml:",omitempty"` + BlobExtraReserve uint64 + EnableIncrSnapshots bool + IncrSnapshotPath string + IncrSnapshotBlockInterval uint64 + IncrSnapshotStateBuffer uint64 + IncrSnapshotKeptBlocks uint64 + UseRemoteIncrSnapshot bool + RemoteIncrSnapshotURL string } var enc Config enc.Genesis = c.Genesis @@ -138,71 +145,85 @@ func (c Config) MarshalTOML() (interface{}, error) { enc.OverrideOsaka = c.OverrideOsaka enc.OverrideVerkle = c.OverrideVerkle enc.BlobExtraReserve = c.BlobExtraReserve + enc.EnableIncrSnapshots = c.EnableIncrSnapshots + enc.IncrSnapshotPath = c.IncrSnapshotPath + enc.IncrSnapshotBlockInterval = c.IncrSnapshotBlockInterval + enc.IncrSnapshotStateBuffer = c.IncrSnapshotStateBuffer + enc.IncrSnapshotKeptBlocks = c.IncrSnapshotKeptBlocks + enc.UseRemoteIncrSnapshot = c.UseRemoteIncrSnapshot + enc.RemoteIncrSnapshotURL = c.RemoteIncrSnapshotURL return &enc, nil } // UnmarshalTOML unmarshals from TOML. func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { type Config struct { - Genesis *core.Genesis `toml:",omitempty"` - NetworkId *uint64 - SyncMode *SyncMode - DisablePeerTxBroadcast *bool - EVNNodeIDsToAdd []enode.ID - EVNNodeIDsToRemove []enode.ID - HistoryMode *history.HistoryMode - EthDiscoveryURLs []string - SnapDiscoveryURLs []string - BscDiscoveryURLs []string - NoPruning *bool - NoPrefetch *bool - EnableBAL *bool - DirectBroadcast *bool - DisableSnapProtocol *bool - RangeLimit *bool - TxLookupLimit *uint64 `toml:",omitempty"` - TransactionHistory *uint64 `toml:",omitempty"` - BlockHistory *uint64 `toml:",omitempty"` - LogHistory *uint64 `toml:",omitempty"` - LogNoHistory *bool `toml:",omitempty"` - LogExportCheckpoints *string - StateHistory *uint64 `toml:",omitempty"` - StateScheme *string `toml:",omitempty"` - PathSyncFlush *bool `toml:",omitempty"` - JournalFileEnabled *bool - DisableTxIndexer *bool `toml:",omitempty"` - RequiredBlocks map[uint64]common.Hash `toml:"-"` - SkipBcVersionCheck *bool `toml:"-"` - DatabaseHandles *int `toml:"-"` - DatabaseCache *int - DatabaseFreezer *string - DatabaseEra *string - PruneAncientData *bool - TrieCleanCache *int - TrieDirtyCache *int - TrieTimeout *time.Duration - SnapshotCache *int - TriesInMemory *uint64 - TriesVerifyMode *core.VerifyMode - Preimages *bool - FilterLogCacheSize *int - Miner *minerconfig.Config - TxPool *legacypool.Config - BlobPool *blobpool.Config - GPO *gasprice.Config - EnablePreimageRecording *bool - VMTrace *string - VMTraceJsonConfig *string - RPCGasCap *uint64 - RPCEVMTimeout *time.Duration - RPCTxFeeCap *float64 - OverridePassedForkTime *uint64 `toml:",omitempty"` - OverrideLorentz *uint64 `toml:",omitempty"` - OverrideMaxwell *uint64 `toml:",omitempty"` - OverrideFermi *uint64 `toml:",omitempty"` - OverrideOsaka *uint64 `toml:",omitempty"` - OverrideVerkle *uint64 `toml:",omitempty"` - BlobExtraReserve *uint64 + Genesis *core.Genesis `toml:",omitempty"` + NetworkId *uint64 + SyncMode *SyncMode + DisablePeerTxBroadcast *bool + EVNNodeIDsToAdd []enode.ID + EVNNodeIDsToRemove []enode.ID + HistoryMode *history.HistoryMode + EthDiscoveryURLs []string + SnapDiscoveryURLs []string + BscDiscoveryURLs []string + NoPruning *bool + NoPrefetch *bool + EnableBAL *bool + DirectBroadcast *bool + DisableSnapProtocol *bool + RangeLimit *bool + TxLookupLimit *uint64 `toml:",omitempty"` + TransactionHistory *uint64 `toml:",omitempty"` + BlockHistory *uint64 `toml:",omitempty"` + LogHistory *uint64 `toml:",omitempty"` + LogNoHistory *bool `toml:",omitempty"` + LogExportCheckpoints *string + StateHistory *uint64 `toml:",omitempty"` + StateScheme *string `toml:",omitempty"` + PathSyncFlush *bool `toml:",omitempty"` + JournalFileEnabled *bool + DisableTxIndexer *bool `toml:",omitempty"` + RequiredBlocks map[uint64]common.Hash `toml:"-"` + SkipBcVersionCheck *bool `toml:"-"` + DatabaseHandles *int `toml:"-"` + DatabaseCache *int + DatabaseFreezer *string + DatabaseEra *string + PruneAncientData *bool + TrieCleanCache *int + TrieDirtyCache *int + TrieTimeout *time.Duration + SnapshotCache *int + TriesInMemory *uint64 + TriesVerifyMode *core.VerifyMode + Preimages *bool + FilterLogCacheSize *int + Miner *minerconfig.Config + TxPool *legacypool.Config + BlobPool *blobpool.Config + GPO *gasprice.Config + EnablePreimageRecording *bool + VMTrace *string + VMTraceJsonConfig *string + RPCGasCap *uint64 + RPCEVMTimeout *time.Duration + RPCTxFeeCap *float64 + OverridePassedForkTime *uint64 `toml:",omitempty"` + OverrideLorentz *uint64 `toml:",omitempty"` + OverrideMaxwell *uint64 `toml:",omitempty"` + OverrideFermi *uint64 `toml:",omitempty"` + OverrideOsaka *uint64 `toml:",omitempty"` + OverrideVerkle *uint64 `toml:",omitempty"` + BlobExtraReserve *uint64 + EnableIncrSnapshots *bool + IncrSnapshotPath *string + IncrSnapshotBlockInterval *uint64 + IncrSnapshotStateBuffer *uint64 + IncrSnapshotKeptBlocks *uint64 + UseRemoteIncrSnapshot *bool + RemoteIncrSnapshotURL *string } var dec Config if err := unmarshal(&dec); err != nil { @@ -385,5 +406,26 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { if dec.BlobExtraReserve != nil { c.BlobExtraReserve = *dec.BlobExtraReserve } + if dec.EnableIncrSnapshots != nil { + c.EnableIncrSnapshots = *dec.EnableIncrSnapshots + } + if dec.IncrSnapshotPath != nil { + c.IncrSnapshotPath = *dec.IncrSnapshotPath + } + if dec.IncrSnapshotBlockInterval != nil { + c.IncrSnapshotBlockInterval = *dec.IncrSnapshotBlockInterval + } + if dec.IncrSnapshotStateBuffer != nil { + c.IncrSnapshotStateBuffer = *dec.IncrSnapshotStateBuffer + } + if dec.IncrSnapshotKeptBlocks != nil { + c.IncrSnapshotKeptBlocks = *dec.IncrSnapshotKeptBlocks + } + if dec.UseRemoteIncrSnapshot != nil { + c.UseRemoteIncrSnapshot = *dec.UseRemoteIncrSnapshot + } + if dec.RemoteIncrSnapshotURL != nil { + c.RemoteIncrSnapshotURL = *dec.RemoteIncrSnapshotURL + } return nil } diff --git a/ethdb/database.go b/ethdb/database.go index 96d9d5abb3..b6510bc64d 100644 --- a/ethdb/database.go +++ b/ethdb/database.go @@ -171,6 +171,8 @@ type AncientWriter interface { // ResetTable will reset certain table with new start point ResetTable(kind string, startAt uint64, onlyEmpty bool) error + + ResetTableForIncr(kind string, startAt uint64, onlyEmpty bool) error } type FreezerEnv struct { @@ -182,6 +184,10 @@ type FreezerEnv struct { type AncientFreezer interface { // SetupFreezerEnv provides params.ChainConfig for checking hark forks, like isCancun. SetupFreezerEnv(env *FreezerEnv, blockHistory uint64) error + + // CleanBlock cleans block data in pebble and chain freezer. + // WARN: it's only used in the incremental snapshot situation. + CleanBlock(kvStore KeyValueStore, start uint64) error } // AncientWriteOp is given to the function argument of ModifyAncients. diff --git a/ethdb/remotedb/remotedb.go b/ethdb/remotedb/remotedb.go index 7e90c1b136..53c6c0ff06 100644 --- a/ethdb/remotedb/remotedb.go +++ b/ethdb/remotedb/remotedb.go @@ -133,6 +133,10 @@ func (db *Database) ResetTable(kind string, startAt uint64, onlyEmpty bool) erro panic("not supported") } +func (db *Database) ResetTableForIncr(kind string, startAt uint64, onlyEmpty bool) error { + panic("not supported") +} + func (db *Database) SyncAncient() error { return nil } @@ -174,6 +178,10 @@ func (db *Database) SetupFreezerEnv(env *ethdb.FreezerEnv, blockHistory uint64) panic("not supported") } +func (db *Database) CleanBlock(ethdb.KeyValueStore, uint64) error { + panic("not supported") +} + func New(client *rpc.Client) ethdb.Database { if client == nil { return nil diff --git a/go.mod b/go.mod index 3e5b574078..44e1b530b3 100644 --- a/go.mod +++ b/go.mod @@ -58,6 +58,7 @@ require ( github.com/olekukonko/tablewriter v0.0.5 github.com/panjf2000/ants/v2 v2.4.5 github.com/peterh/liner v1.2.0 + github.com/pierrec/lz4/v4 v4.1.22 github.com/pion/stun/v2 v2.0.0 github.com/pkg/errors v0.9.1 github.com/protolambda/bls12-381-util v0.1.0 diff --git a/go.sum b/go.sum index 4ed4ad6a06..02a58df78f 100644 --- a/go.sum +++ b/go.sum @@ -871,6 +871,8 @@ github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCr github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.4.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= diff --git a/triedb/database.go b/triedb/database.go index 661fe1b427..46619b1a0c 100644 --- a/triedb/database.go +++ b/triedb/database.go @@ -441,3 +441,50 @@ func (db *Database) IsVerkle() bool { func (db *Database) Disk() ethdb.Database { return db.disk } + +// MergeIncrState merges the state in incremental snapshot into base snapshot +func (db *Database) MergeIncrState(incrDir string) error { + pdb, ok := db.backend.(*pathdb.Database) + if !ok { + log.Error("Not supported") + return nil + } + return pdb.MergeIncrState(incrDir) +} + +// WriteContractCodes used to write contract codes into incremental db. +func (db *Database) WriteContractCodes(codes map[common.Address]rawdb.ContractCode) error { + pdb, ok := db.backend.(*pathdb.Database) + if !ok { + log.Error("Not supported") + return errors.New("not supported WriteContractCodes") + } + return pdb.WriteContractCodes(codes) +} + +// IsIncrEnabled returns true if incremental is enabled, otherwise false. +func (db *Database) IsIncrEnabled() bool { + pdb, ok := db.backend.(*pathdb.Database) + if !ok { + return false + } + return pdb.IsIncrEnabled() +} + +// SetStateGenerator is used to set state generator. +func (db *Database) SetStateGenerator() { + pdb, ok := db.backend.(*pathdb.Database) + if !ok { + return + } + pdb.SetStateGenerator() +} + +// RepairIncrStore is used to repair incr store. +func (db *Database) GetStartBlock() (uint64, error) { + pdb, ok := db.backend.(*pathdb.Database) + if !ok { + return 0, errors.New("not supported GetStartBlock") + } + return pdb.GetStartBlock() +} diff --git a/triedb/pathdb/async_incr_state.go b/triedb/pathdb/async_incr_state.go new file mode 100644 index 0000000000..e4477e6708 --- /dev/null +++ b/triedb/pathdb/async_incr_state.go @@ -0,0 +1,509 @@ +package pathdb + +import ( + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie/trienode" +) + +// asyncIncrStateBuffer writes the incremental state trie nodes into incr state db. +type asyncIncrStateBuffer struct { + mux sync.RWMutex + current *incrNodeBuffer + background *incrNodeBuffer + + truncateChan chan uint64 + flushedStateID atomic.Uint64 + + isFlushing atomic.Bool + stopFlushing atomic.Bool + done chan struct{} +} + +// newAsyncIncrStateBuffer initializes the async incremental state buffer. +func newAsyncIncrStateBuffer(limit, batchSize uint64) *asyncIncrStateBuffer { + b := &asyncIncrStateBuffer{ + current: newIncrNodeBuffer(limit, batchSize, nil, nil, 0), + background: newIncrNodeBuffer(limit, batchSize, nil, nil, 0), + done: make(chan struct{}), + truncateChan: make(chan uint64, 1), + } + + // Start monitoring goroutine + go b.monitorStateBuffer() + + return b +} + +// monitorStateBuffer monitors the state buffer every 5 minutes +func (a *asyncIncrStateBuffer) monitorStateBuffer() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + a.printBufferInfo() + case <-a.done: + log.Debug("Monitor buffer stopped due to done signal") + return + } + } +} + +// printBufferInfo prints detailed info about both current and background buffer +func (a *asyncIncrStateBuffer) printBufferInfo() { + a.mux.RLock() + defer a.mux.RUnlock() + + log.Info("Current buffer Status", "empty", a.current.empty(), "full", a.current.full(), + "totalSize", common.StorageSize(a.current.size()), "nodesSize", common.StorageSize(a.current.nodes.size), + "statesSize", common.StorageSize(a.current.states.size), "layers", a.current.layers, + "immutable", atomic.LoadUint64(&a.current.immutable) == 1, + "stateIDRange", fmt.Sprintf("%d-%d", a.current.stateIDArray[0], a.current.stateIDArray[1]), + "blockNumberRange", fmt.Sprintf("%d-%d", a.current.blockNumberArray[0], a.current.blockNumberArray[1]), + "limit", common.StorageSize(a.current.limit), "batchSize", common.StorageSize(a.current.batchSize)) + + log.Info("Background buffer Status", "empty", a.background.empty(), "full", a.background.full(), + "totalSize", common.StorageSize(a.background.size()), "nodesSize", common.StorageSize(a.background.nodes.size), + "statesSize", common.StorageSize(a.background.states.size), "layers", a.background.layers, + "immutable", atomic.LoadUint64(&a.background.immutable) == 1, + "stateIDRange", fmt.Sprintf("%d-%d", a.background.stateIDArray[0], a.background.stateIDArray[1]), + "blockNumberRange", fmt.Sprintf("%d-%d", a.background.blockNumberArray[0], a.background.blockNumberArray[1]), + "limit", common.StorageSize(a.current.limit), "batchSize", common.StorageSize(a.current.batchSize)) +} + +// commit merges the provided states and trie nodes into the buffer. +func (a *asyncIncrStateBuffer) commit(root common.Hash, nodes *nodeSet, states *stateSet, stateID, blockNumber uint64) *asyncIncrStateBuffer { + a.mux.Lock() + defer a.mux.Unlock() + + err := a.current.commit(root, nodes, states, stateID, blockNumber) + if err != nil { + log.Crit("Failed to commit nodes to async incremental state buffer", "error", err) + } + return a +} + +// empty returns an indicator if buffer contains any state transition inside. +func (a *asyncIncrStateBuffer) empty() bool { + a.mux.RLock() + defer a.mux.RUnlock() + + return a.current.empty() && a.background.empty() +} + +func (a *asyncIncrStateBuffer) getFlushedStateID() uint64 { + old := a.flushedStateID.Load() + a.flushedStateID.Store(0) + return old +} + +// flush persists the in-memory trie nodes to ancient db if the memory threshold is reached. +func (a *asyncIncrStateBuffer) flush(incrDB *rawdb.IncrSnapDB, force bool) error { + a.mux.Lock() + defer a.mux.Unlock() + + if a.stopFlushing.Load() { + return nil + } + + if force { + for { + if atomic.LoadUint64(&a.background.immutable) == 1 { + log.Info("Waiting background incr state buffer flushed to disk for forcing flush") + time.Sleep(3 * time.Second) + continue + } + atomic.StoreUint64(&a.current.immutable, 1) + a.flushedStateID.Store(a.current.stateIDArray[1]) + return a.current.flush(incrDB) + } + } + + if !a.current.full() { + return nil + } + + if atomic.LoadUint64(&a.background.immutable) == 1 { + return nil + } + + atomic.StoreUint64(&a.current.immutable, 1) + a.current, a.background = a.background, a.current + + a.isFlushing.Store(true) + go func() { + defer a.isFlushing.Store(false) + for { + err := a.background.flush(incrDB) + if err == nil { + log.Info("Successfully flushed incremental state buffer to ancient db") + a.forwardTruncateSignal(a.background) + return + } + log.Error("Failed to flush incremental state buffer to ancient db", "error", err) + } + }() + + return nil +} + +// waitAndStopFlushing waits for ongoing flush operations to complete and stops flushing +func (a *asyncIncrStateBuffer) waitAndStopFlushing() { + // Stop monitoring goroutine immediately using done channel + close(a.done) + + // Stop flushing operations + a.stopFlushing.Store(true) + + // Wait for flush operations to complete + for a.isFlushing.Load() { + time.Sleep(time.Second) + log.Warn("Waiting for incremental state buffer flush to complete") + } +} + +// getTruncateSignal returns the truncate signal channel +func (a *asyncIncrStateBuffer) getTruncateSignal() <-chan uint64 { + return a.truncateChan +} + +// forwardTruncateSignal forwards truncate signal from buffer to truncate channel +func (a *asyncIncrStateBuffer) forwardTruncateSignal(buffer *incrNodeBuffer) { + select { + case lastStateID := <-buffer.getTruncateSignal(): + select { + case a.truncateChan <- lastStateID: + log.Info("Forwarded truncate signal", "stateID", lastStateID) + default: + log.Debug("Truncate channel full, skipping signal") + } + default: + } +} + +// incrNodeBuffer is a specialized buffer for incremental trie nodes +type incrNodeBuffer struct { + *buffer + root common.Hash + batchSize uint64 // Maximum flush batch size + stateIDArray [2]uint64 + blockNumberArray [2]uint64 + immutable uint64 // atomic flag: 1 = immutable, 0 = mutable + truncateSignal chan uint64 +} + +var emptyArray = [2]uint64{0, 0} + +// newIncrNodeBuffer creates a new incremental node buffer +func newIncrNodeBuffer(limit, batchSize uint64, nodes *nodeSet, states *stateSet, layers uint64) *incrNodeBuffer { + return &incrNodeBuffer{ + buffer: newBuffer(int(limit), nodes, states, layers), + batchSize: batchSize, + stateIDArray: emptyArray, + blockNumberArray: emptyArray, + immutable: 0, + truncateSignal: make(chan uint64, 1), + } +} + +// commit adds nodes and states to the buffer +func (c *incrNodeBuffer) commit(root common.Hash, nodes *nodeSet, states *stateSet, stateID, blockNumber uint64) error { + if atomic.LoadUint64(&c.immutable) == 1 { + return fmt.Errorf("cannot commit to immutable cache") + } + + c.buffer.commit(nodes, states) + c.root = root + if c.stateIDArray[0] == 0 && c.stateIDArray[1] == 0 { + c.stateIDArray[0] = stateID + c.stateIDArray[1] = stateID + c.blockNumberArray[0] = blockNumber + c.blockNumberArray[1] = blockNumber + } else { + c.stateIDArray[1] = stateID + c.blockNumberArray[1] = blockNumber + } + return nil +} + +// flush writes the immutable buffer to the incremental state db. +func (c *incrNodeBuffer) flush(incrDB *rawdb.IncrSnapDB) error { + if atomic.LoadUint64(&c.immutable) != 1 { + return fmt.Errorf("cannot flush mutable cache") + } + + if err := c.flushTrieNodes(incrDB); err != nil { + return err + } + if err := c.flushStates(incrDB); err != nil { + return err + } + + c.resetIncrBuffer() + return nil +} + +func (c *incrNodeBuffer) flushStates(incrDB *rawdb.IncrSnapDB) error { + var acc accounts + var storages []storage + currentSize := uint64(0) + + // Helper function to write current batch and reset + writeBatch := func() error { + if len(acc.AddrHashes) == 0 && len(storages) == 0 { + return nil + } + + s := statesData{ + RawStorageKey: c.states.rawStorageKey, + Acc: acc, + Storages: storages, + } + + if err := c.writeStatesToAncientDB(incrDB, s); err != nil { + return err + } + + // Reset for next batch + acc = accounts{} + storages = make([]storage, 0) + currentSize = 0 + return nil + } + + // Process account data + for addrHash, blob := range c.states.accountData { + accountSize := uint64(len(addrHash[:]) + len(blob)) + + if currentSize+accountSize > c.batchSize && (len(acc.AddrHashes) > 0 || len(storages) > 0) { + if err := writeBatch(); err != nil { + return err + } + } + + acc.AddrHashes = append(acc.AddrHashes, addrHash) + acc.Accounts = append(acc.Accounts, blob) + currentSize += accountSize + } + + // Process storage data + for addrHash, slots := range c.states.storageData { + keys := make([]common.Hash, 0, len(slots)) + vals := make([][]byte, 0, len(slots)) + storageSize := uint64(len(addrHash[:])) + + for key, val := range slots { + slotSize := uint64(len(key[:]) + len(val)) + + if currentSize+storageSize+slotSize > c.batchSize && (len(acc.AddrHashes) > 0 || len(storages) > 0 || len(keys) > 0) { + // Finish current storage if we have keys + if len(keys) > 0 { + storages = append(storages, storage{ + AddrHash: addrHash, + Keys: keys, + Vals: vals, + }) + } + + if err := writeBatch(); err != nil { + return err + } + + // Start new storage + keys = make([]common.Hash, 0, len(slots)) + vals = make([][]byte, 0, len(slots)) + storageSize = uint64(len(addrHash[:])) + } + + keys = append(keys, key) + vals = append(vals, val) + storageSize += slotSize + } + + if len(keys) > 0 { + storages = append(storages, storage{ + AddrHash: addrHash, + Keys: keys, + Vals: vals, + }) + currentSize += storageSize + } + } + + // Write remaining data + return writeBatch() +} + +func (c *incrNodeBuffer) flushTrieNodes(incrDB *rawdb.IncrSnapDB) error { + jn := make([]journalNodes, 0, len(c.nodes.storageNodes)+1) + totalSize := uint64(0) + + processNodes := func(owner common.Hash, nodes map[string]*trienode.Node) error { + entry := journalNodes{Owner: owner} + ownerSize := uint64(len(owner[:])) + nodesListSize := uint64(0) + + for path, node := range nodes { + entry.Nodes = append(entry.Nodes, journalNode{Path: []byte(path), Blob: node.Blob}) + + nodeSize := uint64(len([]byte(path)) + len(node.Blob)) + nodesListSize += nodeSize + currentEntrySize := ownerSize + nodesListSize + newTotalSize := totalSize + currentEntrySize + + if newTotalSize >= c.batchSize { + log.Info("Batch size limit reached during node iteration, flushing nodes to ancient db", + "newTotalSize", common.StorageSize(newTotalSize), "entryCount", len(jn)+1) + if err := c.writeTrieNodesToAncientDB(incrDB, append(jn, entry)); err != nil { + return err + } + + jn = make([]journalNodes, 0, len(c.nodes.storageNodes)+1) + totalSize = 0 + entry = journalNodes{Owner: owner} // Reset entry for remaining nodes + ownerSize = uint64(len(owner[:])) // Reset owner size + nodesListSize = 0 + } + } + + if len(entry.Nodes) > 0 { + jn = append(jn, entry) + entrySize := ownerSize + nodesListSize + totalSize += entrySize + } + + if totalSize >= c.batchSize { + log.Info("Batch size limit reached after adding entry, flushing nodes to ancient db", + "totalSize", common.StorageSize(totalSize), "entryCount", len(jn)) + if err := c.writeTrieNodesToAncientDB(incrDB, jn); err != nil { + return err + } + jn = make([]journalNodes, 0, len(c.nodes.storageNodes)+1) + totalSize = 0 + } + return nil + } + + if len(c.nodes.accountNodes) > 0 { + if err := processNodes(common.Hash{}, c.nodes.accountNodes); err != nil { + return err + } + } + for owner, subset := range c.nodes.storageNodes { + if err := processNodes(owner, subset); err != nil { + return err + } + } + + // Flush remaining nodes and states + if len(jn) > 0 { + log.Info("Flushing remaining trie nodes to ancient db", "entryCount", len(jn)) + if err := c.writeTrieNodesToAncientDB(incrDB, jn); err != nil { + return err + } + } + + log.Info("Flushed incremental state buffer to ancient db", "size", common.StorageSize(c.nodes.size)) + return nil +} + +// writeTrieNodesToAncientDB writes a batch of trie nodes to the incremental state db. +func (c *incrNodeBuffer) writeTrieNodesToAncientDB(incrDB *rawdb.IncrSnapDB, jn []journalNodes) error { + if len(jn) == 0 { + return nil + } + + ancients, _ := incrDB.GetStateFreezer().Ancients() + incrementalID := ancients + 1 + + encodedBatch, err := rlp.EncodeToBytes(jn) + if err != nil { + return fmt.Errorf("failed to RLP encode trie node batch: %v", err) + } + m := rawdb.IncrStateMetadata{ + Root: c.root, + HasStates: false, + NodeCount: uint64(len(jn)), + Layers: c.layers, + StateIDArray: c.stateIDArray, + BlockNumberArray: c.blockNumberArray, + } + metaBytes, err := rlp.EncodeToBytes(m) + if err != nil { + return fmt.Errorf("failed to RLP encode metadata: %v", err) + } + + if err = incrDB.WriteIncrTrieNodes(incrementalID, metaBytes, encodedBatch); err != nil { + log.Error("Failed to write incr trie nodes", "error", err, "ancients", ancients, + "incrementalID", incrementalID) + return err + } + + select { + case c.truncateSignal <- c.stateIDArray[1]: + log.Debug("Sent truncate signal after WriteIncrState", "incrementalID", incrementalID) + default: + log.Debug("Truncate signal channel full, skipping signal") + } + + log.Info("Wrote incr trie nodes to ancient db", "incrementalID", incrementalID, "nodeCount", len(jn), + "layers", c.layers, "nodesSize", common.StorageSize(len(encodedBatch)), "stateIDArray", c.stateIDArray, + "blockNumberArray", c.blockNumberArray) + return nil +} + +// writeStatesToAncientDB writes a batch of states to the incremental state db. +func (c *incrNodeBuffer) writeStatesToAncientDB(incrDB *rawdb.IncrSnapDB, s statesData) error { + ancients, _ := incrDB.GetStateFreezer().Ancients() + incrementalID := ancients + 1 + + encodedBatch, err := rlp.EncodeToBytes(s) + if err != nil { + return fmt.Errorf("failed to RLP encode trie node batch: %v", err) + } + m := rawdb.IncrStateMetadata{ + Root: c.root, + HasStates: true, + NodeCount: 0, + Layers: c.layers, + StateIDArray: c.stateIDArray, + BlockNumberArray: c.blockNumberArray, + } + metaBytes, err := rlp.EncodeToBytes(m) + if err != nil { + return fmt.Errorf("failed to RLP encode metadata: %v", err) + } + + if err = incrDB.WriteIncrState(incrementalID, metaBytes, encodedBatch); err != nil { + log.Error("Failed to write incr state", "error", err, "ancients", ancients, + "incrementalID", incrementalID) + return err + } + + log.Info("Wrote incr state batch to ancient db", "incrementalID", incrementalID, + "statesSize", common.StorageSize(len(encodedBatch))) + return nil +} + +// resetIncrBuffer resets the incr buffer +func (c *incrNodeBuffer) resetIncrBuffer() { + atomic.StoreUint64(&c.immutable, 0) + c.reset() + c.root = common.Hash{} + c.stateIDArray = emptyArray + c.blockNumberArray = emptyArray +} + +// getTruncateSignal returns the truncate signal channel +func (c *incrNodeBuffer) getTruncateSignal() <-chan uint64 { + return c.truncateSignal +} diff --git a/triedb/pathdb/buffer.go b/triedb/pathdb/buffer.go index 138962110f..0bc30951f7 100644 --- a/triedb/pathdb/buffer.go +++ b/triedb/pathdb/buffer.go @@ -207,3 +207,37 @@ func (b *buffer) waitFlush() error { <-b.done return b.flushErr } + +// flushIncrSnapshot persists incr trie nodes and states to disk. +func (b *buffer) flushIncrSnapshot(root common.Hash, db ethdb.KeyValueStore, freezer ethdb.AncientWriter, progress []byte, + id uint64) error { + var ( + start = time.Now() + batch = db.NewBatchWithSize((b.nodes.dbsize() + b.states.dbsize()) * 11 / 10) // extra 10% for potential pebble internal stuff + ) + if freezer != nil { + if err := freezer.SyncAncient(); err != nil { + return err + } + } + var nodes, accs, slots int + if b.nodes != nil { + nodes = b.nodes.write(batch, nil) + rawdb.WritePersistentStateID(batch, id) + } + if b.states != nil { + accs, slots = b.states.write(batch, progress, nil) + rawdb.WriteSnapshotRoot(batch, root) + } + + // Flush all mutations in a single batch + size := batch.ValueSize() + if err := batch.Write(); err != nil { + return err + } + + b.reset() + log.Info("Persisted buffer content", "nodes", nodes, "accounts", accs, "slots", slots, + "bytes", common.StorageSize(size), "elapsed", common.PrettyDuration(time.Since(start))) + return nil +} diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go index 0f6280fb8b..71ea3413ba 100644 --- a/triedb/pathdb/database.go +++ b/triedb/pathdb/database.go @@ -22,11 +22,13 @@ import ( "fmt" "io" "os" + "path/filepath" "sort" "strconv" "sync" "time" + "github.com/cockroachdb/pebble" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" @@ -140,8 +142,15 @@ type Config struct { NoAsyncFlush bool // Flag whether the background buffer flushing is allowed NoAsyncGeneration bool // Flag whether the background generation is allowed - JournalFilePath string - JournalFile bool + JournalFilePath string // The path of journal file + JournalFile bool // Flag whether store memory diffLayer into file + + EnableIncr bool // Flag whether the freezer db stores incr block and state history + MergeIncr bool // Flag to merge incr snapshots + IncrHistory uint64 // Amount of block and state history stored in incr freezer db + IncrHistoryPath string // The path to store incr block and chain files + IncrStateBuffer uint64 // Maximum memory allowance (in bytes) for incr state buffer + IncrKeptBlocks uint64 // Amount of block kept in incr snapshot } // sanitize checks the provided user configurations and changes anything that's @@ -237,6 +246,7 @@ type Database struct { freezer ethdb.ResettableAncientStore // Freezer for storing trie histories, nil possible in tests lock sync.RWMutex // Lock to prevent mutations from happening at the same time indexer *historyIndexer // History indexer + incr *incrManager // used to store incremental data: block, state and contract codes } // New attempts to load an already existing layer from a persistent key-value @@ -273,6 +283,16 @@ func New(diskdb ethdb.Database, config *Config, isVerkle bool) *Database { if err := db.repairHistory(); err != nil { log.Crit("Failed to repair state history", "err", err) } + + if db.config.EnableIncr { + db.checkIncrConfig() + if err := db.repairIncrStore(); err != nil { + log.Crit("Failed to repair incremental history", "error", err) + } + // Start incremental store async workers + db.incr.Start() + } + // Disable database in case node is still in the initial state sync stage. if rawdb.ReadSnapSyncStatusFlag(diskdb) == rawdb.StateSyncRunning && !db.readOnly { if err := db.Disable(); err != nil { @@ -283,8 +303,10 @@ func New(diskdb ethdb.Database, config *Config, isVerkle bool) *Database { // mandatory. This ensures that uncovered flat states are not accessed, // even if background generation is not allowed. If permitted, the generation // might be scheduled. - if err := db.setStateGenerator(); err != nil { - log.Crit("Failed to setup the generator", "err", err) + if !config.MergeIncr { + if err := db.setStateGenerator(); err != nil { + log.Crit("Failed to setup the generator", "err", err) + } } // TODO (rjl493456442) disable the background indexing in read-only mode if db.freezer != nil && db.config.EnableStateIndexing { @@ -299,6 +321,13 @@ func New(diskdb ethdb.Database, config *Config, isVerkle bool) *Database { return db } +// SetStateGenerator sets state generator. +func (db *Database) SetStateGenerator() { + if err := db.setStateGenerator(); err != nil { + log.Crit("Failed to setup the generator", "err", err) + } +} + // repairHistory truncates leftover state history objects, which may occur due // to an unclean shutdown or other unexpected reasons. func (db *Database) repairHistory() error { @@ -413,6 +442,92 @@ func (db *Database) setStateGenerator() error { return nil } +func (db *Database) checkIncrConfig() { + ancientDir, err := db.diskdb.AncientDatadir() + if err != nil { + log.Crit("Failed to get ancient data dir", "err", err) + } + + if db.config.IncrHistoryPath == "" { + db.config.IncrHistoryPath = filepath.Join(ancientDir, rawdb.IncrementalPath) + } + if db.config.IncrHistory == 0 { + db.config.IncrHistory = 100000 + } + if db.config.IncrStateBuffer == 0 { + db.config.IncrStateBuffer = DefaultIncrStateBufferSize + } + if db.config.IncrKeptBlocks < DefaultKeptBlocks { + db.config.IncrKeptBlocks = DefaultKeptBlocks + } else { + if db.config.IncrKeptBlocks > db.config.IncrHistory { + db.config.IncrKeptBlocks = db.config.IncrHistory + log.Warn("IncrKeptBlocks shouldn't be greater than IncrHistory", "IncrHistory", db.config.IncrHistory, + "IncrKeptBlocks", db.config.IncrKeptBlocks) + } + } + + log.Info("Incr snapshot config", "IncrHistoryPath", db.config.IncrHistoryPath, "IncrHistory", db.config.IncrHistory, + "IncrStateBuffer", common.StorageSize(db.config.IncrStateBuffer), "IncrKeptBlocks", db.config.IncrKeptBlocks) +} + +// repairIncrStore init incremental manager and align incr chain and state freezer. +func (db *Database) repairIncrStore() error { + if err := db.initIncrManager(); err != nil { + log.Error("Failed to initialize incr manager", "error", err) + return err + } + + // Get disk layer state ID for validation + diskLayerID := db.tree.bottom().stateID() + if diskLayerID == 0 { + stateAncients, err := db.incr.incrDB.GetStateFreezer().Ancients() + if err != nil { + log.Error("Failed to retrieve head of incr state history", "error", err) + return err + } + + if stateAncients != 0 { + block, err := db.GetStartBlock() + if err != nil { + log.Error("Failed to retrieve start block", "error", err) + return err + } + if err = db.incr.incrDB.ResetAllIncr(block); err != nil { + log.Error("Failed to reset incremental state histories", "error", err) + return err + } + log.Warn("Reset all incremental state histories") + } + return nil + } + + // Align incremental data with disk layer + return db.alignIncrData(diskLayerID) +} + +func (db *Database) GetStartBlock() (uint64, error) { + var block uint64 + if dl := db.tree.bottomDiffLayer(); dl != nil { + // use the bottom diff layer block number + block = dl.block + } else if db.tree.bottom() != nil { + // force kill case, use the block next to the disk layer block + disk := db.tree.bottom() + var m meta + blob := rawdb.ReadStateHistoryMeta(db.freezer, disk.id) + if err := m.decode(blob); err != nil { + log.Error("Failed to decode state histories", "err", err) + return 0, err + } + block = m.block + 1 + } else { + // start from genesis + block = 1 + } + return block, nil +} + // Update adds a new layer into the tree, if that can be linked to an existing // old parent. It is disallowed to insert a disk layer (the origin of all). Apart // from that this function will flatten the extra diff layers at bottom into disk @@ -668,6 +783,23 @@ func (db *Database) Close() error { if db.freezer == nil { return nil } + + if db.config.EnableIncr { + log.Info("Closing incremental store") + + // Wait for all async write tasks to complete before closing + if db.incr != nil { + log.Info("Waiting for async write tasks to complete", "pending", db.incr.GetQueueLength()) + db.incr.LogStats() + db.incr.Stop() + } + + if err := db.incr.incrDB.Close(); err != nil { + log.Error("Failed to close incremental db", "err", err) + return err + } + } + return db.freezer.Close() } @@ -836,3 +968,363 @@ func (db *Database) StorageIterator(root common.Hash, account common.Hash, seek } return newFastStorageIterator(db, root, account, seek) } + +// IsIncrEnabled returns true if incremental is enabled, otherwise false. +func (db *Database) IsIncrEnabled() bool { + return db.config.EnableIncr +} + +// MergeIncrState merges incremental state data into local data. +func (db *Database) MergeIncrState(incrDir string) error { + incrStateFreezer, err := rawdb.OpenIncrStateFreezer(incrDir, true) + if err != nil { + log.Error("Failed to open incremental state freezer", "error", err) + return err + } + defer incrStateFreezer.Close() + + incrAncients, _ := incrStateFreezer.Ancients() + tail, _ := incrStateFreezer.Tail() + log.Info("Merged incr state freezer info", "ancients", incrAncients, "tail", tail) + + incrStateMeta := rawdb.ReadIncrStateHistoryMeta(incrStateFreezer, incrAncients) + if incrStateMeta == nil { + log.Error("Failed to read incremental chain freezer", "error", err) + return err + } + if err = rawdb.ResetStateTableToNewStartPoint(db.freezer, incrStateMeta.StateIDArray[1]); err != nil { + log.Error("Failed to reset state freezer with new start point", "error", err, + "lastStateID", incrStateMeta.StateIDArray[1]) + return err + } + + dl := db.tree.bottom() + err = dl.mergeIncrNodesWithStates(db.diskdb, db.freezer, incrStateFreezer, tail+1, incrAncients) + if err != nil { + log.Error("Failed to merge incremental trie nodes", "error", err) + return err + } + + root, err := db.hasher(rawdb.ReadAccountTrieNode(db.diskdb, nil)) + if err != nil { + log.Crit("Failed to compute node hash", "err", err) + } + dl = newDiskLayer(root, rawdb.ReadPersistentStateID(db.diskdb), db, nil, nil, newBuffer(db.config.WriteBufferSize, nil, nil, 0), nil) + db.tree = newLayerTree(dl) + log.Info("Completed merging incr state") + return nil +} + +// WriteContractCodes wrote codes into incremental chain freezer +func (db *Database) WriteContractCodes(codes map[common.Address]rawdb.ContractCode) error { + return db.incr.incrDB.WriteIncrContractCodes(codes) +} + +// incrInfo holds information about incremental data state +type incrInfo struct { + stateFreezer ethdb.ResettableAncientStore + chainFreezer ethdb.ResettableAncientStore + stateAncients uint64 + chainAncients uint64 + lastChainStateID uint64 + lastStateID uint64 + lastStateBlock uint64 +} + +func (info *incrInfo) isEmpty() bool { + return info.stateAncients == 0 || info.chainAncients == 0 +} + +// initIncrManager initializes the incremental manager +func (db *Database) initIncrManager() error { + block, err := db.GetStartBlock() + if err != nil { + return err + } + + incrDB, err := rawdb.NewIncrSnapDB(db.config.IncrHistoryPath, db.readOnly, block, db.config.IncrHistory) + if err != nil { + log.Error("Failed to open incremental db", "error", err) + return err + } + + db.incr = NewIncrManager(db, incrDB) + return nil +} + +// loadIncrInfo loads current incremental data information +func (db *Database) loadIncrInfo() (*incrInfo, error) { + info := &incrInfo{} + + info.stateFreezer = db.incr.incrDB.GetStateFreezer() + info.chainFreezer = db.incr.incrDB.GetChainFreezer() + + var err error + info.stateAncients, err = info.stateFreezer.Ancients() + if err != nil { + log.Error("Failed to retrieve head of incr state history", "error", err) + return nil, err + } + info.chainAncients, err = info.chainFreezer.Ancients() + if err != nil { + log.Error("Failed to retrieve head of incr chain history", "error", err) + return nil, err + } + + // Load last state info if data exists + if !info.isEmpty() { + // Read last chain state ID + info.lastChainStateID, err = rawdb.ReadIncrChainMapping(info.chainFreezer, info.chainAncients-1) + if err != nil { + log.Error("Failed to read incr chain mapping", "error", err) + return nil, err + } + + // Read last state metadata + metadata := rawdb.ReadIncrStateHistoryMeta(info.stateFreezer, info.stateAncients) + if metadata == nil { + return nil, fmt.Errorf("last incr state history not found: %d", info.stateAncients) + } + + info.lastStateID = metadata.StateIDArray[1] + info.lastStateBlock = metadata.BlockNumberArray[1] + log.Info("Incr data info", "lastChainStateID", info.lastChainStateID, + "lastStateID", info.lastStateID, "lastChainBlock", info.chainAncients-1, + "lastStateBlock", info.lastStateBlock) + } + + return info, nil +} + +func (db *Database) alignIncrData(diskLayerID uint64) error { + // Load current incremental data info + info, err := db.loadIncrInfo() + if err != nil { + return err + } + + var recordFirstStateID uint64 + data, err := db.incr.incrDB.GetKVDB().Get(rawdb.FirstStateID) + if err != nil { + if errors.Is(err, pebble.ErrNotFound) { + db.incr.incrDB.WriteFirstStateID(diskLayerID) + recordFirstStateID = diskLayerID + } else { + return err + } + } else { + recordFirstStateID = binary.BigEndian.Uint64(data) + } + + // Get start block to avoid duplicate data writing + startBlock, err := db.GetStartBlock() + if err != nil { + log.Error("Failed to get start block", "error", err) + return err + } + + log.Info("Incremental data alignment check", "stateAncients", info.stateAncients, + "chainAncients", info.chainAncients, "diskLayerID", diskLayerID, "startBlock", startBlock, "recordFirstStateID", recordFirstStateID) + + if info.isEmpty() { + log.Info("Force kill with empty data") + if info.chainAncients == 0 && info.stateAncients == 0 { + if err = db.setBlockCount(startBlock, 0); err != nil { + return err + } + return nil + } + if diskLayerID > recordFirstStateID { + h, err := readHistory(db.freezer, recordFirstStateID) + if err != nil { + return err + } + + if err = db.Recover(h.meta.root); err != nil { + log.Error("Failed to recover state after force kill", "root", h.meta.root, "stateID", info.lastStateID, "error", err) + } else { + log.Info("Successfully recovered state after force kill", "root", h.meta.root, "stateID", recordFirstStateID) + } + + db.incr.duplicateEndBlock = h.meta.block + } else { + // use current dir block + start, _, err := db.incr.incrDB.ParseCurrDirBlockNumber() + if err != nil { + return err + } + db.incr.duplicateEndBlock = start - 1 + log.Info("recordFirstStateID is bigger", "start", start) + } + + if err = info.stateFreezer.Reset(); err != nil { + return err + } + if err = info.chainFreezer.Reset(); err != nil { + return err + } + if err = db.incr.resetIncrChainFreezer(db.diskdb, db.incr.duplicateEndBlock+1); err != nil { + return err + } + if err = db.setBlockCount(startBlock, 0); err != nil { + return err + } + return nil + } + + log.Info("Both incr chain and state have data, comparing for alignment", + "lastChainStateID", info.lastChainStateID, "lastStateID", info.lastStateID, + "lastStateBlock", info.lastStateBlock, "chainAncients", info.chainAncients) + + // handle force kill with incr state and chain data + if info.chainAncients-1 != info.lastStateBlock { + log.Info("Force kill with data") + if diskLayerID > info.lastStateID { + h, err := readHistory(db.freezer, info.lastStateID) + if err != nil { + return err + } + if h.meta.block != info.lastStateBlock { + return fmt.Errorf("history block [%d] is unequal to incr recorded block [%d]", h.meta.block, info.lastStateBlock) + } + + if err = db.Recover(h.meta.root); err != nil { + log.Error("Failed to recover state after force kill", "root", h.meta.root, "stateID", info.lastStateID, "error", err) + } else { + log.Info("Successfully recovered state after force kill", "root", h.meta.root, "stateID", info.lastStateID) + } + + db.incr.duplicateEndBlock = h.meta.block + } else { + db.incr.duplicateEndBlock = info.lastStateBlock + } + + if err = info.chainFreezer.Reset(); err != nil { + return err + } + if err = db.incr.resetIncrChainFreezer(db.diskdb, info.lastStateBlock); err != nil { + return err + } + if err = db.setBlockCount(startBlock, info.lastStateBlock); err != nil { + return err + } + return nil + } + + // Find the minimum state ID to ensure consistency + var finalStateID, finalBlock uint64 + if info.lastChainStateID < info.lastStateID { + finalStateID = info.lastChainStateID + finalBlock = info.chainAncients - 1 + } else if info.lastStateID < info.lastChainStateID { + finalStateID = info.lastStateID + finalBlock = info.lastStateBlock + } else { + finalStateID = info.lastStateID + finalBlock = info.lastStateBlock + } + + if finalStateID < diskLayerID { + return fmt.Errorf("Final state ID is less than disk layer ID, diskLayerID: %d, finalStateID: %d", diskLayerID, finalStateID) + } + + // Truncate incr state freezer + if err = db.truncateIncrStateFreezer(info, finalStateID); err != nil { + return err + } + // Truncate incr chain freezer + if err = db.truncateIncrChainFreezer(info, finalBlock); err != nil { + return err + } + + if err = db.setBlockCount(startBlock, finalBlock); err != nil { + return err + } + return nil +} + +// truncateIncrStateFreezer truncates the incr state freezer to align with final state +func (db *Database) truncateIncrStateFreezer(info *incrInfo, finalStateID uint64) error { + truncatePos := info.stateAncients + + // Find the correct truncate position if needed + if info.lastChainStateID < info.lastStateID { + for index := info.stateAncients; index >= 1; index-- { + metadata := rawdb.ReadIncrStateHistoryMeta(info.stateFreezer, index) + if metadata == nil { + return fmt.Errorf("incr state history not found: %d", index) + } + + if finalStateID >= metadata.StateIDArray[0] && finalStateID <= metadata.StateIDArray[1] { + truncatePos = index + break + } + } + } + + pruned, err := truncateFromHead(db.diskdb, info.stateFreezer, truncatePos) + if err != nil { + log.Error("Failed to truncate incr state histories", "error", err) + return err + } + if pruned != 0 { + log.Warn("Truncated incr state histories to align with chain", + "number", pruned, "finalStateID", finalStateID) + } + return nil +} + +// truncateIncrChainFreezer truncates the incr chain freezer to align with final block +func (db *Database) truncateIncrChainFreezer(info *incrInfo, finalBlock uint64) error { + chainTail, err := info.chainFreezer.Tail() + if err != nil { + log.Error("Failed to retrieve tail of incr chain history", "error", err) + return err + } + + if finalBlock < chainTail { + if err = info.chainFreezer.Reset(); err != nil { + log.Error("Failed to reset incr chain history", "error", err) + return err + } + log.Info("Reset incr chain history due to truncation is out of range", + "finalBlock", finalBlock, "tail", chainTail) + return nil + } + + pruned, err := truncateIncrChainFreezerFromHead(info.chainFreezer, finalBlock) + if err != nil { + log.Error("Failed to truncate incr chain histories", "error", err) + return err + } + if pruned != 0 { + log.Warn("Truncated incr chain histories to align with state", + "number", pruned, "finalBlock", finalBlock) + } + return nil +} + +func (db *Database) setBlockCount(startBlock, currBlock uint64) error { + dirStartBlock, dirEndBlock, err := db.incr.incrDB.ParseCurrDirBlockNumber() + if err != nil { + return err + } + + if startBlock > dirEndBlock+1 { + return fmt.Errorf("start block [%d] is beyond dir end block [%d], please reset incr dir", startBlock, dirEndBlock) + } + + var blockCount uint64 + if currBlock < dirStartBlock { + blockCount = 0 + } else if currBlock >= dirStartBlock && currBlock <= dirEndBlock { + blockCount = currBlock - dirStartBlock + } else { + blockCount = db.config.IncrHistory + } + + log.Info("SetBlockCount", "blockCount", blockCount, "dirStartBlock", dirStartBlock, "dirEndBlock", dirEndBlock, + "currBlock", currBlock) + db.incr.incrDB.SetBlockCount(blockCount) + return nil +} diff --git a/triedb/pathdb/disklayer.go b/triedb/pathdb/disklayer.go index 3814f91e81..6ad4f23fc8 100644 --- a/triedb/pathdb/disklayer.go +++ b/triedb/pathdb/disklayer.go @@ -19,6 +19,7 @@ package pathdb import ( "bytes" "fmt" + "strings" "sync" "time" @@ -26,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" ) @@ -362,6 +364,15 @@ func (dl *diskLayer) commit(bottom *diffLayer, force bool) (*diskLayer, error) { } } } + + if dl.db.config.EnableIncr { + err := dl.commitIncrData(bottom) + if err != nil { + log.Error("Failed to commit incremental data after retries", "err", err) + return nil, err + } + } + // Mark the diskLayer as stale before applying any mutations on top. dl.stale = true @@ -447,7 +458,7 @@ func (dl *diskLayer) commit(bottom *diffLayer, force bool) (*diskLayer, error) { } // To remove outdated history objects from the end, we set the 'tail' parameter // to 'oldest-1' due to the offset between the freezer index and the history ID. - if overflow { + if overflow && !dl.db.config.EnableIncr { pruned, err := truncateFromTail(ndl.db.diskdb, ndl.db.freezer, oldest-1) if err != nil { return nil, err @@ -622,3 +633,96 @@ func (dl *diskLayer) terminate() error { } return nil } + +// commitIncrData attempts to commit incremental data with retry mechanism. +func (dl *diskLayer) commitIncrData(bottom *diffLayer) error { + const ( + maxRetries = 5 + baseDelay = 100 * time.Millisecond + maxDelay = 5 * time.Second + ) + + var lastErr error + for attempt := 0; attempt < maxRetries; attempt++ { + err := dl.db.incr.commit(bottom) + if err == nil { + if attempt > 0 { + log.Info("Incremental data commit succeeded after retries", + "block", bottom.block, "stateID", bottom.stateID(), "attempts", attempt+1) + } + return nil + } + lastErr = err + + // Check if this is a queue full error + if strings.Contains(err.Error(), "task queue is full") { + // Calculate delay with exponential backoff + delay := baseDelay * time.Duration(1< maxDelay { + delay = maxDelay + } + + // Check if directory switch is in progress + switching := dl.db.incr.incrDB.IsSwitching() + queueUsage := dl.db.incr.GetQueueUsageRate() + log.Warn("Task queue is full, retrying after delay", "block", bottom.block, + "stateID", bottom.stateID(), "attempt", attempt+1, "maxRetries", maxRetries, "delay", delay, + "switching", switching, "queueUsage", fmt.Sprintf("%.1f%%", queueUsage)) + + // If the directory switch is in progress, use longer delay + if switching { + delay = maxDelay + log.Info("Directory switch detected, using longer delay", "delay", delay) + } + time.Sleep(delay) + continue + } + + log.Error("Non-recoverable error committing incremental data", + "block", bottom.block, "stateID", bottom.stateID(), "err", err) + incrCommitErrorMeter.Mark(1) + return err + } + + incrCommitErrorMeter.Mark(1) + log.Error("Failed to commit incremental data after all retries", + "block", bottom.block, "stateID", bottom.stateID(), "maxRetries", maxRetries, "finalError", lastErr) + dl.db.incr.LogStats() + return fmt.Errorf("failed to commit incremental data after %d retries: %w", maxRetries, lastErr) +} + +// mergeIncrNodesWithStates merges incr trie nodes and states into local data. +func (dl *diskLayer) mergeIncrNodesWithStates(db ethdb.KeyValueStore, freezer ethdb.AncientWriter, + incrFreezer ethdb.ResettableAncientStore, start, end uint64) error { + persistID := rawdb.ReadPersistentStateID(db) + log.Info("Ancient db meta info", "persistent_state_id", persistID, "start", start, "end", end) + + for i := start; i <= end; i++ { + m := rawdb.ReadIncrStateHistoryMeta(incrFreezer, i) + if m == nil { + return fmt.Errorf("not found incr state history meta: %d", i) + } + var combined *buffer + if !m.HasStates { + nodes, err := readIncrTrieNodes(incrFreezer, i) + if err != nil { + return err + } + combined = dl.buffer.commit(nodes, newStates(nil, nil, false)) + } else { + states, err := readIncrStatesData(incrFreezer, i) + if err != nil { + return err + } + combined = dl.buffer.commit(newNodeSet(nil), states) + } + + if err := combined.flushIncrSnapshot(m.Root, db, freezer, nil, m.StateIDArray[1]); err != nil { + return err + } + log.Info("Flush incr nodes and states", "layers", m.Layers, "root", m.Root, "hasStates", m.HasStates) + } + + log.Info("Finished merging incremental state history") + return nil +} diff --git a/triedb/pathdb/history.go b/triedb/pathdb/history.go index 47f224170d..60081dd4b0 100644 --- a/triedb/pathdb/history.go +++ b/triedb/pathdb/history.go @@ -678,3 +678,58 @@ func truncateFromTail(db ethdb.Batcher, store ethdb.AncientStore, ntail uint64) } return int(ntail - otail), nil } + +// truncateIncrChainFreezerFromHead removes the extra incr chain histories from the head with the given +// parameters. It returns the number of items removed from the head. +func truncateIncrChainFreezerFromHead(store ethdb.AncientStore, nhead uint64) (int, error) { + ohead, err := store.Ancients() + if err != nil { + return 0, err + } + otail, err := store.Tail() + if err != nil { + return 0, err + } + // Ensure that the truncation target falls within the specified range. + if ohead < nhead || nhead < otail { + return 0, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", otail, ohead, nhead) + } + // Short circuit if nothing to truncate. + if ohead == nhead { + return 0, nil + } + ohead, err = store.TruncateHead(nhead) + if err != nil { + return 0, err + } + return int(ohead - nhead), nil +} + +// truncateIncrChainFreezerFromTail removes the extra incremental chain histories from the tail +// with the given parameters. It returns the number of items removed from the tail. +func truncateIncrChainFreezerFromTail(store ethdb.AncientStore, ntail uint64) (int, error) { + ohead, err := store.Ancients() + if err != nil { + return 0, err + } + otail, err := store.Tail() + if err != nil { + return 0, err + } + if ohead == otail { + return 0, nil + } + // Ensure that the truncation target falls within the specified range. + if otail > ntail || ntail > ohead { + return 0, fmt.Errorf("out of range, tail: %d, head: %d, target: %d", otail, ohead, ntail) + } + // Short circuit if nothing to truncate. + if otail == ntail { + return 0, nil + } + otail, err = store.TruncateTail(ntail) + if err != nil { + return 0, err + } + return int(ntail - otail), nil +} diff --git a/triedb/pathdb/incr_manager.go b/triedb/pathdb/incr_manager.go new file mode 100644 index 0000000000..3e9b394cbc --- /dev/null +++ b/triedb/pathdb/incr_manager.go @@ -0,0 +1,682 @@ +package pathdb + +import ( + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie/trienode" +) + +const ( + // The default kept blocks in incremental chain freezer: 1024. + DefaultKeptBlocks = 1024 + + // Number of blocks after which to save the parlia snapshot to the database + parliaSnapCheckpointInterval = 1024 + + // The default number of blocks and state history stored in incr freezer db + DefaultBlockInterval = 100000 + + // The default memory allowance for incremental state buffer: 6GB + DefaultIncrStateBufferSize = 6 * 1024 * 1024 * 1024 + + // The maximum size of the batch to be flushed into the ancient db: 2GB + defaultFlushBatchSize = 2 * 1024 * 1024 * 1024 +) + +// writeStats tracks write operation statistics +type writeStats struct { + totalTasks uint64 + completedTasks uint64 + failedTasks uint64 + queueLength int32 + avgProcessTime uint64 // Average processing time in nanoseconds + maxProcessTime uint64 // Maximum processing time in nanoseconds + totalProcessTime uint64 // Total processing time for calculating average + lastResetTime time.Time // Last time stats were reset +} + +// UpdateProcessTime updates processing time statistics +func (ws *writeStats) UpdateProcessTime(duration time.Duration) { + durationNs := uint64(duration.Nanoseconds()) + + // Update max processing time + for { + current := atomic.LoadUint64(&ws.maxProcessTime) + if durationNs <= current || atomic.CompareAndSwapUint64(&ws.maxProcessTime, current, durationNs) { + break + } + } + + // Update total processing time for average calculation + atomic.AddUint64(&ws.totalProcessTime, durationNs) + + // Calculate and update average + completed := atomic.LoadUint64(&ws.completedTasks) + if completed > 0 { + avg := atomic.LoadUint64(&ws.totalProcessTime) / completed + atomic.StoreUint64(&ws.avgProcessTime, avg) + } +} + +// incrManager manages incremental state storage with async write capability +type incrManager struct { + db *Database // Reference to parent Database for accessing diskdb + incrDB *rawdb.IncrSnapDB + chainConfig *params.ChainConfig + + // used to skip duplicate blocks until end block + duplicateEndBlock uint64 + + // Async write control + writeQueue chan *diffLayer + stopChan chan struct{} + wg sync.WaitGroup + + stats writeStats + started bool + lock sync.RWMutex + + // Async incremental state buffer + asyncBuffer *asyncIncrStateBuffer + bufferLimit uint64 // Memory limit for buffer +} + +// NewIncrManager creates a new incremental manager with async write capability +func NewIncrManager(db *Database, incrDB *rawdb.IncrSnapDB) *incrManager { + im := &incrManager{ + db: db, + incrDB: incrDB, + writeQueue: make(chan *diffLayer, 100), + stopChan: make(chan struct{}), + started: false, + bufferLimit: db.config.IncrStateBuffer, + } + + chainConfig, err := rawdb.GetChainConfig(db.diskdb) + if err != nil { + log.Crit("Failed to get chain config", "error", err) + } + im.chainConfig = chainConfig + + // Initialize async incremental state buffer + im.asyncBuffer = newAsyncIncrStateBuffer(im.bufferLimit, defaultFlushBatchSize) + return im +} + +// Start starts the async write workers and directory switch checker +func (im *incrManager) Start() { + im.lock.Lock() + defer im.lock.Unlock() + + if im.started { + log.Warn("Incremental store already started") + return + } + + im.wg.Add(1) + go im.worker() + + im.wg.Add(1) + go im.listenTruncateSignal() + + im.started = true + log.Info("Incremental store async worker started") +} + +// Stop stops the async write workers and directory switch checker +func (im *incrManager) Stop() { + im.lock.Lock() + defer im.lock.Unlock() + + if !im.started { + return + } + + log.Info("Stopping incremental store", "pending_tasks", im.GetQueueLength()) + + // Set a timeout for graceful shutdown + shutdownTimeout := 30 * time.Second + shutdownComplete := make(chan struct{}) + + go func() { + im.drainQueue() + + // Stop workers + close(im.stopChan) + im.wg.Wait() + + close(shutdownComplete) + }() + + // Wait for graceful shutdown or timeout + select { + case <-shutdownComplete: + log.Info("Incremental store stopped gracefully") + case <-time.After(shutdownTimeout): + log.Warn("Incremental store shutdown timeout, forcing stop", + "timeout", shutdownTimeout, "remaining_tasks", im.GetQueueLength()) + } + + if im.asyncBuffer != nil { + if err := im.ForceFlushStateBuffer(); err != nil { + log.Crit("Failed to force flush data", "error", err) + } + } + + ancients, _ := im.incrDB.GetChainFreezer().Ancients() + _ = im.truncateExtraBlock(ancients - 1) + + im.started = false + im.LogStats() +} + +// listenTruncateSignal listens truncate state freezer and incr chain freezer signal. +func (im *incrManager) listenTruncateSignal() { + truncateTicker := time.NewTicker(time.Second * 3) + defer truncateTicker.Stop() + defer im.wg.Done() + + for { + select { + case <-truncateTicker.C: + ancients, err := im.incrDB.GetChainFreezer().Ancients() + if err != nil { + log.Error("Failed to get ancients in truncating", "error", err) + } + if ancients == 0 { + continue + } + if err = im.truncateExtraBlock(ancients - 1); err != nil { + continue + } + + case stateID := <-im.asyncBuffer.getTruncateSignal(): + if err := im.truncateStateFreezer(stateID); err != nil { + continue + } + + case <-im.stopChan: + log.Debug("Truncate signal listener stopped") + return + } + } +} + +// commit submits an async write task +func (im *incrManager) commit(bottom *diffLayer) error { + if !im.started { + return errors.New("incremental store not started") + } + + atomic.AddUint64(&im.stats.totalTasks, 1) + atomic.AddInt32(&im.stats.queueLength, 1) + + if im.incrDB.IsSwitching() { + log.Info("Directory switching in progress, waiting for completion", "block", bottom.block, "stateID", bottom.stateID()) + for im.incrDB.IsSwitching() { + time.Sleep(100 * time.Millisecond) + } + } + + select { + case im.writeQueue <- bottom: + return nil + + case <-im.stopChan: + atomic.AddInt32(&im.stats.queueLength, -1) + return errors.New("incremental store is stopping") + + default: + atomic.AddInt32(&im.stats.queueLength, -1) + log.Error("Task queue is full", "queueLength", im.GetQueueLength(), "block", bottom.block) + im.LogStats() + return fmt.Errorf("task queue is full (length %d, block %d)", im.GetQueueLength(), bottom.block) + } +} + +// worker processes write tasks asynchronously +func (im *incrManager) worker() { + defer im.wg.Done() + + for { + select { + case dl := <-im.writeQueue: + if dl == nil { + log.Crit("Diff layer is nil") + return + } + atomic.AddInt32(&im.stats.queueLength, -1) + + log.Debug("Worker received task", "block", dl.block, "stateID", dl.stateID(), "queueLength", im.GetQueueLength()) + + startTime := time.Now() + err := im.processWriteTask(dl) + processingTime := time.Since(startTime) + im.stats.UpdateProcessTime(processingTime) + if err != nil { + log.Error("Async write task failed", "block", dl.block, "stateID", dl.stateID(), + "processingTime", processingTime, "error", err) + incrProcessErrorMeter.Mark(1) + return + } else { + log.Debug("Task processed successfully", "block", dl.block, "stateID", dl.stateID(), "processingTime", processingTime) + } + + im.updateStats(err) + case <-im.stopChan: + log.Debug("Worker stopping") + return + } + } +} + +func (im *incrManager) processWriteTask(dl *diffLayer) error { + // skip already written incremental data + if dl.block <= im.duplicateEndBlock { + return nil + } + + // Write incremental data + if err := im.resetIncrChainFreezer(im.db.diskdb, dl.block); err != nil { + return err + } + if err := im.writeIncrData(dl); err != nil { + return err + } + + return nil +} + +// writeChainData writes incremental data: chain and state +func (im *incrManager) writeIncrData(dl *diffLayer) error { + head, err := im.incrDB.GetChainFreezer().Ancients() + if err != nil { + log.Error("Failed to get ancients from incr chain freezer", "error", err) + return err + } + + var startBlock uint64 + if dl.block == head { + startBlock = dl.block + } else if dl.block > head { + startBlock = head + } else { + if dl.block < head { + log.Crit("Block number should be greater than or equal to head", "blockNumber", dl.block, + "head", head) + } + } + + for i := startBlock; i <= dl.block; i++ { + // check if this block has state changes + isEmptyBlock := true + currStateID := dl.stateID() - 1 + if i == dl.block { + isEmptyBlock = false + currStateID = dl.stateID() + } + + if im.incrDB.Full() { + if err = im.truncateExtraBlock(i - 1); err != nil { + log.Error("Failed to truncate incr chain freezer", "blockNumber", i-1, "error", err) + return err + } + switched, err := im.incrDB.CheckAndInitiateSwitch(i, im) + if err != nil { + log.Error("Failed to check and switch incremental db", "error", err) + return err + } + + if switched { + im.asyncBuffer = newAsyncIncrStateBuffer(im.bufferLimit, defaultFlushBatchSize) + // record the first state id in pebble + im.incrDB.WriteFirstStateID(dl.stateID() - 1) + log.Info("Directory switch completed", "blockNumber", i, "stateID", dl.stateID()) + } + + if err = im.resetIncrChainFreezer(im.db.diskdb, i); err != nil { + log.Error("Failed to reset incr chain freezer", "blockNumber", i, "error", err) + return err + } + } + + if err = im.writeIncrBlock(im.db.diskdb, i, currStateID, isEmptyBlock); err != nil { + log.Error("Failed to write block data to freezer", "block", i, "stateID", dl.stateID(), "error", err) + return err + } + if !isEmptyBlock { + if err = im.writeIncrStateData(dl); err != nil { + log.Error("Failed to write incr state data", "block", dl.block, "stateID", dl.stateID(), "error", err) + return err + } + } + } + + log.Debug("Incremental block data processing completed", "startBlock", startBlock, "endBlock", dl.block, + "totalProcessed", dl.block-startBlock+1) + return nil +} + +func (im *incrManager) resetIncrChainFreezer(reader ethdb.Reader, blockNumber uint64) error { + blockHash := rawdb.ReadCanonicalHash(reader, blockNumber) + if blockHash == (common.Hash{}) { + return fmt.Errorf("canonical hash not found for block %d", blockNumber) + } + h, _ := rawdb.ReadHeaderAndRaw(reader, blockHash, blockNumber) + if h == nil { + return fmt.Errorf("block header missing, can't freeze block %d", blockNumber) + } + isCancun := im.chainConfig.IsCancun(h.Number, h.Time) + if err := rawdb.ResetEmptyIncrChainTable(im.incrDB.GetChainFreezer(), blockNumber, isCancun); err != nil { + log.Error("Failed to reset empty incr chain freezer", "block", blockNumber, "error", err) + return err + } + return nil +} + +// writeIncrStateData writes incr state data using async incremental state buffer +func (im *incrManager) writeIncrStateData(dl *diffLayer) error { + // Short circuit if states is not available + if dl.states == nil { + return errors.New("state change set is not available") + } + + start := time.Now() + // Commit to async buffer instead of direct write + im.asyncBuffer.commit(dl.root, dl.nodes, dl.states.stateSet, dl.stateID(), dl.block) + if err := im.asyncBuffer.flush(im.incrDB, false); err != nil { + return fmt.Errorf("failed to flush async incremental state buffer: %v", err) + } + log.Debug("Committed to incremental state buffer", "id", dl.stateID(), "block", dl.block, + "nodes_size", dl.nodes.size, "elapsed", common.PrettyDuration(time.Since(start))) + return nil +} + +func (im *incrManager) truncateExtraBlock(blockNumber uint64) error { + // always reload the incr chain freezer to + incrChainFreezer := im.incrDB.GetChainFreezer() + tail, err := incrChainFreezer.Tail() + if err != nil { + log.Error("Failed to get incr chain freezer tail", "error", err) + return err + } + if tail == 0 { + return nil + } + + // Only truncate if we have more blocks than the limit and there are actual blocks to truncate + if blockNumber-tail >= im.db.config.IncrKeptBlocks { + targetTail := blockNumber - im.db.config.IncrKeptBlocks + 1 + pruned, err := truncateIncrChainFreezerFromTail(incrChainFreezer, targetTail) + if err != nil { + log.Error("Failed to truncate chain freezer", "target_tail", targetTail, "current_tail", tail, + "blockNumber", blockNumber, "error", err) + return err + } + + if err = incrChainFreezer.SyncAncient(); err != nil { + log.Error("Failed to sync after incr chain freezer truncation", "error", err) + } else { + log.Debug("Successfully synced incr chain freezer after truncation") + } + log.Debug("Pruned incr chain history", "items", pruned, "target_tail", targetTail, "old_tail", tail) + } + return nil +} + +// updateStats updates operation statistics +func (im *incrManager) updateStats(err error) { + if err != nil { + atomic.AddUint64(&im.stats.failedTasks, 1) + } else { + atomic.AddUint64(&im.stats.completedTasks, 1) + } +} + +// drainQueue waits for all pending tasks to be processed +func (im *incrManager) drainQueue() { + for { + queueLen := im.GetQueueLength() + if queueLen == 0 { + break + } + log.Debug("Waiting for queue to drain", "remaining", queueLen) + time.Sleep(100 * time.Millisecond) + } +} + +// GetQueueLength returns the current number of pending tasks +func (im *incrManager) GetQueueLength() int { + return int(atomic.LoadInt32(&im.stats.queueLength)) +} + +// GetQueueCapacity returns the maximum queue capacity +func (im *incrManager) GetQueueCapacity() int { + return cap(im.writeQueue) +} + +// GetQueueUsageRate returns the queue usage rate as a percentage +func (im *incrManager) GetQueueUsageRate() float64 { + queueLen := im.GetQueueLength() + capacity := im.GetQueueCapacity() + if capacity == 0 { + return 0 + } + return float64(queueLen) / float64(capacity) * 100 +} + +// GetStats returns current statistics +func (im *incrManager) GetStats() (total, completed, failed uint64, queueLen int) { + return atomic.LoadUint64(&im.stats.totalTasks), + atomic.LoadUint64(&im.stats.completedTasks), + atomic.LoadUint64(&im.stats.failedTasks), + im.GetQueueLength() +} + +// LogStats logs current statistics +func (im *incrManager) LogStats() { + total := atomic.LoadUint64(&im.stats.totalTasks) + completed := atomic.LoadUint64(&im.stats.completedTasks) + failed := atomic.LoadUint64(&im.stats.failedTasks) + queueLen := im.GetQueueLength() + queueCapacity := im.GetQueueCapacity() + queueUsage := im.GetQueueUsageRate() + + avgProcessTime := atomic.LoadUint64(&im.stats.avgProcessTime) + maxProcessTime := atomic.LoadUint64(&im.stats.maxProcessTime) + + successRate := float64(0) + if total > 0 { + successRate = float64(completed) / float64(total) * 100 + } + + log.Info("Incremental store statistics", "total_tasks", total, "completed", completed, + "failed", failed, "pending", queueLen, "queue_capacity", queueCapacity, "queue_usage", fmt.Sprintf("%.1f%%", queueUsage), + "success_rate", fmt.Sprintf("%.2f%%", successRate), "avg_process_time", time.Duration(avgProcessTime), + "max_process_time", time.Duration(maxProcessTime), "switching", im.incrDB.IsSwitching(), + "uptime", time.Since(im.stats.lastResetTime).Round(time.Second)) +} + +// writeIncrBlock writes incremental block +func (im *incrManager) writeIncrBlock(reader ethdb.Reader, blockNumber, stateID uint64, isEmptyBlock bool) error { + blockHash := rawdb.ReadCanonicalHash(reader, blockNumber) + if blockHash == (common.Hash{}) { + return fmt.Errorf("canonical hash not found for block %d", blockNumber) + } + h, header := rawdb.ReadHeaderAndRaw(reader, blockHash, blockNumber) + if len(header) == 0 { + return fmt.Errorf("block header missing, can't freeze block %d", blockNumber) + } + body := rawdb.ReadBodyRLP(reader, blockHash, blockNumber) + if len(body) == 0 { + return fmt.Errorf("block body missing, can't freeze block %d", blockNumber) + } + receipts := rawdb.ReadReceiptsRLP(reader, blockHash, blockNumber) + if len(receipts) == 0 { + return fmt.Errorf("block receipts missing, can't freeze block %d", blockNumber) + } + td := rawdb.ReadTdRLP(reader, blockHash, blockNumber) + if len(td) == 0 { + return fmt.Errorf("total difficulty not found for block %d (hash: %s)", blockNumber, blockHash.Hex()) + } + + chainConfig, err := rawdb.GetChainConfig(reader) + if err != nil { + log.Error("Failed to get chain config", "error", err) + return err + } + // blobs is nil before cancun fork + var sidecars rlp.RawValue + isCancun := chainConfig.IsCancun(h.Number, h.Time) + if isCancun { + sidecars = rawdb.ReadBlobSidecarsRLP(reader, blockHash, blockNumber) + if len(sidecars) == 0 { + return fmt.Errorf("block blobs missing, can't freeze block %d", blockNumber) + } + } + + err = im.incrDB.WriteIncrBlockData(blockNumber, stateID, blockHash[:], header, body, receipts, td, sidecars, isEmptyBlock, isCancun) + if err != nil { + log.Error("Failed to write block data", "error", err) + return err + } + + if blockNumber%parliaSnapCheckpointInterval == 0 { + blob, err := reader.Get(append(rawdb.ParliaSnapshotPrefix, blockHash[:]...)) + if err != nil { + log.Error("Failed to get parlia snapshot", "error", err) + return err + } + im.incrDB.WriteParliaSnapshot(blockHash, blob) + log.Debug("Writing parlia snapshot into incremental", "blockNumber", blockNumber) + } + + log.Debug("Write one block data into incr chain freezer", "block", blockNumber, "hash", blockHash.Hex()) + return nil +} + +// ForceFlushStateBuffer forces all buffered data in asyncIncrStateBuffer to be flushed. +// This is called before directory switch to ensure data integrity +func (im *incrManager) ForceFlushStateBuffer() error { + if im.asyncBuffer == nil { + return nil + } + + // Check if there's any data to flush + if im.asyncBuffer.empty() { + log.Info("No buffered data to flush") + return nil + } + + // Force flush all data + if err := im.asyncBuffer.flush(im.incrDB, true); err != nil { + return fmt.Errorf("failed to force flush all buffered data: %v", err) + } + + im.asyncBuffer.waitAndStopFlushing() + + // Get the last stateID from the buffer + stateID := im.asyncBuffer.getFlushedStateID() + if stateID > 0 { + if err := im.truncateStateFreezer(stateID); err != nil { + log.Error("Failed to truncate state freezer", "stateID", stateID, "error", err) + return err + } + } + + return nil +} + +// truncateStateFreezer truncate state history by flushed stateID. +func (im *incrManager) truncateStateFreezer(stateID uint64) error { + tail, err := im.db.freezer.Tail() + if err != nil { + return nil + } + limit := im.db.config.StateHistory + if limit == 0 || stateID-tail <= limit { + log.Info("No truncation needed", "stateID", stateID, "tail", tail, "limit", limit) + return nil + } + + pruned, err := truncateFromTail(im.db.diskdb, im.db.freezer, stateID-limit) + if err != nil { + log.Error("Failed to truncate from tail", "error", err, "target", stateID) + return err + } + log.Info("Successfully truncated state history", "pruned_items", pruned, "target_tail", stateID) + return nil +} + +func readIncrTrieNodes(reader ethdb.AncientReader, id uint64) (*nodeSet, error) { + data, err := rawdb.ReadIncrStateTrieNodes(reader, id) + if err != nil { + log.Error("Failed to read incremental trie nodes", "id", id, "error", err) + return nil, err + } + + var decodedTrieNodes []journalNodes + if err = rlp.DecodeBytes(data, &decodedTrieNodes); err != nil { + log.Error("Failed to decode incremental trie nodes", "id", id, "error", err) + return nil, err + } + + return newNodeSet(flattenTrieNodes(decodedTrieNodes)), nil +} + +func readIncrStatesData(reader ethdb.AncientReader, id uint64) (*stateSet, error) { + data, err := rawdb.ReadIncrStatesData(reader, id) + if err != nil { + log.Error("Failed to read incr states data", "id", id, "error", err) + return nil, err + } + + var s statesData + if err = rlp.DecodeBytes(data, &s); err != nil { + log.Error("Failed to decode incr states data", "id", id, "error", err) + return nil, err + } + + accountSet := make(map[common.Hash][]byte) + for i := 0; i < len(s.Acc.AddrHashes); i++ { + accountSet[s.Acc.AddrHashes[i]] = s.Acc.Accounts[i] + } + + storageSet := make(map[common.Hash]map[common.Hash][]byte) + for _, entry := range s.Storages { + storageSet[entry.AddrHash] = make(map[common.Hash][]byte, len(entry.Keys)) + for i := 0; i < len(entry.Keys); i++ { + storageSet[entry.AddrHash][entry.Keys[i]] = entry.Vals[i] + } + } + + return newStates(accountSet, storageSet, s.RawStorageKey), nil +} + +// flattenTrieNodes returns a two-dimensional map for internal nodes. +func flattenTrieNodes(jn []journalNodes) map[common.Hash]map[string]*trienode.Node { + nodes := make(map[common.Hash]map[string]*trienode.Node) + for _, entry := range jn { + subset := make(map[string]*trienode.Node) + for _, n := range entry.Nodes { + if len(n.Blob) > 0 { + subset[string(n.Path)] = trienode.New(crypto.Keccak256Hash(n.Blob), n.Blob) + } else { + subset[string(n.Path)] = trienode.NewDeleted() + } + } + nodes[entry.Owner] = subset + } + return nodes +} diff --git a/triedb/pathdb/layertree.go b/triedb/pathdb/layertree.go index c0224dfed4..25cdca6b73 100644 --- a/triedb/pathdb/layertree.go +++ b/triedb/pathdb/layertree.go @@ -387,3 +387,28 @@ func (tree *layerTree) front() common.Hash { parent = children[0] } } + +// bottomDiffLayer returns the bottom-most diff layer in this tree. +// It returns the first diffLayer that is directly built on top of a diskLayer. +func (tree *layerTree) bottomDiffLayer() *diffLayer { + tree.lock.RLock() + defer tree.lock.RUnlock() + + bottomDisk := tree.bottom() + if bottomDisk == nil { + return nil + } + + // Find diffLayer that has bottomDisk as parent + for _, l := range tree.layers { + if dl, ok := l.(*diffLayer); ok { + if parent := dl.parentLayer(); parent != nil { + if parentDisk, ok := parent.(*diskLayer); ok && parentDisk.rootHash() == bottomDisk.rootHash() { + return dl + } + } + } + } + + return nil +} diff --git a/triedb/pathdb/metrics.go b/triedb/pathdb/metrics.go index 779f9d813f..b3a33fcdd5 100644 --- a/triedb/pathdb/metrics.go +++ b/triedb/pathdb/metrics.go @@ -81,6 +81,9 @@ var ( historicalAccountReadTimer = metrics.NewRegisteredResettingTimer("pathdb/history/account/reads", nil) historicalStorageReadTimer = metrics.NewRegisteredResettingTimer("pathdb/history/storage/reads", nil) + + incrProcessErrorMeter = metrics.NewRegisteredMeter("pathdb/incr/process/error", nil) + incrCommitErrorMeter = metrics.NewRegisteredMeter("pathdb/incr/commit/error", nil) ) // Metrics in generation diff --git a/triedb/pathdb/states.go b/triedb/pathdb/states.go index bc638a569e..567726f6dd 100644 --- a/triedb/pathdb/states.go +++ b/triedb/pathdb/states.go @@ -330,16 +330,29 @@ func (s *stateSet) updateSize(delta int) { s.size = 0 } +type statesData struct { + RawStorageKey bool + Acc accounts + Storages []storage +} + +type accounts struct { + AddrHashes []common.Hash + Accounts [][]byte +} + +type storage struct { + AddrHash common.Hash + Keys []common.Hash + Vals [][]byte +} + // encode serializes the content of state set into the provided writer. func (s *stateSet) encode(w io.Writer) error { // Encode accounts if err := rlp.Encode(w, s.rawStorageKey); err != nil { return err } - type accounts struct { - AddrHashes []common.Hash - Accounts [][]byte - } var enc accounts for addrHash, blob := range s.accountData { enc.AddrHashes = append(enc.AddrHashes, addrHash) @@ -349,12 +362,7 @@ func (s *stateSet) encode(w io.Writer) error { return err } // Encode storages - type Storage struct { - AddrHash common.Hash - Keys []common.Hash - Vals [][]byte - } - storages := make([]Storage, 0, len(s.storageData)) + storages := make([]storage, 0, len(s.storageData)) for addrHash, slots := range s.storageData { keys := make([]common.Hash, 0, len(slots)) vals := make([][]byte, 0, len(slots)) @@ -362,7 +370,7 @@ func (s *stateSet) encode(w io.Writer) error { keys = append(keys, key) vals = append(vals, val) } - storages = append(storages, Storage{ + storages = append(storages, storage{ AddrHash: addrHash, Keys: keys, Vals: vals, @@ -376,10 +384,6 @@ func (s *stateSet) decode(r *rlp.Stream) error { if err := r.Decode(&s.rawStorageKey); err != nil { return fmt.Errorf("load diff raw storage key flag: %v", err) } - type accounts struct { - AddrHashes []common.Hash - Accounts [][]byte - } var ( dec accounts accountSet = make(map[common.Hash][]byte) @@ -393,11 +397,6 @@ func (s *stateSet) decode(r *rlp.Stream) error { s.accountData = accountSet // Decode storages - type storage struct { - AddrHash common.Hash - Keys []common.Hash - Vals [][]byte - } var ( storages []storage storageSet = make(map[common.Hash]map[common.Hash][]byte)