diff --git a/XDCxDAO/interfaces.go b/XDCxDAO/interfaces.go index 066df40b0ab..2227e5e6846 100644 --- a/XDCxDAO/interfaces.go +++ b/XDCxDAO/interfaces.go @@ -42,9 +42,16 @@ type XDCXDAO interface { HasAncient(kind string, number uint64) (bool, error) Ancient(kind string, number uint64) ([]byte, error) Ancients() (uint64, error) + Tail() (uint64, error) AncientSize(kind string) (uint64, error) AppendAncient(number uint64, hash, header, body, receipt, td []byte) error - TruncateAncients(n uint64) error + TruncateHead(n uint64) error + TruncateTail(n uint64) error + AncientRange(kind string, start, count, maxBytes uint64) ([][]byte, error) + ReadAncients(fn func(ethdb.AncientReaderOp) error) (err error) + MigrateTable(string, func([]byte) ([]byte, error)) error + AncientDatadir() (string, error) + ModifyAncients(fn func(ethdb.AncientWriteOp) error) (writeSize int64, err error) Sync() error NewIterator(prefix []byte, start []byte) ethdb.Iterator diff --git a/XDCxDAO/leveldb.go b/XDCxDAO/leveldb.go index 504fc1f501d..30d0c08e2ae 100644 --- a/XDCxDAO/leveldb.go +++ b/XDCxDAO/leveldb.go @@ -145,6 +145,11 @@ func (db *BatchDatabase) Ancients() (uint64, error) { return 0, errNotSupported } +// Tail returns an error as we don't have a backing chain freezer. +func (db *BatchDatabase) Tail() (uint64, error) { + return 0, errNotSupported +} + // AncientSize returns an error as we don't have a backing chain freezer. func (db *BatchDatabase) AncientSize(kind string) (uint64, error) { return 0, errNotSupported @@ -155,11 +160,39 @@ func (db *BatchDatabase) AppendAncient(number uint64, hash, header, body, receip return errNotSupported } -// TruncateAncients returns an error as we don't have a backing chain freezer. -func (db *BatchDatabase) TruncateAncients(items uint64) error { +// TruncateHead returns an error as we don't have a backing chain freezer. +func (db *BatchDatabase) TruncateHead(items uint64) error { + return errNotSupported +} + +// TruncateTail returns an error as we don't have a backing chain freezer. +func (db *BatchDatabase) TruncateTail(items uint64) error { + return errNotSupported +} + +func (db *BatchDatabase) AncientRange(kind string, start, max, maxByteSize uint64) ([][]byte, error) { + return nil, errNotSupported +} + +func (db *BatchDatabase) ReadAncients(fn func(ethdb.AncientReaderOp) error) (err error) { + return fn(db) +} + +// MigrateTable processes the entries in a given table in sequence +// converting them to a new format if they're of an old format. +func (db *BatchDatabase) MigrateTable(kind string, convert func([]byte) ([]byte, error)) error { return errNotSupported } +// AncientDatadir returns an error as we don't have a backing chain freezer. +func (db *BatchDatabase) AncientDatadir() (string, error) { + return "", errNotSupported +} + +func (db *BatchDatabase) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (writeSize int64, err error) { + return 0, errNotSupported +} + // Sync returns an error as we don't have a backing chain freezer. func (db *BatchDatabase) Sync() error { return errNotSupported diff --git a/XDCxDAO/mongodb.go b/XDCxDAO/mongodb.go index 8c8e45052ca..2dbfe88d58b 100644 --- a/XDCxDAO/mongodb.go +++ b/XDCxDAO/mongodb.go @@ -845,6 +845,11 @@ func (db *MongoDatabase) Ancients() (uint64, error) { return 0, errNotSupported } +// Tail returns an error as we don't have a backing chain freezer. +func (db *MongoDatabase) Tail() (uint64, error) { + return 0, errNotSupported +} + // AncientSize returns an error as we don't have a backing chain freezer. func (db *MongoDatabase) AncientSize(kind string) (uint64, error) { return 0, errNotSupported @@ -855,11 +860,39 @@ func (db *MongoDatabase) AppendAncient(number uint64, hash, header, body, receip return errNotSupported } -// TruncateAncients returns an error as we don't have a backing chain freezer. -func (db *MongoDatabase) TruncateAncients(items uint64) error { +// TruncateHead returns an error as we don't have a backing chain freezer. +func (db *MongoDatabase) TruncateHead(items uint64) error { + return errNotSupported +} + +// TruncateTail returns an error as we don't have a backing chain freezer. +func (db *MongoDatabase) TruncateTail(items uint64) error { + return errNotSupported +} + +func (db *MongoDatabase) AncientRange(kind string, start, max, maxByteSize uint64) ([][]byte, error) { + return nil, errNotSupported +} + +func (db *MongoDatabase) ReadAncients(fn func(ethdb.AncientReaderOp) error) (err error) { + return fn(db) +} + +// MigrateTable processes the entries in a given table in sequence +// converting them to a new format if they're of an old format. +func (db *MongoDatabase) MigrateTable(kind string, convert func([]byte) ([]byte, error)) error { return errNotSupported } +// AncientDatadir returns an error as we don't have a backing chain freezer. +func (db *MongoDatabase) AncientDatadir() (string, error) { + return "", errNotSupported +} + +func (db *MongoDatabase) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (writeSize int64, err error) { + return 0, errNotSupported +} + // Sync returns an error as we don't have a backing chain freezer. func (db *MongoDatabase) Sync() error { return errNotSupported diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go index 1b1ccfd46f4..a3e45f73032 100644 --- a/accounts/abi/bind/backends/simulated.go +++ b/accounts/abi/bind/backends/simulated.go @@ -138,7 +138,7 @@ func NewXDCSimulatedBackend(alloc types.GenesisAlloc, gasLimit uint64, chainConf return lendingServ } - blockchain, _ := core.NewBlockChain(database, nil, genesis.Config, consensus, vm.Config{}) + blockchain, _ := core.NewBlockChain(database, nil, genesis.Config, consensus, vm.Config{}, nil) backend := &SimulatedBackend{ database: database, @@ -162,7 +162,7 @@ func NewSimulatedBackend(alloc types.GenesisAlloc, gasLimit uint64) *SimulatedBa database := rawdb.NewMemoryDatabase() genesis := core.Genesis{Config: params.AllEthashProtocolChanges, GasLimit: gasLimit, Alloc: alloc} genesis.MustCommit(database) - blockchain, _ := core.NewBlockChain(database, nil, genesis.Config, ethash.NewFaker(), vm.Config{}) + blockchain, _ := core.NewBlockChain(database, nil, genesis.Config, ethash.NewFaker(), vm.Config{}, nil) backend := &SimulatedBackend{ database: database, diff --git a/cmd/XDC/chaincmd.go b/cmd/XDC/chaincmd.go index 0566f0faed4..8b50904e2bb 100644 --- a/cmd/XDC/chaincmd.go +++ b/cmd/XDC/chaincmd.go @@ -29,6 +29,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/cmd/utils" "github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/core" + "github.com/XinFinOrg/XDPoSChain/core/rawdb" "github.com/XinFinOrg/XDPoSChain/core/state" "github.com/XinFinOrg/XDPoSChain/core/types" xdc_genesis "github.com/XinFinOrg/XDPoSChain/genesis" @@ -73,6 +74,7 @@ It expects the genesis file or the network name [ mainnet | testnet | devnet ] a utils.MetricsInfluxDBTokenFlag, utils.MetricsInfluxDBBucketFlag, utils.MetricsInfluxDBOrganizationFlag, + utils.TxLookupLimitFlag, }, utils.DatabaseFlags), Description: ` The import command imports blocks from an RLP-encoded form. The form can be one file @@ -201,7 +203,7 @@ func importChain(ctx *cli.Context) error { // Start metrics export if enabled utils.SetupMetrics(&cfg.Metrics) - chain, db := utils.MakeChain(ctx, stack, false) + chain, db := utils.MakeChain(ctx, stack) defer db.Close() // Start periodically gathering memory profiles @@ -276,7 +278,7 @@ func exportChain(ctx *cli.Context) error { stack, _, _ := makeFullNode(ctx) defer stack.Close() - chain, db := utils.MakeChain(ctx, stack, true) + chain, db := utils.MakeChain(ctx, stack) defer db.Close() start := time.Now() @@ -347,22 +349,27 @@ func dump(ctx *cli.Context) error { stack, _, _ := makeFullNode(ctx) defer stack.Close() - chain, chainDb := utils.MakeChain(ctx, stack, true) - defer chainDb.Close() - + db := utils.MakeChainDatabase(ctx, stack, true) for _, arg := range ctx.Args().Slice() { - var block *types.Block + var header *types.Header if hashish(arg) { - block = chain.GetBlockByHash(common.HexToHash(arg)) + hash := common.HexToHash(arg) + number := rawdb.ReadHeaderNumber(db, hash) + if number != nil { + header = rawdb.ReadHeader(db, hash, *number) + } } else { - num, _ := strconv.Atoi(arg) - block = chain.GetBlockByNumber(uint64(num)) + number, _ := strconv.Atoi(arg) + hash := rawdb.ReadCanonicalHash(db, uint64(number)) + if hash != (common.Hash{}) { + header = rawdb.ReadHeader(db, hash, uint64(number)) + } } - if block == nil { + if header == nil { fmt.Println("{}") utils.Fatalf("block not found") } else { - state, err := state.New(block.Root(), state.NewDatabase(chainDb)) + state, err := state.New(header.Root, state.NewDatabase(db)) if err != nil { utils.Fatalf("could not create new state: %v", err) } diff --git a/cmd/XDC/config.go b/cmd/XDC/config.go index 8d812165bd0..b5a51b70fab 100644 --- a/cmd/XDC/config.go +++ b/cmd/XDC/config.go @@ -243,6 +243,18 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend, XDCConfig) { }) } + // Warn users to migrate if they have a legacy freezer format. + if eth != nil { + firstIdx := uint64(0) + isLegacy, _, err := dbHasLegacyReceipts(eth.ChainDb(), firstIdx) + if err != nil { + utils.Fatalf("Failed to check db for legacy receipts: %v", err) + } + if isLegacy { + log.Warn("Database has receipts with a legacy format. Please run `XDC db freezer-migrate`.") + } + } + // Add the Ethereum Stats daemon if requested. if cfg.Ethstats.URL != "" { utils.RegisterEthStatsService(stack, backend, cfg.Ethstats.URL) diff --git a/cmd/XDC/dbcmd.go b/cmd/XDC/dbcmd.go index 1d78dd24a50..423ca5507d9 100644 --- a/cmd/XDC/dbcmd.go +++ b/cmd/XDC/dbcmd.go @@ -17,10 +17,12 @@ package main import ( + "bytes" "fmt" "os" "path/filepath" "slices" + "strconv" "time" "github.com/XinFinOrg/XDPoSChain/cmd/utils" @@ -28,8 +30,10 @@ import ( "github.com/XinFinOrg/XDPoSChain/common/hexutil" "github.com/XinFinOrg/XDPoSChain/console" "github.com/XinFinOrg/XDPoSChain/core/rawdb" + "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/ethdb" "github.com/XinFinOrg/XDPoSChain/log" + "github.com/olekukonko/tablewriter" "github.com/urfave/cli/v2" ) @@ -54,6 +58,9 @@ Remove blockchain and state databases`, dbGetCmd, dbDeleteCmd, dbPutCmd, + dbDumpFreezerIndex, + dbMetadataCmd, + dbMigrateFreezerCmd, }, } dbInspectCmd = &cli.Command{ @@ -119,17 +126,63 @@ WARNING: This is a low-level operation which may cause database corruption!`, Description: `This command sets a given database key to the given value. WARNING: This is a low-level operation which may cause database corruption!`, } + dbDumpFreezerIndex = &cli.Command{ + Action: freezerInspect, + Name: "freezer-index", + Usage: "Dump out the index of a specific freezer table", + ArgsUsage: " ", + Flags: slices.Concat([]cli.Flag{ + utils.SyncModeFlag, + }, utils.NetworkFlags, utils.DatabaseFlags), + Description: "This command displays information about the freezer index.", + } + dbMetadataCmd = &cli.Command{ + Action: showMetaData, + Name: "metadata", + Usage: "Shows metadata about the chain status.", + Flags: slices.Concat([]cli.Flag{ + utils.SyncModeFlag, + }, utils.NetworkFlags, utils.DatabaseFlags), + Description: "Shows metadata about the chain status.", + } + dbMigrateFreezerCmd = &cli.Command{ + Action: freezerMigrate, + Name: "freezer-migrate", + Usage: "Migrate legacy parts of the freezer. (WARNING: may take a long time)", + ArgsUsage: "", + Flags: slices.Concat([]cli.Flag{ + utils.SyncModeFlag, + }, utils.NetworkFlags, utils.DatabaseFlags), + Description: `The freezer-migrate command checks your database for receipts in a legacy format and updates those. +WARNING: please back-up the receipt files in your ancients before running this command.`, + } ) func removeDB(ctx *cli.Context) error { - stack, _ := makeConfigNode(ctx) - name := "chaindata" - dbdir := stack.ResolvePath(name) - if common.FileExist(dbdir) { - confirmAndRemoveDB(dbdir, name) + stack, config := makeConfigNode(ctx) + + // Remove the full node state database + path := stack.ResolvePath("chaindata") + if common.FileExist(path) { + confirmAndRemoveDB(path, "full node state database") + } else { + log.Info("Full node state database missing", "path", path) + } + + // Remove the full node ancient database + path = config.Eth.DatabaseFreezer + switch { + case path == "": + path = filepath.Join(stack.ResolvePath("chaindata"), "ancient") + case !filepath.IsAbs(path): + path = config.Node.ResolvePath(path) + } + if common.FileExist(path) { + confirmAndRemoveDB(path, "full node ancient database") } else { - log.Info("Database doesn't exist, skipping", "path", dbdir) + log.Info("Full node ancient database missing", "path", path) } + return nil } @@ -197,7 +250,7 @@ func inspect(ctx *cli.Context) error { return rawdb.InspectDatabase(db, prefix, start) } -func showLeveldbStats(db ethdb.Stater) { +func showLeveldbStats(db ethdb.KeyValueStater) { if stats, err := db.Stat("leveldb.stats"); err != nil { log.Warn("Failed to read database stats", "error", err) } else { @@ -328,3 +381,139 @@ func dbPut(ctx *cli.Context) error { } return db.Put(key, value) } + +func freezerInspect(ctx *cli.Context) error { + if ctx.NArg() < 4 { + return fmt.Errorf("required arguments: %v", ctx.Command.ArgsUsage) + } + var ( + freezer = ctx.Args().Get(0) + table = ctx.Args().Get(1) + ) + start, err := strconv.ParseInt(ctx.Args().Get(2), 10, 64) + if err != nil { + log.Info("Could not read start-param", "err", err) + return err + } + end, err := strconv.ParseInt(ctx.Args().Get(3), 10, 64) + if err != nil { + log.Info("Could not read count param", "err", err) + return err + } + stack, _ := makeConfigNode(ctx) + ancient := stack.ResolveAncient("chaindata", ctx.String(utils.AncientFlag.Name)) + stack.Close() + return rawdb.InspectFreezerTable(ancient, freezer, table, start, end) +} + +func showMetaData(ctx *cli.Context) error { + stack, _ := makeConfigNode(ctx) + defer stack.Close() + db := utils.MakeChainDatabase(ctx, stack, true) + ancients, err := db.Ancients() + if err != nil { + fmt.Fprintf(os.Stderr, "Error accessing ancients: %v", err) + } + data := rawdb.ReadChainMetadata(db) + data = append(data, []string{"frozen", fmt.Sprintf("%d items", ancients)}) + if b := rawdb.ReadHeadBlock(db); b != nil { + data = append(data, []string{"headBlock.Hash", fmt.Sprintf("%v", b.Hash())}) + data = append(data, []string{"headBlock.Root", fmt.Sprintf("%v", b.Root())}) + data = append(data, []string{"headBlock.Number", fmt.Sprintf("%d (%#x)", b.Number(), b.Number())}) + } + if h := rawdb.ReadHeadHeader(db); h != nil { + data = append(data, []string{"headHeader.Hash", fmt.Sprintf("%v", h.Hash())}) + data = append(data, []string{"headHeader.Root", fmt.Sprintf("%v", h.Root)}) + data = append(data, []string{"headHeader.Number", fmt.Sprintf("%d (%#x)", h.Number, h.Number)}) + } + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"Field", "Value"}) + table.AppendBulk(data) + table.Render() + return nil +} + +func freezerMigrate(ctx *cli.Context) error { + stack, _ := makeConfigNode(ctx) + defer stack.Close() + + db := utils.MakeChainDatabase(ctx, stack, false) + defer db.Close() + + // Check first block for legacy receipt format + numAncients, err := db.Ancients() + if err != nil { + return err + } + if numAncients < 1 { + log.Info("No receipts in freezer to migrate") + return nil + } + + isFirstLegacy, firstIdx, err := dbHasLegacyReceipts(db, 0) + if err != nil { + return err + } + if !isFirstLegacy { + log.Info("No legacy receipts to migrate") + return nil + } + + log.Info("Starting migration", "ancients", numAncients, "firstLegacy", firstIdx) + start := time.Now() + if err := db.MigrateTable("receipts", types.ConvertLegacyStoredReceipts); err != nil { + return err + } + if err := db.Close(); err != nil { + return err + } + log.Info("Migration finished", "duration", time.Since(start)) + + return nil +} + +// dbHasLegacyReceipts checks freezer entries for legacy receipts. It stops at the first +// non-empty receipt and checks its format. The index of this first non-empty element is +// the second return parameter. +func dbHasLegacyReceipts(db ethdb.Database, firstIdx uint64) (bool, uint64, error) { + // Check first block for legacy receipt format + numAncients, err := db.Ancients() + if err != nil { + return false, 0, err + } + if numAncients < 1 { + return false, 0, nil + } + if firstIdx >= numAncients { + return false, firstIdx, nil + } + var ( + legacy bool + blob []byte + emptyRLPList = []byte{192} + ) + // Find first block with non-empty receipt, only if + // the index is not already provided. + if firstIdx == 0 { + for i := uint64(0); i < numAncients; i++ { + blob, err = db.Ancient("receipts", i) + if err != nil { + return false, 0, err + } + if len(blob) == 0 { + continue + } + if !bytes.Equal(blob, emptyRLPList) { + firstIdx = i + break + } + } + } + // Is first non-empty receipt legacy? + first, err := db.Ancient("receipts", firstIdx) + if err != nil { + return false, 0, err + } + legacy, err = types.IsLegacyStoredReceipts(first) + return legacy, firstIdx, err +} diff --git a/cmd/XDC/main.go b/cmd/XDC/main.go index 1f63a9d007a..2f3780ab793 100644 --- a/cmd/XDC/main.go +++ b/cmd/XDC/main.go @@ -90,6 +90,7 @@ var ( utils.TxPoolLifetimeFlag, utils.SyncModeFlag, utils.GCModeFlag, + utils.TxLookupLimitFlag, // utils.LightServFlag, // deprecated // utils.LightPeersFlag, // deprecated //utils.LightKDFFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 04385353bb2..96d757d7eff 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -41,6 +41,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/consensus/XDPoS" "github.com/XinFinOrg/XDPoSChain/consensus/ethash" "github.com/XinFinOrg/XDPoSChain/core" + "github.com/XinFinOrg/XDPoSChain/core/rawdb" "github.com/XinFinOrg/XDPoSChain/core/txpool" "github.com/XinFinOrg/XDPoSChain/core/vm" "github.com/XinFinOrg/XDPoSChain/crypto" @@ -85,6 +86,11 @@ var ( Value: flags.DirectoryString(node.DefaultDataDir()), Category: flags.EthCategory, } + AncientFlag = &flags.DirectoryFlag{ + Name: "datadir.ancient", + Usage: "Root directory for ancient data (default = inside chaindata)", + Category: flags.EthCategory, + } KeyStoreDirFlag = &flags.DirectoryFlag{ Name: "keystore", Usage: "Directory for the keystore (default = inside the datadir)", @@ -155,6 +161,12 @@ var ( Value: "full", Category: flags.EthCategory, } + TxLookupLimitFlag = &cli.Uint64Flag{ + Name: "txlookuplimit", + Usage: "Number of recent blocks to maintain transactions index for (default = about one year, 0 = entire chain)", + Value: 0, + Category: flags.EthCategory, + } LightKDFFlag = &cli.BoolFlag{ Name: "lightkdf", Usage: "Reduce key-derivation RAM & CPU usage at some expense of KDF strength", @@ -804,6 +816,7 @@ var ( DatabaseFlags = []cli.Flag{ DataDirFlag, XDCXDataDirFlag, + AncientFlag, } ) @@ -1362,6 +1375,7 @@ func SetXDCXConfig(ctx *cli.Context, cfg *XDCx.Config, XDCDataDir string) { func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { // Avoid conflicting network flags CheckExclusive(ctx, MainnetFlag, TestnetFlag, DevnetFlag, DeveloperFlag) + CheckExclusive(ctx, GCModeFlag, "archive", TxLookupLimitFlag) ks := stack.AccountManager().Backends(keystore.KeyStoreType)[0].(*keystore.KeyStore) setEtherbase(ctx, ks, cfg) @@ -1418,12 +1432,17 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { cfg.DatabaseCache = ctx.Int(CacheFlag.Name) * ctx.Int(CacheDatabaseFlag.Name) / 100 } cfg.DatabaseHandles = MakeDatabaseHandles(ctx.Int(FDLimitFlag.Name)) + if ctx.IsSet(AncientFlag.Name) { + cfg.DatabaseFreezer = ctx.String(AncientFlag.Name) + } if gcmode := ctx.String(GCModeFlag.Name); gcmode != "full" && gcmode != "archive" { Fatalf("--%s must be either 'full' or 'archive'", GCModeFlag.Name) } cfg.NoPruning = ctx.String(GCModeFlag.Name) == "archive" - + if ctx.IsSet(TxLookupLimitFlag.Name) { + cfg.TxLookupLimit = ctx.Uint64(TxLookupLimitFlag.Name) + } if ctx.IsSet(CacheFlag.Name) || ctx.IsSet(CacheGCFlag.Name) { cfg.TrieCache = ctx.Int(CacheFlag.Name) * ctx.Int(CacheGCFlag.Name) / 100 } @@ -1487,6 +1506,10 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { } cfg.Genesis = core.DefaultDevnetGenesisBlock() case ctx.Bool(DeveloperFlag.Name): + if !ctx.IsSet(NetworkIdFlag.Name) { + cfg.NetworkId = 1337 + } + cfg.SyncMode = downloader.FullSync // Create new developer account or reuse existing one var ( developer accounts.Account @@ -1505,7 +1528,17 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { } log.Info("Using developer account", "address", developer.Address) + // Create a new developer genesis block or reuse existing one cfg.Genesis = core.DeveloperGenesisBlock(uint64(ctx.Int(DeveloperPeriodFlag.Name)), developer.Address) + if ctx.IsSet(DataDirFlag.Name) { + // Check if we have an already initialized chain and fall back to + // that if so. Otherwise we need to generate a new genesis spec. + chaindb := MakeChainDatabase(ctx, stack, true) + if rawdb.ReadCanonicalHash(chaindb, 0) != (common.Hash{}) { + cfg.Genesis = nil // fallback to db content + } + chaindb.Close() + } if !ctx.IsSet(MinerGasPriceFlag.Name) { cfg.GasPrice = big.NewInt(1) } @@ -1621,7 +1654,7 @@ func MakeChainDatabase(ctx *cli.Context, stack *node.Node, readonly bool) ethdb. cache = ctx.Int(CacheFlag.Name) * ctx.Int(CacheDatabaseFlag.Name) / 100 handles = MakeDatabaseHandles(ctx.Int(FDLimitFlag.Name)) ) - chainDb, err := stack.OpenDatabase("chaindata", cache, handles, "", readonly) + chainDb, err := stack.OpenDatabaseWithFreezer("chaindata", cache, handles, ctx.String(AncientFlag.Name), "", readonly) if err != nil { Fatalf("Could not open database: %v", err) } @@ -1644,9 +1677,9 @@ func MakeGenesis(ctx *cli.Context) *core.Genesis { } // MakeChain creates a chain manager from set command line flags. -func MakeChain(ctx *cli.Context, stack *node.Node, readonly bool) (chain *core.BlockChain, chainDb ethdb.Database) { +func MakeChain(ctx *cli.Context, stack *node.Node) (chain *core.BlockChain, chainDb ethdb.Database) { var err error - chainDb = MakeChainDatabase(ctx, stack, readonly) + chainDb = MakeChainDatabase(ctx, stack, false) config, _, err := core.SetupGenesisBlock(chainDb, MakeGenesis(ctx)) if err != nil { @@ -1681,7 +1714,10 @@ func MakeChain(ctx *cli.Context, stack *node.Node, readonly bool) (chain *core.B cache.TrieNodeLimit = ctx.Int(CacheFlag.Name) * ctx.Int(CacheGCFlag.Name) / 100 } vmcfg := vm.Config{EnablePreimageRecording: ctx.Bool(VMEnableDebugFlag.Name)} - chain, err = core.NewBlockChain(chainDb, cache, config, engine, vmcfg) + + // TODO(rjl493456442) disable snapshot generation/wiping if the chain is read only. + // Disable transaction indexing/unindexing by default. + chain, err = core.NewBlockChain(chainDb, cache, config, engine, vmcfg, nil) if err != nil { Fatalf("Can't create BlockChain: %v", err) } diff --git a/common/format.go b/common/format.go index 197e8697d9a..7af41f52d54 100644 --- a/common/format.go +++ b/common/format.go @@ -17,6 +17,7 @@ package common import ( + "fmt" "regexp" "strings" "time" @@ -37,3 +38,45 @@ func (d PrettyDuration) String() string { } return label } + +// PrettyAge is a pretty printed version of a time.Duration value that rounds +// the values up to a single most significant unit, days/weeks/years included. +type PrettyAge time.Time + +// ageUnits is a list of units the age pretty printing uses. +var ageUnits = []struct { + Size time.Duration + Symbol string +}{ + {12 * 30 * 24 * time.Hour, "y"}, + {30 * 24 * time.Hour, "mo"}, + {7 * 24 * time.Hour, "w"}, + {24 * time.Hour, "d"}, + {time.Hour, "h"}, + {time.Minute, "m"}, + {time.Second, "s"}, +} + +// String implements the Stringer interface, allowing pretty printing of duration +// values rounded to the most significant time unit. +func (t PrettyAge) String() string { + // Calculate the time difference and handle the 0 cornercase + diff := time.Since(time.Time(t)) + if diff < time.Second { + return "0" + } + // Accumulate a precision of 3 components before returning + result, prec := "", 0 + + for _, unit := range ageUnits { + if diff > unit.Size { + result = fmt.Sprintf("%s%d%s", result, diff/unit.Size, unit.Symbol) + diff %= unit.Size + + if prec += 1; prec >= 3 { + break + } + } + } + return result +} diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index da06f9e55fe..127faf08766 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -402,8 +402,11 @@ func (c *Clique) snapshot(chain consensus.ChainReader, number uint64, hash commo break } } - // If we're at block zero, make a snapshot - if number == 0 { + // If we're at the genesis, snapshot the initial state. Alternatively if we're + // at a checkpoint block without a parent (light client CHT), or we have piled + // up more headers than allowed to be reorged (chain reinit from a freezer), + // consider the checkpoint trusted and snapshot it. + if number == 0 || (number%c.config.Epoch == 0 && (len(headers) > params.FullImmutabilityThreshold || chain.GetHeaderByNumber(number-1) == nil)) { genesis := chain.GetHeaderByNumber(0) if err := c.VerifyHeader(chain, genesis, false); err != nil { return nil, err diff --git a/consensus/clique/snapshot.go b/consensus/clique/snapshot.go index ff7c5a4c8e9..044a21e96ca 100644 --- a/consensus/clique/snapshot.go +++ b/consensus/clique/snapshot.go @@ -19,11 +19,13 @@ package clique import ( "bytes" "encoding/json" + "time" "github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/common/lru" "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/ethdb" + "github.com/XinFinOrg/XDPoSChain/log" "github.com/XinFinOrg/XDPoSChain/params" ) @@ -191,7 +193,11 @@ func (s *Snapshot) apply(headers []*types.Header) (*Snapshot, error) { // Iterate through the headers and create a new snapshot snap := s.copy() - for _, header := range headers { + var ( + start = time.Now() + logged = time.Now() + ) + for i, header := range headers { // Remove any votes on checkpoint blocks number := header.Number.Uint64() if number%s.config.Epoch == 0 { @@ -279,6 +285,14 @@ func (s *Snapshot) apply(headers []*types.Header) (*Snapshot, error) { } delete(snap.Tally, header.Coinbase) } + // If we're taking too much time (ecrecover), notify the user once a while + if time.Since(logged) > 8*time.Second { + log.Info("Reconstructing voting history", "processed", i, "total", len(headers), "elapsed", common.PrettyDuration(time.Since(start))) + logged = time.Now() + } + } + if time.Since(start) > 8*time.Second { + log.Info("Reconstructed voting history", "processed", len(headers), "elapsed", common.PrettyDuration(time.Since(start))) } snap.Number += uint64(len(headers)) snap.Hash = headers[len(headers)-1].Hash() diff --git a/console/prompter.go b/console/prompter.go index 3769d52f674..cfe0b55be6b 100644 --- a/console/prompter.go +++ b/console/prompter.go @@ -142,7 +142,7 @@ func (p *terminalPrompter) PromptPassword(prompt string) (passwd string, err err // PromptConfirm displays the given prompt to the user and requests a boolean // choice to be made, returning that choice. func (p *terminalPrompter) PromptConfirm(prompt string) (bool, error) { - input, err := p.Prompt(prompt + " [y/N] ") + input, err := p.Prompt(prompt + " [y/n] ") if len(input) > 0 && strings.ToUpper(input[:1]) == "Y" { return true, nil } diff --git a/core/bench_test.go b/core/bench_test.go index fdef0fe89b5..d15573ba682 100644 --- a/core/bench_test.go +++ b/core/bench_test.go @@ -170,7 +170,7 @@ func benchInsertChain(b *testing.B, disk bool, gen func(int, *BlockGen)) { // Time the insertion of the new chain. // State and blocks are stored in the same DB. - chainman, _ := NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}) + chainman, _ := NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) defer chainman.Stop() b.ReportAllocs() b.ResetTimer() @@ -275,7 +275,7 @@ func benchReadChain(b *testing.B, full bool, count uint64) { if err != nil { b.Fatalf("error opening database at %v: %v", dir, err) } - chain, err := NewBlockChain(db, nil, params.TestChainConfig, ethash.NewFaker(), vm.Config{}) + chain, err := NewBlockChain(db, nil, params.TestChainConfig, ethash.NewFaker(), vm.Config{}, nil) if err != nil { b.Fatalf("error creating chain: %v", err) } diff --git a/core/block_validator_test.go b/core/block_validator_test.go index 14c43c8d0e8..1d180ebf434 100644 --- a/core/block_validator_test.go +++ b/core/block_validator_test.go @@ -43,7 +43,7 @@ func TestHeaderVerification(t *testing.T) { headers[i] = block.Header() } // Run the header checker for blocks one-by-one, checking for both valid and invalid nonces - chain, _ := NewBlockChain(testdb, nil, params.TestChainConfig, ethash.NewFaker(), vm.Config{}) + chain, _ := NewBlockChain(testdb, nil, params.TestChainConfig, ethash.NewFaker(), vm.Config{}, nil) defer chain.Stop() for i := 0; i < len(blocks); i++ { @@ -105,11 +105,11 @@ func testHeaderConcurrentVerification(t *testing.T, threads int) { for i, valid := range []bool{true, false} { var results <-chan error if valid { - chain, _ := NewBlockChain(testdb, nil, params.TestChainConfig, ethash.NewFaker(), vm.Config{}) + chain, _ := NewBlockChain(testdb, nil, params.TestChainConfig, ethash.NewFaker(), vm.Config{}, nil) _, results = chain.engine.VerifyHeaders(chain, headers, seals) chain.Stop() } else { - chain, _ := NewBlockChain(testdb, nil, params.TestChainConfig, ethash.NewFakeFailer(uint64(len(headers)-1)), vm.Config{}) + chain, _ := NewBlockChain(testdb, nil, params.TestChainConfig, ethash.NewFakeFailer(uint64(len(headers)-1)), vm.Config{}, nil) _, results = chain.engine.VerifyHeaders(chain, headers, seals) chain.Stop() } @@ -172,7 +172,7 @@ func testHeaderConcurrentAbortion(t *testing.T, threads int) { defer runtime.GOMAXPROCS(old) // Start the verifications and immediately abort - chain, _ := NewBlockChain(testdb, nil, params.TestChainConfig, ethash.NewFakeDelayer(time.Millisecond), vm.Config{}) + chain, _ := NewBlockChain(testdb, nil, params.TestChainConfig, ethash.NewFakeDelayer(time.Millisecond), vm.Config{}, nil) defer chain.Stop() abort, results := chain.engine.VerifyHeaders(chain, headers, seals) close(abort) diff --git a/core/blockchain.go b/core/blockchain.go index 01e2e0520e3..bd6a3367a5e 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -107,30 +107,45 @@ const ( // * the `BlockNumber`, `TxHash`, `TxIndex`, `BlockHash` and `Index` fields of log are deleted // * the `Bloom` field of receipt is deleted // * the `BlockIndex` and `TxIndex` fields of txlookup are deleted + // // - Version 5 // The following incompatible database changes were added: // * the `TxHash`, `GasCost`, and `ContractAddress` fields are no longer stored for a receipt // * the `TxHash`, `GasCost`, and `ContractAddress` fields are computed by looking up the // receipts' corresponding block + // // - Version 6 // The following incompatible database changes were added: // * Transaction lookup information stores the corresponding block number instead of block hash + // // - Version 7 // The following incompatible database changes were added: // * New scheme for contract code in order to separate the codes and trie nodes - BlockChainVersion uint64 = 7 + // + // - Version 8 + // The following incompatible database changes were added: + // * Use freezer as the ancient database to maintain all ancient data + BlockChainVersion uint64 = 8 // Maximum length of chain to cache by block's number blocksHashCacheLimit = 900 ) -// CacheConfig contains the configuration values for the trie caching/pruning +// CacheConfig contains the configuration values for the trie database // that's resident in a blockchain. type CacheConfig struct { Disabled bool // Whether to disable trie write caching (archive node) TrieNodeLimit int // Memory limit (MB) at which to flush the current in-memory trie to disk TrieTimeLimit time.Duration // Time limit after which to flush the current in-memory trie to disk } + +// defaultCacheConfig are the default caching values if none are specified by the +// user (also used during testing). +var defaultCacheConfig = &CacheConfig{ + TrieNodeLimit: 256 * 1024 * 1024, + TrieTimeLimit: 5 * time.Minute, +} + type ResultProcessBlock struct { logs []*types.Log receipts []*types.Receipt @@ -164,6 +179,13 @@ type BlockChain struct { triegc *prque.Prque[int64, common.Hash] // Priority queue mapping block numbers to tries to gc gcproc time.Duration // Accumulates canonical block processing for trie dumping + // txLookupLimit is the maximum number of blocks from head whose tx indices + // are reserved: + // * 0: means no limit and regenerate any missing indexes + // * N: means N block limit [HEAD-N+1, HEAD] and delete extra indexes + // * nil: disable tx reindexer/deleter, but still index new blocks + txLookupLimit uint64 + hc *HeaderChain rmLogsFeed event.Feed chainFeed event.Feed @@ -223,12 +245,9 @@ type BlockChain struct { // NewBlockChain returns a fully initialised block chain using information // available in the database. It initialises the default Ethereum Validator and // Processor. -func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *params.ChainConfig, engine consensus.Engine, vmConfig vm.Config) (*BlockChain, error) { +func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *params.ChainConfig, engine consensus.Engine, vmConfig vm.Config, txLookupLimit *uint64) (*BlockChain, error) { if cacheConfig == nil { - cacheConfig = &CacheConfig{ - TrieNodeLimit: 256 * 1024 * 1024, - TrieTimeLimit: 5 * time.Minute, - } + cacheConfig = defaultCacheConfig } bc := &BlockChain{ @@ -277,9 +296,64 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par // Update chain info data metrics chainInfoGauge.Update(metrics.GaugeInfoValue{"chain_id": bc.chainConfig.ChainId.String()}) + // Initialize the chain with ancient data if it isn't empty. + var txIndexBlock uint64 + + if bc.empty() { + rawdb.InitDatabaseFromFreezer(bc.db) + // If ancient database is not empty, reconstruct all missing + // indices in the background. + frozen, _ := bc.db.Ancients() + if frozen > 0 { + txIndexBlock = frozen + } + } if err := bc.loadLastState(); err != nil { return nil, err } + // Make sure the state associated with the block is available + head := bc.CurrentBlock() + if err := bc.verifyChainHead(head); err != nil { + log.Warn("Head state missing, repairing", "number", head.Number(), "hash", head.Hash(), "err", err) + if _, err := bc.setHeadBeyondRoot(head.NumberU64(), common.Hash{}, true); err != nil { + return nil, err + } + } + // Ensure that a previous crash in SetHead doesn't leave extra ancients + if frozen, err := bc.db.Ancients(); err == nil && frozen > 0 { + var ( + needRewind bool + low uint64 + ) + // The head full block may be rolled back to a very low height due to + // blockchain repair. If the head full block is even lower than the ancient + // chain, truncate the ancient store. + fullBlock := bc.CurrentBlock() + if fullBlock != nil && fullBlock.Hash() != bc.genesisBlock.Hash() && fullBlock.NumberU64() < frozen-1 { + needRewind = true + low = fullBlock.NumberU64() + } + // In fast sync, it may happen that ancient data has been written to the + // ancient store, but the LastFastBlock has not been updated, truncate the + // extra data here. + fastBlock := bc.CurrentFastBlock() + if fastBlock != nil && fastBlock.NumberU64() < frozen-1 { + needRewind = true + if fastBlock.NumberU64() < low || low == 0 { + low = fastBlock.NumberU64() + } + } + if needRewind { + log.Error("Truncating ancient chain", "from", bc.CurrentHeader().Number.Uint64(), "to", low) + if err := bc.SetHead(low); err != nil { + return nil, err + } + } + } + // The first thing the node will do is reconstruct the verification data for + // the head block (ethash cache or clique voting snapshot). Might as well do + // it in advance. + bc.engine.VerifyHeader(bc, bc.CurrentHeader(), true) // Check the current state of the block hashes and make sure that we do not have any of the bad blocks in our chain for hash := range BadHashes { @@ -298,7 +372,10 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par // Start future block processor. bc.wg.Add(1) go bc.futureBlocksLoop() - + if txLookupLimit != nil { + bc.txLookupLimit = *txLookupLimit + go bc.maintainTxIndex(txIndexBlock) + } return bc, nil } @@ -307,9 +384,23 @@ func (bc *BlockChain) GetVMConfig() *vm.Config { return &bc.vmConfig } +// empty returns an indicator whether the blockchain is empty. +// Note, it's a special case that we connect a non-empty ancient +// database with an empty node, so that we can plugin the ancient +// into node seamlessly. +func (bc *BlockChain) empty() bool { + genesis := bc.genesisBlock.Hash() + for _, hash := range []common.Hash{rawdb.ReadHeadBlockHash(bc.db), rawdb.ReadHeadHeaderHash(bc.db), rawdb.ReadHeadFastBlockHash(bc.db)} { + if hash != genesis { + return false + } + } + return true +} + // NewBlockChainEx extend old blockchain, add order state db -func NewBlockChainEx(db ethdb.Database, XDCxDb ethdb.XDCxDatabase, cacheConfig *CacheConfig, chainConfig *params.ChainConfig, engine consensus.Engine, vmConfig vm.Config) (*BlockChain, error) { - blockchain, err := NewBlockChain(db, cacheConfig, chainConfig, engine, vmConfig) +func NewBlockChainEx(db ethdb.Database, XDCxDb ethdb.XDCxDatabase, cacheConfig *CacheConfig, chainConfig *params.ChainConfig, engine consensus.Engine, vmConfig vm.Config, txLookupLimit *uint64) (*BlockChain, error) { + blockchain, err := NewBlockChain(db, cacheConfig, chainConfig, engine, vmConfig, txLookupLimit) if err != nil { return nil, err } @@ -340,53 +431,6 @@ func (bc *BlockChain) loadLastState() error { log.Warn("Head block missing, resetting chain", "hash", head) return bc.Reset() } - // Make sure the state associated with the block is available - repair := false - _, err := state.New(currentBlock.Root(), bc.stateCache) - if err != nil { - repair = true - } else { - engine, ok := bc.Engine().(*XDPoS.XDPoS) - if ok { - tradingService := engine.GetXDCXService() - lendingService := engine.GetLendingService() - if bc.Config().IsTIPXDCX(currentBlock.Number()) && bc.chainConfig.XDPoS != nil && currentBlock.NumberU64() > bc.chainConfig.XDPoS.Epoch && tradingService != nil && lendingService != nil { - author, _ := bc.Engine().Author(currentBlock.Header()) - tradingRoot, err := tradingService.GetTradingStateRoot(currentBlock, author) - if err != nil { - repair = true - } else { - if tradingService.GetStateCache() != nil { - _, err = tradingstate.New(tradingRoot, tradingService.GetStateCache()) - if err != nil { - repair = true - } - } - } - - if !repair { - lendingRoot, err := lendingService.GetLendingStateRoot(currentBlock, author) - if err != nil { - repair = true - } else { - if lendingService.GetStateCache() != nil { - _, err = lendingstate.New(lendingRoot, lendingService.GetStateCache()) - if err != nil { - repair = true - } - } - } - } - } - } - } - if repair { - // Dangling block without a state associated, init from scratch - log.Warn("Head state missing, repairing chain", "number", currentBlock.Number(), "hash", currentBlock.Hash()) - if err := bc.repair(¤tBlock); err != nil { - return err - } - } // Everything seems to be fine, set as the head block bc.currentBlock.Store(currentBlock) headBlockGauge.Update(int64(currentBlock.NumberU64())) @@ -410,7 +454,6 @@ func (bc *BlockChain) loadLastState() error { headFastBlockGauge.Update(int64(block.NumberU64())) } } - // Issue a status log for the user currentFastBlock := bc.CurrentFastBlock() @@ -418,33 +461,88 @@ func (bc *BlockChain) loadLastState() error { blockTd := bc.GetTd(currentBlock.Hash(), currentBlock.NumberU64()) fastTd := bc.GetTd(currentFastBlock.Hash(), currentFastBlock.NumberU64()) - log.Info("Loaded most recent local header", "number", currentHeader.Number, "hash", currentHeader.Hash(), "td", headerTd) - log.Info("Loaded most recent local full block", "number", currentBlock.Number(), "hash", currentBlock.Hash(), "td", blockTd) - log.Info("Loaded most recent local fast block", "number", currentFastBlock.Number(), "hash", currentFastBlock.Hash(), "td", fastTd) + log.Info("Loaded most recent local header", "number", currentHeader.Number, "hash", currentHeader.Hash(), "td", headerTd, "age", common.PrettyAge(time.Unix(currentHeader.Time.Int64(), 0))) + log.Info("Loaded most recent local full block", "number", currentBlock.Number(), "hash", currentBlock.Hash(), "td", blockTd, "age", common.PrettyAge(time.Unix(currentBlock.Time().Int64(), 0))) + log.Info("Loaded most recent local fast block", "number", currentFastBlock.Number(), "hash", currentFastBlock.Hash(), "td", fastTd, "age", common.PrettyAge(time.Unix(currentFastBlock.Time().Int64(), 0))) + + if pivot := rawdb.ReadLastPivotNumber(bc.db); pivot != nil { + log.Info("Loaded last fast-sync pivot marker", "number", *pivot) + } return nil } -// SetHead rewinds the local chain to a new head. In the case of headers, everything -// above the new head will be deleted and the new one set. In the case of blocks -// though, the head may be further rewound if block bodies are missing (non-archive -// nodes after a fast sync). +// SetHead rewinds the local chain to a new head. Depending on whether the node +// was fast synced or full synced and in which state, the method will try to +// delete minimal data from disk whilst retaining chain consistency. func (bc *BlockChain) SetHead(head uint64) error { + _, err := bc.setHeadBeyondRoot(head, common.Hash{}, false) + return err +} + +// setHeadBeyondRoot rewinds the local chain to a new head with the extra condition +// that the rewind must pass the specified state root. This method is meant to be +// used when rewiding with snapshots enabled to ensure that we go back further than +// persistent disk layer. Depending on whether the node was fast synced or full, and +// in which state, the method will try to delete minimal data from disk whilst +// retaining chain consistency. +// +// The method returns the block number where the requested root cap was found. +func (bc *BlockChain) setHeadBeyondRoot(head uint64, root common.Hash, repair bool) (uint64, error) { if !bc.chainmu.TryLock() { - return errChainStopped + return 0, errChainStopped } defer bc.chainmu.Unlock() - updateFn := func(db ethdb.KeyValueWriter, header *types.Header) { - // Rewind the block chain, ensuring we don't end up with a stateless head block - if currentBlock := bc.CurrentBlock(); currentBlock != nil && header.Number.Uint64() < currentBlock.NumberU64() { + // Track the block number of the requested root hash + var rootNumber uint64 // (no root == always 0) + + // Retrieve the last pivot block to short circuit rollbacks beyond it and the + // current freezer limit to start nuking id underflown + pivot := rawdb.ReadLastPivotNumber(bc.db) + frozen, _ := bc.db.Ancients() + + updateFn := func(db ethdb.KeyValueWriter, header *types.Header) (uint64, bool) { + // Rewind the blockchain, ensuring we don't end up with a stateless head + // block. Note, depth equality is permitted to allow using SetHead as a + // chain reparation mechanism without deleting any data! + if currentBlock := bc.CurrentBlock(); currentBlock != nil && header.Number.Uint64() <= currentBlock.NumberU64() { newHeadBlock := bc.GetBlock(header.Hash(), header.Number.Uint64()) if newHeadBlock == nil { + log.Error("Gap in the chain, rewinding to genesis", "number", header.Number, "hash", header.Hash()) newHeadBlock = bc.genesisBlock } else { - if _, err := state.New(newHeadBlock.Root(), bc.stateCache); err != nil { - // Rewound state missing, rolled back to before pivot, reset to genesis - newHeadBlock = bc.genesisBlock + // Block exists, keep rewinding until we find one with state, + // keeping rewinding until we exceed the optional threshold + // root hash + beyondRoot := (root == common.Hash{}) // Flag whether we're beyond the requested root (no root, always true) + + for { + // If a root threshold was requested but not yet crossed, check + if root != (common.Hash{}) && !beyondRoot && newHeadBlock.Root() == root { + beyondRoot, rootNumber = true, newHeadBlock.NumberU64() + } + if bc.verifyChainHead(newHeadBlock) != nil { + log.Trace("Block state missing, rewinding further", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash()) + if pivot == nil || newHeadBlock.NumberU64() > *pivot { + parent := bc.GetBlock(newHeadBlock.ParentHash(), newHeadBlock.NumberU64()-1) + if parent != nil { + newHeadBlock = parent + continue + } + log.Error("Missing block in the middle, aiming genesis", "number", newHeadBlock.NumberU64()-1, "hash", newHeadBlock.ParentHash()) + newHeadBlock = bc.genesisBlock + } else { + log.Trace("Rewind passed pivot, aiming genesis", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash(), "pivot", *pivot) + newHeadBlock = bc.genesisBlock + } + } + if beyondRoot || newHeadBlock.NumberU64() == 0 { + log.Info("Rewound to block with state", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash()) + break + } + log.Debug("Skipping block with threshold state", "number", newHeadBlock.NumberU64(), "hash", newHeadBlock.Hash(), "root", newHeadBlock.Root()) + newHeadBlock = bc.GetBlock(newHeadBlock.ParentHash(), newHeadBlock.NumberU64()-1) // Keep rewinding } } rawdb.WriteHeadBlockHash(db, newHeadBlock.Hash()) @@ -456,7 +554,6 @@ func (bc *BlockChain) SetHead(head uint64) error { bc.currentBlock.Store(newHeadBlock) headBlockGauge.Update(int64(newHeadBlock.NumberU64())) } - // Rewind the fast block in a simpleton way to the target head if currentFastBlock := bc.CurrentFastBlock(); currentFastBlock != nil && header.Number.Uint64() < currentFastBlock.NumberU64() { newHeadFastBlock := bc.GetBlock(header.Hash(), header.Number.Uint64()) @@ -473,6 +570,16 @@ func (bc *BlockChain) SetHead(head uint64) error { bc.currentFastBlock.Store(newHeadFastBlock) headFastBlockGauge.Update(int64(newHeadFastBlock.NumberU64())) } + head := bc.CurrentBlock().NumberU64() + + // If setHead underflown the freezer threshold and the block processing + // intent afterwards is full block importing, delete the chain segment + // between the stateful-block and the sethead target. + var wipe bool + if head+1 < frozen { + wipe = pivot == nil || head >= *pivot + } + return head, wipe // Only force wipe if full synced } // Rewind the header chain, deleting all block bodies until then @@ -482,10 +589,9 @@ func (bc *BlockChain) SetHead(head uint64) error { if num+1 <= frozen { // Truncate all relative data(header, total difficulty, body, receipt // and canonical hash) from ancient store. - if err := bc.db.TruncateAncients(num + 1); err != nil { + if err := bc.db.TruncateHead(num); err != nil { log.Crit("Failed to truncate ancient data", "number", num, "err", err) } - // Remove the hash <-> number mapping from the active store. rawdb.DeleteHeaderNumber(db, hash) } else { @@ -497,7 +603,19 @@ func (bc *BlockChain) SetHead(head uint64) error { } // Todo(rjl493456442) txlookup, bloombits, etc } - bc.hc.SetHead(head, updateFn, delFn) + + // If SetHead was only called as a chain reparation method, try to skip + // touching the header chain altogether, unless the freezer is broken + if repair { + if target, force := updateFn(bc.db, bc.CurrentBlock().Header()); force { + bc.hc.SetHead(target, updateFn, delFn) + } + } else { + // Rewind the chain to the requested head and keep going backwards until a + // block with a state is found or fast sync pivot is passed + log.Warn("Rewinding blockchain", "target", head) + bc.hc.SetHead(head, updateFn, delFn) + } // Clear out any stale content from the caches bc.bodyCache.Purge() @@ -507,7 +625,7 @@ func (bc *BlockChain) SetHead(head uint64) error { bc.futureBlocks.Purge() bc.blocksHashCache.Purge() - return bc.loadLastState() + return rootNumber, bc.loadLastState() } // FastSyncCommitHead sets the current head block to the one defined by the hash @@ -631,7 +749,7 @@ func (bc *BlockChain) LendingStateAt(block *types.Block) (*lendingstate.LendingS return nil, err } } - return nil, errors.New("Get XDCx state fail") + return nil, errors.New("get XDCx state fail") } @@ -672,50 +790,43 @@ func (bc *BlockChain) ResetWithGenesisBlock(genesis *types.Block) error { return nil } -// repair tries to repair the current blockchain by rolling back the current block -// until one with associated state is found. This is needed to fix incomplete db -// writes caused either by crashes/power outages, or simply non-committed tries. -// -// This method only rolls back the current block. The current header and current -// fast block are left intact. -func (bc *BlockChain) repair(head **types.Block) error { - for { - // Abort if we've rewound to a head block that does have associated state - if (common.RollbackNumber == 0) || ((*head).Number().Uint64() < common.RollbackNumber) { - if _, err := state.New((*head).Root(), bc.stateCache); err == nil { - log.Info("Rewound blockchain to past state", "number", (*head).Number(), "hash", (*head).Hash()) - engine, ok := bc.Engine().(*XDPoS.XDPoS) - if ok { - tradingService := engine.GetXDCXService() - lendingService := engine.GetLendingService() - if bc.Config().IsTIPXDCXReceiver((*head).Number()) && bc.chainConfig.XDPoS != nil && (*head).NumberU64() > bc.chainConfig.XDPoS.Epoch && tradingService != nil && lendingService != nil { - author, _ := bc.Engine().Author((*head).Header()) - tradingRoot, err := tradingService.GetTradingStateRoot(*head, author) - if err == nil { - _, err = tradingstate.New(tradingRoot, tradingService.GetStateCache()) - } - if err == nil { - lendingRoot, err := lendingService.GetLendingStateRoot(*head, author) - if err == nil { - _, err = lendingstate.New(lendingRoot, lendingService.GetStateCache()) - if err == nil { - return nil - } - } - } - } else { - return nil - } - } else { - return nil +func (bc *BlockChain) verifyChainHead(head *types.Block) error { + if _, err := state.New(head.Root(), bc.stateCache); err != nil { + return err + } + + engine, ok := bc.Engine().(*XDPoS.XDPoS) + if ok { + tradingService := engine.GetXDCXService() + lendingService := engine.GetLendingService() + if bc.Config().IsTIPXDCXReceiver((*head).Number()) && bc.chainConfig.XDPoS != nil && (*head).NumberU64() > bc.chainConfig.XDPoS.Epoch && tradingService != nil && lendingService != nil { + author, _ := bc.Engine().Author((*head).Header()) + tradingRoot, err := tradingService.GetTradingStateRoot(head, author) + if err != nil { + return err + } + tradingDb := tradingService.GetStateCache() + if tradingDb != nil { + _, err = tradingstate.New(tradingRoot, tradingDb) + if err != nil { + return err + } + } + lendingRoot, err := lendingService.GetLendingStateRoot(head, author) + if err != nil { + return err + } + lendingDb := lendingService.GetStateCache() + if lendingDb != nil { + _, err = lendingstate.New(lendingRoot, lendingDb) + if err != nil { + return err } } - } else { - log.Info("Rewound blockchain to past state", "number", (*head).Number(), "hash", (*head).Hash()) } - // Otherwise rewind one block and recheck state availability there - (*head) = bc.GetBlock((*head).ParentHash(), (*head).NumberU64()-1) } + + return nil } // Export writes the active chain to the given writer. @@ -1173,143 +1284,284 @@ const ( SideStatTy ) -// Rollback is designed to remove a chain of links from the database that aren't -// certain enough to be valid. -func (bc *BlockChain) Rollback(chain []common.Hash) { - if !bc.chainmu.TryLock() { - return - } - defer bc.chainmu.Unlock() - - batch := bc.db.NewBatch() - for i := len(chain) - 1; i >= 0; i-- { - hash := chain[i] - - // Degrade the chain markers if they are explicitly reverted. - // In theory we should update all in-memory markers in the - // last step, however the direction of rollback is from high - // to low, so it's safe the update in-memory markers directly. - currentHeader := bc.hc.CurrentHeader() - if currentHeader.Hash() == hash { - newHeadHeader := bc.GetHeader(currentHeader.ParentHash, currentHeader.Number.Uint64()-1) - rawdb.WriteHeadHeaderHash(batch, currentHeader.ParentHash) - bc.hc.SetCurrentHeader(newHeadHeader) - } - if currentFastBlock := bc.CurrentFastBlock(); currentFastBlock.Hash() == hash { - newFastBlock := bc.GetBlock(currentFastBlock.ParentHash(), currentFastBlock.NumberU64()-1) - rawdb.WriteHeadFastBlockHash(batch, currentFastBlock.ParentHash()) - bc.currentFastBlock.Store(newFastBlock) - headFastBlockGauge.Update(int64(newFastBlock.NumberU64())) - } - if currentBlock := bc.CurrentBlock(); currentBlock.Hash() == hash { - newBlock := bc.GetBlock(currentBlock.ParentHash(), currentBlock.NumberU64()-1) - rawdb.WriteHeadBlockHash(batch, currentBlock.ParentHash()) - bc.currentBlock.Store(newBlock) - headBlockGauge.Update(int64(newBlock.NumberU64())) - } - } - if err := batch.Write(); err != nil { - log.Crit("Failed to rollback chain markers", "err", err) - } - // TODO: Truncate ancient data which exceeds the current header. -} - // InsertReceiptChain attempts to complete an already existing header chain with // transaction and receipt data. -func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain []types.Receipts) (int, error) { +func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain []types.Receipts, ancientLimit uint64) (int, error) { // We don't require the chainMu here since we want to maximize the // concurrency of header insertion and receipt insertion. bc.wg.Add(1) defer bc.wg.Done() + var ( + ancientBlocks, liveBlocks types.Blocks + ancientReceipts, liveReceipts []types.Receipts + ) // Do a sanity check that the provided chain is actually ordered and linked - for i := 1; i < len(blockChain); i++ { - if blockChain[i].NumberU64() != blockChain[i-1].NumberU64()+1 || blockChain[i].ParentHash() != blockChain[i-1].Hash() { - log.Error("Non contiguous receipt insert", "number", blockChain[i].Number(), "hash", blockChain[i].Hash(), "parent", blockChain[i].ParentHash(), - "prevnumber", blockChain[i-1].Number(), "prevhash", blockChain[i-1].Hash()) - return 0, fmt.Errorf("non contiguous insert: item %d is #%d [%x..], item %d is #%d [%x..] (parent [%x..])", i-1, blockChain[i-1].NumberU64(), - blockChain[i-1].Hash().Bytes()[:4], i, blockChain[i].NumberU64(), blockChain[i].Hash().Bytes()[:4], blockChain[i].ParentHash().Bytes()[:4]) + for i := 0; i < len(blockChain); i++ { + if i != 0 { + if blockChain[i].NumberU64() != blockChain[i-1].NumberU64()+1 || blockChain[i].ParentHash() != blockChain[i-1].Hash() { + log.Error("Non contiguous receipt insert", "number", blockChain[i].Number(), "hash", blockChain[i].Hash(), "parent", blockChain[i].ParentHash(), + "prevnumber", blockChain[i-1].Number(), "prevhash", blockChain[i-1].Hash()) + return 0, fmt.Errorf("non contiguous insert: item %d is #%d [%x..], item %d is #%d [%x..] (parent [%x..])", i-1, blockChain[i-1].NumberU64(), + blockChain[i-1].Hash().Bytes()[:4], i, blockChain[i].NumberU64(), blockChain[i].Hash().Bytes()[:4], blockChain[i].ParentHash().Bytes()[:4]) + } + } + if blockChain[i].NumberU64() <= ancientLimit { + ancientBlocks, ancientReceipts = append(ancientBlocks, blockChain[i]), append(ancientReceipts, receiptChain[i]) + } else { + liveBlocks, liveReceipts = append(liveBlocks, blockChain[i]), append(liveReceipts, receiptChain[i]) } } var ( stats = struct{ processed, ignored int32 }{} start = time.Now() - bytes = 0 - batch = bc.db.NewBatch() + size = int64(0) ) - for i, block := range blockChain { - receipts := receiptChain[i] - // Short circuit insertion if shutting down or processing failed - if atomic.LoadInt32(&bc.procInterrupt) == 1 { - return 0, nil + + // updateHead updates the head fast sync block if the inserted blocks are better + // and returns a indicator whether the inserted blocks are canonical. + updateHead := func(head *types.Block) bool { + if !bc.chainmu.TryLock() { + return false } - blockHash, blockNumber := block.Hash(), block.NumberU64() - // Short circuit if the owner header is unknown - if !bc.HasHeader(blockHash, blockNumber) { - return i, fmt.Errorf("containing header #%d [%x..] unknown", blockNumber, blockHash.Bytes()[:4]) + defer bc.chainmu.Unlock() + + // Rewind may have occurred, skip in that case. + if bc.CurrentHeader().Number.Cmp(head.Number()) >= 0 { + currentFastBlock, td := bc.CurrentFastBlock(), bc.GetTd(head.Hash(), head.NumberU64()) + if bc.GetTd(currentFastBlock.Hash(), currentFastBlock.NumberU64()).Cmp(td) < 0 { + rawdb.WriteHeadFastBlockHash(bc.db, head.Hash()) + bc.currentFastBlock.Store(head) + headFastBlockGauge.Update(int64(head.NumberU64())) + return true + } } - // Skip if the entire data is already known - if bc.HasBlock(blockHash, blockNumber) { - stats.ignored++ - continue + return false + } + + // writeAncient writes blockchain and corresponding receipt chain into ancient store. + // + // this function only accepts canonical chain data. All side chain will be reverted + // eventually. + writeAncient := func(blockChain types.Blocks, receiptChain []types.Receipts) (int, error) { + first := blockChain[0] + last := blockChain[len(blockChain)-1] + + // Ensure genesis is in ancients. + if first.NumberU64() == 1 { + if frozen, _ := bc.db.Ancients(); frozen == 0 { + b := bc.genesisBlock + td := bc.genesisBlock.Difficulty() + writeSize, err := rawdb.WriteAncientBlocks(bc.db, []*types.Block{b}, []types.Receipts{nil}, td) + size += writeSize + if err != nil { + log.Error("Error writing genesis to ancients", "err", err) + return 0, err + } + log.Info("Wrote genesis to ancients") + } } - // Compute all the non-consensus fields of the receipts - if err := receipts.DeriveFields(bc.chainConfig, blockHash, blockNumber, block.BaseFee(), block.Transactions()); err != nil { - return i, fmt.Errorf("failed to derive receipts data: %v", err) + // Before writing the blocks to the ancients, we need to ensure that + // they correspond to the what the headerchain 'expects'. + // We only check the last block/header, since it's a contiguous chain. + if !bc.HasHeader(last.Hash(), last.NumberU64()) { + return 0, fmt.Errorf("containing header #%d [%x..] unknown", last.Number(), last.Hash().Bytes()[:4]) } - // Write all the data out into the database - rawdb.WriteBody(batch, blockHash, blockNumber, block.Body()) - rawdb.WriteReceipts(batch, blockHash, blockNumber, receipts) - rawdb.WriteTxLookupEntriesByBlock(batch, block) + // Write all chain data to ancients. + td := bc.GetTd(first.Hash(), first.NumberU64()) + writeSize, err := rawdb.WriteAncientBlocks(bc.db, blockChain, receiptChain, td) + size += writeSize + if err != nil { + log.Error("Error importing chain data to ancients", "err", err) + return 0, err + } + + // Write tx indices if any condition is satisfied: + // * If user requires to reserve all tx indices(txlookuplimit=0) + // * If all ancient tx indices are required to be reserved(txlookuplimit is even higher than ancientlimit) + // * If block number is large enough to be regarded as a recent block + // It means blocks below the ancientLimit-txlookupLimit won't be indexed. + // + // But if the `TxIndexTail` is not nil, e.g. Geth is initialized with + // an external ancient database, during the setup, blockchain will start + // a background routine to re-indexed all indices in [ancients - txlookupLimit, ancients) + // range. In this case, all tx indices of newly imported blocks should be + // generated. + var batch = bc.db.NewBatch() + for _, block := range blockChain { + if bc.txLookupLimit == 0 || ancientLimit <= bc.txLookupLimit || block.NumberU64() >= ancientLimit-bc.txLookupLimit { + rawdb.WriteTxLookupEntriesByBlock(batch, block) + } else if rawdb.ReadTxIndexTail(bc.db) != nil { + rawdb.WriteTxLookupEntriesByBlock(batch, block) + } + stats.processed++ + } + + // Flush all tx-lookup index data. + size += int64(batch.ValueSize()) + if err := batch.Write(); err != nil { + // The tx index data could not be written. + // Roll back the ancient store update. + fastBlock := bc.CurrentFastBlock().NumberU64() + if err := bc.db.TruncateHead(fastBlock + 1); err != nil { + log.Error("Can't truncate ancient store after failed insert", "err", err) + } + return 0, err + } + + // Sync the ancient store explicitly to ensure all data has been flushed to disk. + if err := bc.db.Sync(); err != nil { + return 0, err + } + + // Update the current fast block because all block data is now present in DB. + previousFastBlock := bc.CurrentFastBlock().NumberU64() + if !updateHead(blockChain[len(blockChain)-1]) { + // We end up here if the header chain has reorg'ed, and the blocks/receipts + // don't match the canonical chain. + if err := bc.db.TruncateHead(previousFastBlock + 1); err != nil { + log.Error("Can't truncate ancient store after failed insert", "err", err) + } + return 0, errSideChainReceipts + } + + // Delete block data from the main database. + batch.Reset() + canonHashes := make(map[common.Hash]struct{}) + for _, block := range blockChain { + canonHashes[block.Hash()] = struct{}{} + if block.NumberU64() == 0 { + continue + } + rawdb.DeleteCanonicalHash(batch, block.NumberU64()) + rawdb.DeleteBlockWithoutNumber(batch, block.Hash(), block.NumberU64()) + } + // Delete side chain hash-to-number mappings. + for _, nh := range rawdb.ReadAllHashesInRange(bc.db, first.NumberU64(), last.NumberU64()) { + if _, canon := canonHashes[nh.Hash]; !canon { + rawdb.DeleteHeader(batch, nh.Hash, nh.Number) + } + } + if err := batch.Write(); err != nil { + return 0, err + } + return 0, nil + } + + // writeLive writes blockchain and corresponding receipt chain into active store. + writeLive := func(blockChain types.Blocks, receiptChain []types.Receipts) (int, error) { + skipPresenceCheck := false + batch := bc.db.NewBatch() + for i, block := range blockChain { + // Short circuit insertion if shutting down or processing failed + if bc.insertStopped() { + return 0, errInsertionInterrupted + } + // Short circuit if the owner header is unknown + if !bc.HasHeader(block.Hash(), block.NumberU64()) { + return i, fmt.Errorf("containing header #%d [%x..] unknown", block.Number(), block.Hash().Bytes()[:4]) + } + if !skipPresenceCheck { + // Ignore if the entire data is already known + if bc.HasBlock(block.Hash(), block.NumberU64()) { + stats.ignored++ + continue + } else { + // If block N is not present, neither are the later blocks. + // This should be true, but if we are mistaken, the shortcut + // here will only cause overwriting of some existing data + skipPresenceCheck = true + } + } + // Write all the data out into the database + rawdb.WriteBody(batch, block.Hash(), block.NumberU64(), block.Body()) + rawdb.WriteReceipts(batch, block.Hash(), block.NumberU64(), receiptChain[i]) + rawdb.WriteTxLookupEntriesByBlock(batch, block) // Always write tx indices for live blocks, we assume they are needed + + // Write everything belongs to the blocks into the database. So that + // we can ensure all components of body is completed(body, receipts, + // tx indexes) + if batch.ValueSize() >= ethdb.IdealBatchSize { + if err := batch.Write(); err != nil { + return 0, err + } + size += int64(batch.ValueSize()) + batch.Reset() + } + stats.processed++ + } // Write everything belongs to the blocks into the database. So that // we can ensure all components of body is completed(body, receipts, // tx indexes) - if batch.ValueSize() >= ethdb.IdealBatchSize { + if batch.ValueSize() > 0 { + size += int64(batch.ValueSize()) if err := batch.Write(); err != nil { return 0, err } - bytes += batch.ValueSize() - batch.Reset() } - stats.processed++ + updateHead(blockChain[len(blockChain)-1]) + return 0, nil } - // Write everything belongs to the blocks into the database. So that - // we can ensure all components of body is completed(body, receipts, - // tx indexes) - if batch.ValueSize() > 0 { - bytes += batch.ValueSize() - if err := batch.Write(); err != nil { - return 0, err + + // Write downloaded chain data and corresponding receipt chain data + if len(ancientBlocks) > 0 { + if n, err := writeAncient(ancientBlocks, ancientReceipts); err != nil { + if err == errInsertionInterrupted { + return 0, nil + } + return n, err } } - - // Update the head fast sync block if better - if !bc.chainmu.TryLock() { - return 0, errChainStopped + // Write the tx index tail (block number from where we index) before write any live blocks + if len(liveBlocks) > 0 && liveBlocks[0].NumberU64() == ancientLimit+1 { + // The tx index tail can only be one of the following two options: + // * 0: all ancient blocks have been indexed + // * ancient-limit: the indices of blocks before ancient-limit are ignored + if tail := rawdb.ReadTxIndexTail(bc.db); tail == nil { + if bc.txLookupLimit == 0 || ancientLimit <= bc.txLookupLimit { + rawdb.WriteTxIndexTail(bc.db, 0) + } else { + rawdb.WriteTxIndexTail(bc.db, ancientLimit-bc.txLookupLimit) + } + } } - head := blockChain[len(blockChain)-1] - if td := bc.GetTd(head.Hash(), head.NumberU64()); td != nil { // Rewind may have occurred, skip in that case - currentFastBlock := bc.CurrentFastBlock() - if bc.GetTd(currentFastBlock.Hash(), currentFastBlock.NumberU64()).Cmp(td) < 0 { - rawdb.WriteHeadFastBlockHash(bc.db, head.Hash()) - bc.currentFastBlock.Store(head) - headFastBlockGauge.Update(int64(head.NumberU64())) + if len(liveBlocks) > 0 { + if n, err := writeLive(liveBlocks, liveReceipts); err != nil { + if err == errInsertionInterrupted { + return 0, nil + } + return n, err } } - bc.chainmu.Unlock() - log.Info("Imported new block receipts", - "count", stats.processed, - "elapsed", common.PrettyDuration(time.Since(start)), - "number", head.Number(), - "hash", head.Hash(), - "size", common.StorageSize(bytes), - "ignored", stats.ignored) + head := blockChain[len(blockChain)-1] + context := []interface{}{ + "count", stats.processed, "elapsed", common.PrettyDuration(time.Since(start)), + "number", head.Number(), "hash", head.Hash(), "age", common.PrettyAge(time.Unix(head.Time().Int64(), 0)), + "size", common.StorageSize(size), + } + if stats.ignored > 0 { + context = append(context, []interface{}{"ignored", stats.ignored}...) + } + log.Info("Imported new block receipts", context...) + return 0, nil } +// SetTxLookupLimit is responsible for updating the txlookup limit to the +// original one stored in db if the new mismatches with the old one. +func (bc *BlockChain) SetTxLookupLimit(limit uint64) { + bc.txLookupLimit = limit +} + +// TxLookupLimit retrieves the txlookup limit used by blockchain to prune +// stale transaction indices. +func (bc *BlockChain) TxLookupLimit() uint64 { + return bc.txLookupLimit +} + var lastWrite uint64 // writeBlockWithoutState writes only the block and its metadata to the database, @@ -2581,6 +2833,86 @@ func (bc *BlockChain) futureBlocksLoop() { } } +// maintainTxIndex is responsible for the construction and deletion of the +// transaction index. +// +// User can use flag `txlookuplimit` to specify a "recentness" block, below +// which ancient tx indices get deleted. If `txlookuplimit` is 0, it means +// all tx indices will be reserved. +// +// The user can adjust the txlookuplimit value for each launch after fast +// sync, Geth will automatically construct the missing indices and delete +// the extra indices. +func (bc *BlockChain) maintainTxIndex(ancients uint64) { + // Before starting the actual maintenance, we need to handle a special case, + // where user might init Geth with an external ancient database. If so, we + // need to reindex all necessary transactions before starting to process any + // pruning requests. + if ancients > 0 { + var from = uint64(0) + if bc.txLookupLimit != 0 && ancients > bc.txLookupLimit { + from = ancients - bc.txLookupLimit + } + rawdb.IndexTransactions(bc.db, from, ancients) + } + // indexBlocks reindexes or unindexes transactions depending on user configuration + indexBlocks := func(tail *uint64, head uint64, done chan struct{}) { + defer func() { done <- struct{}{} }() + + // If the user just upgraded Geth to a new version which supports transaction + // index pruning, write the new tail and remove anything older. + if tail == nil { + if bc.txLookupLimit == 0 || head < bc.txLookupLimit { + // Nothing to delete, write the tail and return + rawdb.WriteTxIndexTail(bc.db, 0) + } else { + // Prune all stale tx indices and record the tx index tail + rawdb.UnindexTransactions(bc.db, 0, head-bc.txLookupLimit+1) + } + return + } + // If a previous indexing existed, make sure that we fill in any missing entries + if bc.txLookupLimit == 0 || head < bc.txLookupLimit { + if *tail > 0 { + rawdb.IndexTransactions(bc.db, 0, *tail) + } + return + } + // Update the transaction index to the new chain state + if head-bc.txLookupLimit+1 < *tail { + // Reindex a part of missing indices and rewind index tail to HEAD-limit + rawdb.IndexTransactions(bc.db, head-bc.txLookupLimit+1, *tail) + } else { + // Unindex a part of stale indices and forward index tail to HEAD-limit + rawdb.UnindexTransactions(bc.db, *tail, head-bc.txLookupLimit+1) + } + } + // Any reindexing done, start listening to chain events and moving the index window + var ( + done chan struct{} // Non-nil if background unindexing or reindexing routine is active. + headCh = make(chan ChainHeadEvent, 1) // Buffered to avoid locking up the event feed + ) + sub := bc.SubscribeChainHeadEvent(headCh) + if sub == nil { + return + } + defer sub.Unsubscribe() + + for { + select { + case head := <-headCh: + if done == nil { + done = make(chan struct{}) + go indexBlocks(rawdb.ReadTxIndexTail(bc.db), head.Block.NumberU64(), done) + } + case <-done: + done = nil + case <-bc.quit: + return + } + } +} + // BadBlockArgs represents the entries in the list returned when bad blocks are queried. type BadBlockArgs struct { Hash common.Hash `json:"hash"` diff --git a/core/blockchain_repair_test.go b/core/blockchain_repair_test.go new file mode 100644 index 00000000000..1a596a29516 --- /dev/null +++ b/core/blockchain_repair_test.go @@ -0,0 +1,1654 @@ +// Copyright 2020 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Tests that abnormal program termination (i.e.crash) and restart doesn't leave +// the database in some strange state with gaps in the chain, nor with block data +// dangling in the future. + +package core + +import ( + "math/big" + "os" + "testing" + + "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/consensus/ethash" + "github.com/XinFinOrg/XDPoSChain/core/rawdb" + "github.com/XinFinOrg/XDPoSChain/core/types" + "github.com/XinFinOrg/XDPoSChain/core/vm" + "github.com/XinFinOrg/XDPoSChain/params" +) + +// Tests a recovery for a short canonical chain where a recent block was already +// committed to disk and then the process crashed. In this case we expect the full +// chain to be rolled back to the committed block, but the chain data itself left +// in the database for replaying. +func TestShortRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // + // Frozen: none + // Commit: G, C4 + // Pivot : none + // + // CRASH + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Expected head header : C8 + // Expected head fast block: C8 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 0, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + expCanonicalBlocks: 8, + expSidechainBlocks: 0, + expFrozen: 0, + expHeadHeader: 8, + expHeadFastBlock: 8, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a short canonical chain where the fast sync pivot point was +// already committed, after which the process crashed. In this case we expect the full +// chain to be rolled back to the committed block, but the chain data itself left in +// the database for replaying. +func TestShortFastSyncedRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // + // Frozen: none + // Commit: G, C4 + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Expected head header : C8 + // Expected head fast block: C8 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 0, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 8, + expSidechainBlocks: 0, + expFrozen: 0, + expHeadHeader: 8, + expHeadFastBlock: 8, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a short canonical chain where the fast sync pivot point was +// not yet committed, but the process crashed. In this case we expect the chain to +// detect that it was fast syncing and not delete anything, since we can just pick +// up directly where we left off. +func TestShortFastSyncingRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // + // Frozen: none + // Commit: G + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Expected head header : C8 + // Expected head fast block: C8 + // Expected head block : G + testRepair(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 0, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 8, + expSidechainBlocks: 0, + expFrozen: 0, + expHeadHeader: 8, + expHeadFastBlock: 8, + expHeadBlock: 0, + }) +} + +// Tests a recovery for a short canonical chain and a shorter side chain, where a +// recent block was already committed to disk and then the process crashed. In this +// test scenario the side chain is below the committed block. In this case we expect +// the canonical chain to be rolled back to the committed block, but the chain data +// itself left in the database for replaying. +func TestShortOldForkedRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // â””->S1->S2->S3 + // + // Frozen: none + // Commit: G, C4 + // Pivot : none + // + // CRASH + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // â””->S1->S2->S3 + // + // Expected head header : C8 + // Expected head fast block: C8 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 3, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + expCanonicalBlocks: 8, + expSidechainBlocks: 3, + expFrozen: 0, + expHeadHeader: 8, + expHeadFastBlock: 8, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a short canonical chain and a shorter side chain, where +// the fast sync pivot point was already committed to disk and then the process +// crashed. In this test scenario the side chain is below the committed block. In +// this case we expect the canonical chain to be rolled back to the committed block, +// but the chain data itself left in the database for replaying. +func TestShortOldForkedFastSyncedRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // â””->S1->S2->S3 + // + // Frozen: none + // Commit: G, C4 + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // â””->S1->S2->S3 + // + // Expected head header : C8 + // Expected head fast block: C8 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 3, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 8, + expSidechainBlocks: 3, + expFrozen: 0, + expHeadHeader: 8, + expHeadFastBlock: 8, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a short canonical chain and a shorter side chain, where +// the fast sync pivot point was not yet committed, but the process crashed. In this +// test scenario the side chain is below the committed block. In this case we expect +// the chain to detect that it was fast syncing and not delete anything, since we +// can just pick up directly where we left off. +func TestShortOldForkedFastSyncingRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // â””->S1->S2->S3 + // + // Frozen: none + // Commit: G + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // â””->S1->S2->S3 + // + // Expected head header : C8 + // Expected head fast block: C8 + // Expected head block : G + testRepair(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 3, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 8, + expSidechainBlocks: 3, + expFrozen: 0, + expHeadHeader: 8, + expHeadFastBlock: 8, + expHeadBlock: 0, + }) +} + +// Tests a recovery for a short canonical chain and a shorter side chain, where a +// recent block was already committed to disk and then the process crashed. In this +// test scenario the side chain reaches above the committed block. In this case we +// expect the canonical chain to be rolled back to the committed block, but the +// chain data itself left in the database for replaying. +func TestShortNewlyForkedRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // â””->S1->S2->S3->S4->S5->S6 + // + // Frozen: none + // Commit: G, C4 + // Pivot : none + // + // CRASH + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // â””->S1->S2->S3->S4->S5->S6 + // + // Expected head header : C8 + // Expected head fast block: C8 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 6, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + expCanonicalBlocks: 8, + expSidechainBlocks: 6, + expFrozen: 0, + expHeadHeader: 8, + expHeadFastBlock: 8, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a short canonical chain and a shorter side chain, where +// the fast sync pivot point was already committed to disk and then the process +// crashed. In this test scenario the side chain reaches above the committed block. +// In this case we expect the canonical chain to be rolled back to the committed +// block, but the chain data itself left in the database for replaying. +func TestShortNewlyForkedFastSyncedRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // â””->S1->S2->S3->S4->S5->S6 + // + // Frozen: none + // Commit: G, C4 + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // â””->S1->S2->S3->S4->S5->S6 + // + // Expected head header : C8 + // Expected head fast block: C8 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 6, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 8, + expSidechainBlocks: 6, + expFrozen: 0, + expHeadHeader: 8, + expHeadFastBlock: 8, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a short canonical chain and a shorter side chain, where +// the fast sync pivot point was not yet committed, but the process crashed. In +// this test scenario the side chain reaches above the committed block. In this +// case we expect the chain to detect that it was fast syncing and not delete +// anything, since we can just pick up directly where we left off. +func TestShortNewlyForkedFastSyncingRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // â””->S1->S2->S3->S4->S5->S6 + // + // Frozen: none + // Commit: G + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // â””->S1->S2->S3->S4->S5->S6 + // + // Expected head header : C8 + // Expected head fast block: C8 + // Expected head block : G + testRepair(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 6, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 8, + expSidechainBlocks: 6, + expFrozen: 0, + expHeadHeader: 8, + expHeadFastBlock: 8, + expHeadBlock: 0, + }) +} + +// Tests a recovery for a short canonical chain and a longer side chain, where a +// recent block was already committed to disk and then the process crashed. In this +// case we expect the canonical chain to be rolled back to the committed block, but +// the chain data itself left in the database for replaying. +func TestShortReorgedRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10 + // + // Frozen: none + // Commit: G, C4 + // Pivot : none + // + // CRASH + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10 + // + // Expected head header : C8 + // Expected head fast block: C8 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 10, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + expCanonicalBlocks: 8, + expSidechainBlocks: 10, + expFrozen: 0, + expHeadHeader: 8, + expHeadFastBlock: 8, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a short canonical chain and a longer side chain, where +// the fast sync pivot point was already committed to disk and then the process +// crashed. In this case we expect the canonical chain to be rolled back to the +// committed block, but the chain data itself left in the database for replaying. +func TestShortReorgedFastSyncedRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10 + // + // Frozen: none + // Commit: G, C4 + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10 + // + // Expected head header : C8 + // Expected head fast block: C8 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 10, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 8, + expSidechainBlocks: 10, + expFrozen: 0, + expHeadHeader: 8, + expHeadFastBlock: 8, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a short canonical chain and a longer side chain, where +// the fast sync pivot point was not yet committed, but the process crashed. In +// this case we expect the chain to detect that it was fast syncing and not delete +// anything, since we can just pick up directly where we left off. +func TestShortReorgedFastSyncingRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10 + // + // Frozen: none + // Commit: G + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10 + // + // Expected head header : C8 + // Expected head fast block: C8 + // Expected head block : G + testRepair(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 10, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 8, + expSidechainBlocks: 10, + expFrozen: 0, + expHeadHeader: 8, + expHeadFastBlock: 8, + expHeadBlock: 0, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks where a recent +// block - newer than the ancient limit - was already committed to disk and then +// the process crashed. In this case we expect the chain to be rolled back to the +// committed block, with everything afterwads kept as fast sync data. +func TestLongShallowRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // + // Frozen: + // G->C1->C2 + // + // Commit: G, C4 + // Pivot : none + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 + // + // Expected head header : C18 + // Expected head fast block: C18 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 0, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + expCanonicalBlocks: 18, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 18, + expHeadFastBlock: 18, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks where a recent +// block - older than the ancient limit - was already committed to disk and then +// the process crashed. In this case we expect the chain to be rolled back to the +// committed block, with everything afterwads deleted. +func TestLongDeepRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G, C4 + // Pivot : none + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4 + // + // Expected in leveldb: none + // + // Expected head header : C4 + // Expected head fast block: C4 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 0, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + expCanonicalBlocks: 4, + expSidechainBlocks: 0, + expFrozen: 5, + expHeadHeader: 4, + expHeadFastBlock: 4, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks where the fast +// sync pivot point - newer than the ancient limit - was already committed, after +// which the process crashed. In this case we expect the chain to be rolled back +// to the committed block, with everything afterwads kept as fast sync data. +func TestLongFastSyncedShallowRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // + // Frozen: + // G->C1->C2 + // + // Commit: G, C4 + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 + // + // Expected head header : C18 + // Expected head fast block: C18 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 0, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 18, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 18, + expHeadFastBlock: 18, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks where the fast +// sync pivot point - older than the ancient limit - was already committed, after +// which the process crashed. In this case we expect the chain to be rolled back +// to the committed block, with everything afterwads deleted. +func TestLongFastSyncedDeepRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G, C4 + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4 + // + // Expected in leveldb: none + // + // Expected head header : C4 + // Expected head fast block: C4 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 0, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 4, + expSidechainBlocks: 0, + expFrozen: 5, + expHeadHeader: 4, + expHeadFastBlock: 4, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks where the fast +// sync pivot point - older than the ancient limit - was not yet committed, but the +// process crashed. In this case we expect the chain to detect that it was fast +// syncing and not delete anything, since we can just pick up directly where we +// left off. +func TestLongFastSyncingShallowRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // + // Frozen: + // G->C1->C2 + // + // Commit: G + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 + // + // Expected head header : C18 + // Expected head fast block: C18 + // Expected head block : G + testRepair(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 0, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 18, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 18, + expHeadFastBlock: 18, + expHeadBlock: 0, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks where the fast +// sync pivot point - newer than the ancient limit - was not yet committed, but the +// process crashed. In this case we expect the chain to detect that it was fast +// syncing and not delete anything, since we can just pick up directly where we +// left off. +func TestLongFastSyncingDeepRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Expected in leveldb: + // C8)->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 + // + // Expected head header : C24 + // Expected head fast block: C24 + // Expected head block : G + testRepair(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 0, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 24, + expSidechainBlocks: 0, + expFrozen: 9, + expHeadHeader: 24, + expHeadFastBlock: 24, + expHeadBlock: 0, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks and a shorter +// side chain, where a recent block - newer than the ancient limit - was already +// committed to disk and then the process crashed. In this test scenario the side +// chain is below the committed block. In this case we expect the chain to be +// rolled back to the committed block, with everything afterwads kept as fast +// sync data; the side chain completely nuked by the freezer. +func TestLongOldForkedShallowRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // â””->S1->S2->S3 + // + // Frozen: + // G->C1->C2 + // + // Commit: G, C4 + // Pivot : none + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 + // + // Expected head header : C18 + // Expected head fast block: C18 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 3, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + expCanonicalBlocks: 18, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 18, + expHeadFastBlock: 18, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks and a shorter +// side chain, where a recent block - older than the ancient limit - was already +// committed to disk and then the process crashed. In this test scenario the side +// chain is below the committed block. In this case we expect the canonical chain +// to be rolled back to the committed block, with everything afterwads deleted; +// the side chain completely nuked by the freezer. +func TestLongOldForkedDeepRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // â””->S1->S2->S3 + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G, C4 + // Pivot : none + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4 + // + // Expected in leveldb: none + // + // Expected head header : C4 + // Expected head fast block: C4 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 3, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + expCanonicalBlocks: 4, + expSidechainBlocks: 0, + expFrozen: 5, + expHeadHeader: 4, + expHeadFastBlock: 4, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks and a shorter +// side chain, where the fast sync pivot point - newer than the ancient limit - +// was already committed to disk and then the process crashed. In this test scenario +// the side chain is below the committed block. In this case we expect the chain +// to be rolled back to the committed block, with everything afterwads kept as +// fast sync data; the side chain completely nuked by the freezer. +func TestLongOldForkedFastSyncedShallowRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // â””->S1->S2->S3 + // + // Frozen: + // G->C1->C2 + // + // Commit: G, C4 + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 + // + // Expected head header : C18 + // Expected head fast block: C18 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 3, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 18, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 18, + expHeadFastBlock: 18, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks and a shorter +// side chain, where the fast sync pivot point - older than the ancient limit - +// was already committed to disk and then the process crashed. In this test scenario +// the side chain is below the committed block. In this case we expect the canonical +// chain to be rolled back to the committed block, with everything afterwads deleted; +// the side chain completely nuked by the freezer. +func TestLongOldForkedFastSyncedDeepRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // â””->S1->S2->S3 + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G, C4 + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4 + // + // Expected in leveldb: none + // + // Expected head header : C4 + // Expected head fast block: C4 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 3, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 4, + expSidechainBlocks: 0, + expFrozen: 5, + expHeadHeader: 4, + expHeadFastBlock: 4, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks and a shorter +// side chain, where the fast sync pivot point - older than the ancient limit - +// was not yet committed, but the process crashed. In this test scenario the side +// chain is below the committed block. In this case we expect the chain to detect +// that it was fast syncing and not delete anything. The side chain is completely +// nuked by the freezer. +func TestLongOldForkedFastSyncingShallowRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // â””->S1->S2->S3 + // + // Frozen: + // G->C1->C2 + // + // Commit: G + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 + // + // Expected head header : C18 + // Expected head fast block: C18 + // Expected head block : G + testRepair(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 3, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 18, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 18, + expHeadFastBlock: 18, + expHeadBlock: 0, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks and a shorter +// side chain, where the fast sync pivot point - older than the ancient limit - +// was not yet committed, but the process crashed. In this test scenario the side +// chain is below the committed block. In this case we expect the chain to detect +// that it was fast syncing and not delete anything. The side chain is completely +// nuked by the freezer. +func TestLongOldForkedFastSyncingDeepRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // â””->S1->S2->S3 + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Expected in leveldb: + // C8)->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 + // + // Expected head header : C24 + // Expected head fast block: C24 + // Expected head block : G + testRepair(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 3, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 24, + expSidechainBlocks: 0, + expFrozen: 9, + expHeadHeader: 24, + expHeadFastBlock: 24, + expHeadBlock: 0, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks and a shorter +// side chain, where a recent block - newer than the ancient limit - was already +// committed to disk and then the process crashed. In this test scenario the side +// chain is above the committed block. In this case we expect the chain to be +// rolled back to the committed block, with everything afterwads kept as fast +// sync data; the side chain completely nuked by the freezer. +func TestLongNewerForkedShallowRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12 + // + // Frozen: + // G->C1->C2 + // + // Commit: G, C4 + // Pivot : none + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 + // + // Expected head header : C18 + // Expected head fast block: C18 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 12, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + expCanonicalBlocks: 18, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 18, + expHeadFastBlock: 18, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks and a shorter +// side chain, where a recent block - older than the ancient limit - was already +// committed to disk and then the process crashed. In this test scenario the side +// chain is above the committed block. In this case we expect the canonical chain +// to be rolled back to the committed block, with everything afterwads deleted; +// the side chain completely nuked by the freezer. +func TestLongNewerForkedDeepRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12 + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G, C4 + // Pivot : none + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4 + // + // Expected in leveldb: none + // + // Expected head header : C4 + // Expected head fast block: C4 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 12, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + expCanonicalBlocks: 4, + expSidechainBlocks: 0, + expFrozen: 5, + expHeadHeader: 4, + expHeadFastBlock: 4, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks and a shorter +// side chain, where the fast sync pivot point - newer than the ancient limit - +// was already committed to disk and then the process crashed. In this test scenario +// the side chain is above the committed block. In this case we expect the chain +// to be rolled back to the committed block, with everything afterwads kept as fast +// sync data; the side chain completely nuked by the freezer. +func TestLongNewerForkedFastSyncedShallowRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12 + // + // Frozen: + // G->C1->C2 + // + // Commit: G, C4 + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 + // + // Expected head header : C18 + // Expected head fast block: C18 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 12, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 18, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 18, + expHeadFastBlock: 18, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks and a shorter +// side chain, where the fast sync pivot point - older than the ancient limit - +// was already committed to disk and then the process crashed. In this test scenario +// the side chain is above the committed block. In this case we expect the canonical +// chain to be rolled back to the committed block, with everything afterwads deleted; +// the side chain completely nuked by the freezer. +func TestLongNewerForkedFastSyncedDeepRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12 + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G, C4 + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4 + // + // Expected in leveldb: none + // + // Expected head header : C4 + // Expected head fast block: C4 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 12, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 4, + expSidechainBlocks: 0, + expFrozen: 5, + expHeadHeader: 4, + expHeadFastBlock: 4, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks and a shorter +// side chain, where the fast sync pivot point - older than the ancient limit - +// was not yet committed, but the process crashed. In this test scenario the side +// chain is above the committed block. In this case we expect the chain to detect +// that it was fast syncing and not delete anything. The side chain is completely +// nuked by the freezer. +func TestLongNewerForkedFastSyncingShallowRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12 + // + // Frozen: + // G->C1->C2 + // + // Commit: G + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 + // + // Expected head header : C18 + // Expected head fast block: C18 + // Expected head block : G + testRepair(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 12, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 18, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 18, + expHeadFastBlock: 18, + expHeadBlock: 0, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks and a shorter +// side chain, where the fast sync pivot point - older than the ancient limit - +// was not yet committed, but the process crashed. In this test scenario the side +// chain is above the committed block. In this case we expect the chain to detect +// that it was fast syncing and not delete anything. The side chain is completely +// nuked by the freezer. +func TestLongNewerForkedFastSyncingDeepRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12 + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Expected in leveldb: + // C8)->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 + // + // Expected head header : C24 + // Expected head fast block: C24 + // Expected head block : G + testRepair(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 12, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 24, + expSidechainBlocks: 0, + expFrozen: 9, + expHeadHeader: 24, + expHeadFastBlock: 24, + expHeadBlock: 0, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks and a longer side +// chain, where a recent block - newer than the ancient limit - was already committed +// to disk and then the process crashed. In this case we expect the chain to be +// rolled back to the committed block, with everything afterwads kept as fast sync +// data. The side chain completely nuked by the freezer. +func TestLongReorgedShallowRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12->S13->S14->S15->S16->S17->S18->S19->S20->S21->S22->S23->S24->S25->S26 + // + // Frozen: + // G->C1->C2 + // + // Commit: G, C4 + // Pivot : none + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 + // + // Expected head header : C18 + // Expected head fast block: C18 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 26, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + expCanonicalBlocks: 18, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 18, + expHeadFastBlock: 18, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks and a longer side +// chain, where a recent block - older than the ancient limit - was already committed +// to disk and then the process crashed. In this case we expect the canonical chains +// to be rolled back to the committed block, with everything afterwads deleted. The +// side chain completely nuked by the freezer. +func TestLongReorgedDeepRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12->S13->S14->S15->S16->S17->S18->S19->S20->S21->S22->S23->S24->S25->S26 + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G, C4 + // Pivot : none + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4 + // + // Expected in leveldb: none + // + // Expected head header : C4 + // Expected head fast block: C4 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 26, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + expCanonicalBlocks: 4, + expSidechainBlocks: 0, + expFrozen: 5, + expHeadHeader: 4, + expHeadFastBlock: 4, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks and a longer +// side chain, where the fast sync pivot point - newer than the ancient limit - +// was already committed to disk and then the process crashed. In this case we +// expect the chain to be rolled back to the committed block, with everything +// afterwads kept as fast sync data. The side chain completely nuked by the +// freezer. +func TestLongReorgedFastSyncedShallowRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12->S13->S14->S15->S16->S17->S18->S19->S20->S21->S22->S23->S24->S25->S26 + // + // Frozen: + // G->C1->C2 + // + // Commit: G, C4 + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 + // + // Expected head header : C18 + // Expected head fast block: C18 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 26, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 18, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 18, + expHeadFastBlock: 18, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks and a longer +// side chain, where the fast sync pivot point - older than the ancient limit - +// was already committed to disk and then the process crashed. In this case we +// expect the canonical chains to be rolled back to the committed block, with +// everything afterwads deleted. The side chain completely nuked by the freezer. +func TestLongReorgedFastSyncedDeepRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12->S13->S14->S15->S16->S17->S18->S19->S20->S21->S22->S23->S24->S25->S26 + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G, C4 + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4 + // + // Expected in leveldb: none + // + // Expected head header : C4 + // Expected head fast block: C4 + // Expected head block : C4 + testRepair(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 26, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 4, + expSidechainBlocks: 0, + expFrozen: 5, + expHeadHeader: 4, + expHeadFastBlock: 4, + expHeadBlock: 4, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks and a longer +// side chain, where the fast sync pivot point - newer than the ancient limit - +// was not yet committed, but the process crashed. In this case we expect the +// chain to detect that it was fast syncing and not delete anything, since we +// can just pick up directly where we left off. +func TestLongReorgedFastSyncingShallowRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12->S13->S14->S15->S16->S17->S18->S19->S20->S21->S22->S23->S24->S25->S26 + // + // Frozen: + // G->C1->C2 + // + // Commit: G + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 + // + // Expected head header : C18 + // Expected head fast block: C18 + // Expected head block : G + testRepair(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 26, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 18, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 18, + expHeadFastBlock: 18, + expHeadBlock: 0, + }) +} + +// Tests a recovery for a long canonical chain with frozen blocks and a longer +// side chain, where the fast sync pivot point - older than the ancient limit - +// was not yet committed, but the process crashed. In this case we expect the +// chain to detect that it was fast syncing and not delete anything, since we +// can just pick up directly where we left off. +func TestLongReorgedFastSyncingDeepRepair(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12->S13->S14->S15->S16->S17->S18->S19->S20->S21->S22->S23->S24->S25->S26 + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G + // Pivot : C4 + // + // CRASH + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Expected in leveldb: + // C8)->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 + // + // Expected head header : C24 + // Expected head fast block: C24 + // Expected head block : G + testRepair(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 26, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + expCanonicalBlocks: 24, + expSidechainBlocks: 0, + expFrozen: 9, + expHeadHeader: 24, + expHeadFastBlock: 24, + expHeadBlock: 0, + }) +} + +func testRepair(t *testing.T, tt *rewindTest) { + // It's hard to follow the test case, visualize the input + //log.Root().SetHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true)))) + //fmt.Println(tt.dump(true)) + + // Create a temporary persistent database + datadir, err := os.MkdirTemp("", "") + if err != nil { + t.Fatalf("Failed to create temporary datadir: %v", err) + } + os.RemoveAll(datadir) + + db, err := rawdb.NewLevelDBDatabaseWithFreezer(datadir, 0, 0, datadir, "", false) + if err != nil { + t.Fatalf("Failed to create persistent database: %v", err) + } + defer db.Close() // Might double close, should be fine + + // Initialize a fresh chain + var ( + genesis = (&Genesis{ + Config: params.TestChainConfig, + }).MustCommit(db) + engine = ethash.NewFullFaker() + ) + chain, err := NewBlockChain(db, nil, params.AllEthashProtocolChanges, engine, vm.Config{}, nil) + if err != nil { + t.Fatalf("Failed to create chain: %v", err) + } + // If sidechain blocks are needed, make a light chain and import it + var sideblocks types.Blocks + if tt.sidechainBlocks > 0 { + sideblocks, _ = GenerateChain(params.TestChainConfig, genesis, engine, rawdb.NewMemoryDatabase(), tt.sidechainBlocks, func(i int, b *BlockGen) { + b.SetCoinbase(common.Address{0x01}) + }) + if _, err := chain.InsertChain(sideblocks); err != nil { + t.Fatalf("Failed to import side chain: %v", err) + } + } + canonblocks, _ := GenerateChain(params.TestChainConfig, genesis, engine, rawdb.NewMemoryDatabase(), tt.canonicalBlocks, func(i int, b *BlockGen) { + b.SetCoinbase(common.Address{0x02}) + b.SetDifficulty(big.NewInt(1000000)) + }) + if _, err := chain.InsertChain(canonblocks[:tt.commitBlock]); err != nil { + t.Fatalf("Failed to import canonical chain start: %v", err) + } + if tt.commitBlock > 0 { + chain.stateCache.TrieDB().Commit(canonblocks[tt.commitBlock-1].Root(), true) + } + if _, err := chain.InsertChain(canonblocks[tt.commitBlock:]); err != nil { + t.Fatalf("Failed to import canonical chain tail: %v", err) + } + // Force run a freeze cycle + type freezer interface { + Freeze(threshold uint64) error + Ancients() (uint64, error) + } + db.(freezer).Freeze(tt.freezeThreshold) + + // Set the simulated pivot block + if tt.pivotBlock != nil { + rawdb.WriteLastPivotNumber(db, *tt.pivotBlock) + } + // Pull the plug on the database, simulating a hard crash + db.Close() + + // Start a new blockchain back up and see where the repait leads us + db, err = rawdb.NewLevelDBDatabaseWithFreezer(datadir, 0, 0, datadir, "", false) + if err != nil { + t.Fatalf("Failed to reopen persistent database: %v", err) + } + defer db.Close() + + chain, err = NewBlockChain(db, nil, params.AllEthashProtocolChanges, engine, vm.Config{}, nil) + if err != nil { + t.Fatalf("Failed to recreate chain: %v", err) + } + defer chain.Stop() + + // Iterate over all the remaining blocks and ensure there are no gaps + verifyNoGaps(t, chain, true, canonblocks) + verifyNoGaps(t, chain, false, sideblocks) + verifyCutoff(t, chain, true, canonblocks, tt.expCanonicalBlocks) + verifyCutoff(t, chain, false, sideblocks, tt.expSidechainBlocks) + + if head := chain.CurrentHeader(); head.Number.Uint64() != tt.expHeadHeader { + t.Errorf("Head header mismatch: have %d, want %d", head.Number, tt.expHeadHeader) + } + if head := chain.CurrentFastBlock(); head.NumberU64() != tt.expHeadFastBlock { + t.Errorf("Head fast block mismatch: have %d, want %d", head.NumberU64(), tt.expHeadFastBlock) + } + if head := chain.CurrentBlock(); head.NumberU64() != tt.expHeadBlock { + t.Errorf("Head block mismatch: have %d, want %d", head.NumberU64(), tt.expHeadBlock) + } + if frozen, err := db.(freezer).Ancients(); err != nil { + t.Errorf("Failed to retrieve ancient count: %v\n", err) + } else if int(frozen) != tt.expFrozen { + t.Errorf("Frozen block count mismatch: have %d, want %d", frozen, tt.expFrozen) + } +} diff --git a/core/blockchain_sethead_test.go b/core/blockchain_sethead_test.go new file mode 100644 index 00000000000..333bf82da39 --- /dev/null +++ b/core/blockchain_sethead_test.go @@ -0,0 +1,1950 @@ +// Copyright 2020 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Tests that setting the chain head backwards doesn't leave the database in some +// strange state with gaps in the chain, nor with block data dangling in the future. + +package core + +import ( + "fmt" + "math/big" + "os" + "strings" + "testing" + + "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/consensus/ethash" + "github.com/XinFinOrg/XDPoSChain/core/rawdb" + "github.com/XinFinOrg/XDPoSChain/core/types" + "github.com/XinFinOrg/XDPoSChain/core/vm" + "github.com/XinFinOrg/XDPoSChain/params" +) + +// rewindTest is a test case for chain rollback upon user request. +type rewindTest struct { + canonicalBlocks int // Number of blocks to generate for the canonical chain (heavier) + sidechainBlocks int // Number of blocks to generate for the side chain (lighter) + freezeThreshold uint64 // Block number until which to move things into the freezer + commitBlock uint64 // Block number for which to commit the state to disk + pivotBlock *uint64 // Pivot block number in case of fast sync + + setheadBlock uint64 // Block number to set head back to + expCanonicalBlocks int // Number of canonical blocks expected to remain in the database (excl. genesis) + expSidechainBlocks int // Number of sidechain blocks expected to remain in the database (excl. genesis) + expFrozen int // Number of canonical blocks expected to be in the freezer (incl. genesis) + expHeadHeader uint64 // Block number of the expected head header + expHeadFastBlock uint64 // Block number of the expected head fast sync block + expHeadBlock uint64 // Block number of the expected head full block +} + +func (tt *rewindTest) dump(crash bool) string { + buffer := new(strings.Builder) + + fmt.Fprint(buffer, "Chain:\n G") + for i := 0; i < tt.canonicalBlocks; i++ { + fmt.Fprintf(buffer, "->C%d", i+1) + } + fmt.Fprint(buffer, " (HEAD)\n") + if tt.sidechainBlocks > 0 { + fmt.Fprintf(buffer, " â””") + for i := 0; i < tt.sidechainBlocks; i++ { + fmt.Fprintf(buffer, "->S%d", i+1) + } + fmt.Fprintf(buffer, "\n") + } + fmt.Fprintf(buffer, "\n") + + if tt.canonicalBlocks > int(tt.freezeThreshold) { + fmt.Fprint(buffer, "Frozen:\n G") + for i := 0; i < tt.canonicalBlocks-int(tt.freezeThreshold); i++ { + fmt.Fprintf(buffer, "->C%d", i+1) + } + fmt.Fprintf(buffer, "\n\n") + } else { + fmt.Fprintf(buffer, "Frozen: none\n") + } + fmt.Fprintf(buffer, "Commit: G") + if tt.commitBlock > 0 { + fmt.Fprintf(buffer, ", C%d", tt.commitBlock) + } + fmt.Fprint(buffer, "\n") + + if tt.pivotBlock == nil { + fmt.Fprintf(buffer, "Pivot : none\n") + } else { + fmt.Fprintf(buffer, "Pivot : C%d\n", *tt.pivotBlock) + } + if crash { + fmt.Fprintf(buffer, "\nCRASH\n\n") + } else { + fmt.Fprintf(buffer, "\nSetHead(%d)\n\n", tt.setheadBlock) + } + fmt.Fprintf(buffer, "------------------------------\n\n") + + if tt.expFrozen > 0 { + fmt.Fprint(buffer, "Expected in freezer:\n G") + for i := 0; i < tt.expFrozen-1; i++ { + fmt.Fprintf(buffer, "->C%d", i+1) + } + fmt.Fprintf(buffer, "\n\n") + } + if tt.expFrozen > 0 { + if tt.expFrozen >= tt.expCanonicalBlocks { + fmt.Fprintf(buffer, "Expected in leveldb: none\n") + } else { + fmt.Fprintf(buffer, "Expected in leveldb:\n C%d)", tt.expFrozen-1) + for i := tt.expFrozen - 1; i < tt.expCanonicalBlocks; i++ { + fmt.Fprintf(buffer, "->C%d", i+1) + } + fmt.Fprint(buffer, "\n") + if tt.expSidechainBlocks > tt.expFrozen { + fmt.Fprintf(buffer, " â””") + for i := tt.expFrozen - 1; i < tt.expSidechainBlocks; i++ { + fmt.Fprintf(buffer, "->S%d", i+1) + } + fmt.Fprintf(buffer, "\n") + } + } + } else { + fmt.Fprint(buffer, "Expected in leveldb:\n G") + for i := tt.expFrozen; i < tt.expCanonicalBlocks; i++ { + fmt.Fprintf(buffer, "->C%d", i+1) + } + fmt.Fprint(buffer, "\n") + if tt.expSidechainBlocks > tt.expFrozen { + fmt.Fprintf(buffer, " â””") + for i := tt.expFrozen; i < tt.expSidechainBlocks; i++ { + fmt.Fprintf(buffer, "->S%d", i+1) + } + fmt.Fprintf(buffer, "\n") + } + } + fmt.Fprintf(buffer, "\n") + fmt.Fprintf(buffer, "Expected head header : C%d\n", tt.expHeadHeader) + fmt.Fprintf(buffer, "Expected head fast block: C%d\n", tt.expHeadFastBlock) + if tt.expHeadBlock == 0 { + fmt.Fprintf(buffer, "Expected head block : G\n") + } else { + fmt.Fprintf(buffer, "Expected head block : C%d\n", tt.expHeadBlock) + } + return buffer.String() +} + +// Tests a sethead for a short canonical chain where a recent block was already +// committed to disk and then the sethead called. In this case we expect the full +// chain to be rolled back to the committed block. Everything above the sethead +// point should be deleted. In between the committed block and the requested head +// the data can remain as "fast sync" data to avoid redownloading it. +func TestShortSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // + // Frozen: none + // Commit: G, C4 + // Pivot : none + // + // SetHead(7) + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7 + // + // Expected head header : C7 + // Expected head fast block: C7 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 0, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + setheadBlock: 7, + expCanonicalBlocks: 7, + expSidechainBlocks: 0, + expFrozen: 0, + expHeadHeader: 7, + expHeadFastBlock: 7, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a short canonical chain where the fast sync pivot point was +// already committed, after which sethead was called. In this case we expect the +// chain to behave like in full sync mode, rolling back to the committed block +// Everything above the sethead point should be deleted. In between the committed +// block and the requested head the data can remain as "fast sync" data to avoid +// redownloading it. +func TestShortFastSyncedSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // + // Frozen: none + // Commit: G, C4 + // Pivot : C4 + // + // SetHead(7) + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7 + // + // Expected head header : C7 + // Expected head fast block: C7 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 0, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + setheadBlock: 7, + expCanonicalBlocks: 7, + expSidechainBlocks: 0, + expFrozen: 0, + expHeadHeader: 7, + expHeadFastBlock: 7, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a short canonical chain where the fast sync pivot point was +// not yet committed, but sethead was called. In this case we expect the chain to +// detect that it was fast syncing and delete everything from the new head, since +// we can just pick up fast syncing from there. The head full block should be set +// to the genesis. +func TestShortFastSyncingSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // + // Frozen: none + // Commit: G + // Pivot : C4 + // + // SetHead(7) + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7 + // + // Expected head header : C7 + // Expected head fast block: C7 + // Expected head block : G + testSetHead(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 0, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + setheadBlock: 7, + expCanonicalBlocks: 7, + expSidechainBlocks: 0, + expFrozen: 0, + expHeadHeader: 7, + expHeadFastBlock: 7, + expHeadBlock: 0, + }) +} + +// Tests a sethead for a short canonical chain and a shorter side chain, where a +// recent block was already committed to disk and then sethead was called. In this +// test scenario the side chain is below the committed block. In this case we expect +// the canonical full chain to be rolled back to the committed block. Everything +// above the sethead point should be deleted. In between the committed block and +// the requested head the data can remain as "fast sync" data to avoid redownloading +// it. The side chain should be left alone as it was shorter. +func TestShortOldForkedSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // â””->S1->S2->S3 + // + // Frozen: none + // Commit: G, C4 + // Pivot : none + // + // SetHead(7) + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7 + // â””->S1->S2->S3 + // + // Expected head header : C7 + // Expected head fast block: C7 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 3, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + setheadBlock: 7, + expCanonicalBlocks: 7, + expSidechainBlocks: 3, + expFrozen: 0, + expHeadHeader: 7, + expHeadFastBlock: 7, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a short canonical chain and a shorter side chain, where +// the fast sync pivot point was already committed to disk and then sethead was +// called. In this test scenario the side chain is below the committed block. In +// this case we expect the canonical full chain to be rolled back to the committed +// block. Everything above the sethead point should be deleted. In between the +// committed block and the requested head the data can remain as "fast sync" data +// to avoid redownloading it. The side chain should be left alone as it was shorter. +func TestShortOldForkedFastSyncedSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // â””->S1->S2->S3 + // + // Frozen: none + // Commit: G, C4 + // Pivot : C4 + // + // SetHead(7) + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7 + // â””->S1->S2->S3 + // + // Expected head header : C7 + // Expected head fast block: C7 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 3, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + setheadBlock: 7, + expCanonicalBlocks: 7, + expSidechainBlocks: 3, + expFrozen: 0, + expHeadHeader: 7, + expHeadFastBlock: 7, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a short canonical chain and a shorter side chain, where +// the fast sync pivot point was not yet committed, but sethead was called. In this +// test scenario the side chain is below the committed block. In this case we expect +// the chain to detect that it was fast syncing and delete everything from the new +// head, since we can just pick up fast syncing from there. The head full block +// should be set to the genesis. +func TestShortOldForkedFastSyncingSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // â””->S1->S2->S3 + // + // Frozen: none + // Commit: G + // Pivot : C4 + // + // SetHead(7) + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7 + // â””->S1->S2->S3 + // + // Expected head header : C7 + // Expected head fast block: C7 + // Expected head block : G + testSetHead(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 3, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + setheadBlock: 7, + expCanonicalBlocks: 7, + expSidechainBlocks: 3, + expFrozen: 0, + expHeadHeader: 7, + expHeadFastBlock: 7, + expHeadBlock: 0, + }) +} + +// Tests a sethead for a short canonical chain and a shorter side chain, where a +// recent block was already committed to disk and then sethead was called. In this +// test scenario the side chain reaches above the committed block. In this case we +// expect the canonical full chain to be rolled back to the committed block. All +// data above the sethead point should be deleted. In between the committed block +// and the requested head the data can remain as "fast sync" data to avoid having +// to redownload it. The side chain should be truncated to the head set. +// +// The side chain could be left to be if the fork point was before the new head +// we are deleting to, but it would be exceedingly hard to detect that case and +// properly handle it, so we'll trade extra work in exchange for simpler code. +func TestShortNewlyForkedSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8 + // + // Frozen: none + // Commit: G, C4 + // Pivot : none + // + // SetHead(7) + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7 + // â””->S1->S2->S3->S4->S5->S6->S7 + // + // Expected head header : C7 + // Expected head fast block: C7 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 10, + sidechainBlocks: 8, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + setheadBlock: 7, + expCanonicalBlocks: 7, + expSidechainBlocks: 7, + expFrozen: 0, + expHeadHeader: 7, + expHeadFastBlock: 7, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a short canonical chain and a shorter side chain, where +// the fast sync pivot point was already committed to disk and then sethead was +// called. In this case we expect the canonical full chain to be rolled back to +// between the committed block and the requested head the data can remain as +// "fast sync" data to avoid having to redownload it. The side chain should be +// truncated to the head set. +// +// The side chain could be left to be if the fork point was before the new head +// we are deleting to, but it would be exceedingly hard to detect that case and +// properly handle it, so we'll trade extra work in exchange for simpler code. +func TestShortNewlyForkedFastSyncedSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8 + // + // Frozen: none + // Commit: G, C4 + // Pivot : C4 + // + // SetHead(7) + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7 + // â””->S1->S2->S3->S4->S5->S6->S7 + // + // Expected head header : C7 + // Expected head fast block: C7 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 10, + sidechainBlocks: 8, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + setheadBlock: 7, + expCanonicalBlocks: 7, + expSidechainBlocks: 7, + expFrozen: 0, + expHeadHeader: 7, + expHeadFastBlock: 7, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a short canonical chain and a shorter side chain, where +// the fast sync pivot point was not yet committed, but sethead was called. In +// this test scenario the side chain reaches above the committed block. In this +// case we expect the chain to detect that it was fast syncing and delete +// everything from the new head, since we can just pick up fast syncing from +// there. +// +// The side chain could be left to be if the fork point was before the new head +// we are deleting to, but it would be exceedingly hard to detect that case and +// properly handle it, so we'll trade extra work in exchange for simpler code. +func TestShortNewlyForkedFastSyncingSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8 + // + // Frozen: none + // Commit: G + // Pivot : C4 + // + // SetHead(7) + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7 + // â””->S1->S2->S3->S4->S5->S6->S7 + // + // Expected head header : C7 + // Expected head fast block: C7 + // Expected head block : G + testSetHead(t, &rewindTest{ + canonicalBlocks: 10, + sidechainBlocks: 8, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + setheadBlock: 7, + expCanonicalBlocks: 7, + expSidechainBlocks: 7, + expFrozen: 0, + expHeadHeader: 7, + expHeadFastBlock: 7, + expHeadBlock: 0, + }) +} + +// Tests a sethead for a short canonical chain and a longer side chain, where a +// recent block was already committed to disk and then sethead was called. In this +// case we expect the canonical full chain to be rolled back to the committed block. +// All data above the sethead point should be deleted. In between the committed +// block and the requested head the data can remain as "fast sync" data to avoid +// having to redownload it. The side chain should be truncated to the head set. +// +// The side chain could be left to be if the fork point was before the new head +// we are deleting to, but it would be exceedingly hard to detect that case and +// properly handle it, so we'll trade extra work in exchange for simpler code. +func TestShortReorgedSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10 + // + // Frozen: none + // Commit: G, C4 + // Pivot : none + // + // SetHead(7) + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7 + // â””->S1->S2->S3->S4->S5->S6->S7 + // + // Expected head header : C7 + // Expected head fast block: C7 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 10, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + setheadBlock: 7, + expCanonicalBlocks: 7, + expSidechainBlocks: 7, + expFrozen: 0, + expHeadHeader: 7, + expHeadFastBlock: 7, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a short canonical chain and a longer side chain, where +// the fast sync pivot point was already committed to disk and then sethead was +// called. In this case we expect the canonical full chain to be rolled back to +// the committed block. All data above the sethead point should be deleted. In +// between the committed block and the requested head the data can remain as +// "fast sync" data to avoid having to redownload it. The side chain should be +// truncated to the head set. +// +// The side chain could be left to be if the fork point was before the new head +// we are deleting to, but it would be exceedingly hard to detect that case and +// properly handle it, so we'll trade extra work in exchange for simpler code. +func TestShortReorgedFastSyncedSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10 + // + // Frozen: none + // Commit: G, C4 + // Pivot : C4 + // + // SetHead(7) + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7 + // â””->S1->S2->S3->S4->S5->S6->S7 + // + // Expected head header : C7 + // Expected head fast block: C7 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 10, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + setheadBlock: 7, + expCanonicalBlocks: 7, + expSidechainBlocks: 7, + expFrozen: 0, + expHeadHeader: 7, + expHeadFastBlock: 7, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a short canonical chain and a longer side chain, where +// the fast sync pivot point was not yet committed, but sethead was called. In +// this case we expect the chain to detect that it was fast syncing and delete +// everything from the new head, since we can just pick up fast syncing from +// there. +// +// The side chain could be left to be if the fork point was before the new head +// we are deleting to, but it would be exceedingly hard to detect that case and +// properly handle it, so we'll trade extra work in exchange for simpler code. +func TestShortReorgedFastSyncingSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10 + // + // Frozen: none + // Commit: G + // Pivot : C4 + // + // SetHead(7) + // + // ------------------------------ + // + // Expected in leveldb: + // G->C1->C2->C3->C4->C5->C6->C7 + // â””->S1->S2->S3->S4->S5->S6->S7 + // + // Expected head header : C7 + // Expected head fast block: C7 + // Expected head block : G + testSetHead(t, &rewindTest{ + canonicalBlocks: 8, + sidechainBlocks: 10, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + setheadBlock: 7, + expCanonicalBlocks: 7, + expSidechainBlocks: 7, + expFrozen: 0, + expHeadHeader: 7, + expHeadFastBlock: 7, + expHeadBlock: 0, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks where a recent +// block - newer than the ancient limit - was already committed to disk and then +// sethead was called. In this case we expect the full chain to be rolled back +// to the committed block. Everything above the sethead point should be deleted. +// In between the committed block and the requested head the data can remain as +// "fast sync" data to avoid redownloading it. +func TestLongShallowSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // + // Frozen: + // G->C1->C2 + // + // Commit: G, C4 + // Pivot : none + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6 + // + // Expected head header : C6 + // Expected head fast block: C6 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 0, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + setheadBlock: 6, + expCanonicalBlocks: 6, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 6, + expHeadFastBlock: 6, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks where a recent +// block - older than the ancient limit - was already committed to disk and then +// sethead was called. In this case we expect the full chain to be rolled back +// to the committed block. Since the ancient limit was underflown, everything +// needs to be deleted onwards to avoid creating a gap. +func TestLongDeepSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G, C4 + // Pivot : none + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4 + // + // Expected in leveldb: none + // + // Expected head header : C4 + // Expected head fast block: C4 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 0, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + setheadBlock: 6, + expCanonicalBlocks: 4, + expSidechainBlocks: 0, + expFrozen: 5, + expHeadHeader: 4, + expHeadFastBlock: 4, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks where the fast +// sync pivot point - newer than the ancient limit - was already committed, after +// which sethead was called. In this case we expect the full chain to be rolled +// back to the committed block. Everything above the sethead point should be +// deleted. In between the committed block and the requested head the data can +// remain as "fast sync" data to avoid redownloading it. +func TestLongFastSyncedShallowSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // + // Frozen: + // G->C1->C2 + // + // Commit: G, C4 + // Pivot : C4 + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6 + // + // Expected head header : C6 + // Expected head fast block: C6 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 0, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + setheadBlock: 6, + expCanonicalBlocks: 6, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 6, + expHeadFastBlock: 6, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks where the fast +// sync pivot point - older than the ancient limit - was already committed, after +// which sethead was called. In this case we expect the full chain to be rolled +// back to the committed block. Since the ancient limit was underflown, everything +// needs to be deleted onwards to avoid creating a gap. +func TestLongFastSyncedDeepSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G, C4 + // Pivot : C4 + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4 + // + // Expected in leveldb: none + // + // Expected head header : C4 + // Expected head fast block: C4 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 0, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + setheadBlock: 6, + expCanonicalBlocks: 4, + expSidechainBlocks: 0, + expFrozen: 5, + expHeadHeader: 4, + expHeadFastBlock: 4, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks where the fast +// sync pivot point - newer than the ancient limit - was not yet committed, but +// sethead was called. In this case we expect the chain to detect that it was fast +// syncing and delete everything from the new head, since we can just pick up fast +// syncing from there. +func TestLongFastSyncingShallowSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // + // Frozen: + // G->C1->C2 + // + // Commit: G + // Pivot : C4 + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6 + // + // Expected head header : C6 + // Expected head fast block: C6 + // Expected head block : G + testSetHead(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 0, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + setheadBlock: 6, + expCanonicalBlocks: 6, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 6, + expHeadFastBlock: 6, + expHeadBlock: 0, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks where the fast +// sync pivot point - older than the ancient limit - was not yet committed, but +// sethead was called. In this case we expect the chain to detect that it was fast +// syncing and delete everything from the new head, since we can just pick up fast +// syncing from there. +func TestLongFastSyncingDeepSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G + // Pivot : C4 + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4->C5->C6 + // + // Expected in leveldb: none + // + // Expected head header : C6 + // Expected head fast block: C6 + // Expected head block : G + testSetHead(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 0, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + setheadBlock: 6, + expCanonicalBlocks: 6, + expSidechainBlocks: 0, + expFrozen: 7, + expHeadHeader: 6, + expHeadFastBlock: 6, + expHeadBlock: 0, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks and a shorter side +// chain, where a recent block - newer than the ancient limit - was already committed +// to disk and then sethead was called. In this case we expect the canonical full +// chain to be rolled back to the committed block. Everything above the sethead point +// should be deleted. In between the committed block and the requested head the data +// can remain as "fast sync" data to avoid redownloading it. The side chain is nuked +// by the freezer. +func TestLongOldForkedShallowSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // â””->S1->S2->S3 + // + // Frozen: + // G->C1->C2 + // + // Commit: G, C4 + // Pivot : none + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6 + // + // Expected head header : C6 + // Expected head fast block: C6 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 3, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + setheadBlock: 6, + expCanonicalBlocks: 6, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 6, + expHeadFastBlock: 6, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks and a shorter side +// chain, where a recent block - older than the ancient limit - was already committed +// to disk and then sethead was called. In this case we expect the canonical full +// chain to be rolled back to the committed block. Since the ancient limit was +// underflown, everything needs to be deleted onwards to avoid creating a gap. The +// side chain is nuked by the freezer. +func TestLongOldForkedDeepSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // â””->S1->S2->S3 + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G, C4 + // Pivot : none + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4 + // + // Expected in leveldb: none + // + // Expected head header : C4 + // Expected head fast block: C4 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 3, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + setheadBlock: 6, + expCanonicalBlocks: 4, + expSidechainBlocks: 0, + expFrozen: 5, + expHeadHeader: 4, + expHeadFastBlock: 4, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks and a shorter +// side chain, where the fast sync pivot point - newer than the ancient limit - +// was already committed to disk and then sethead was called. In this test scenario +// the side chain is below the committed block. In this case we expect the canonical +// full chain to be rolled back to the committed block. Everything above the +// sethead point should be deleted. In between the committed block and the +// requested head the data can remain as "fast sync" data to avoid redownloading +// it. The side chain is nuked by the freezer. +func TestLongOldForkedFastSyncedShallowSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // â””->S1->S2->S3 + // + // Frozen: + // G->C1->C2 + // + // Commit: G, C4 + // Pivot : C4 + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6 + // + // Expected head header : C6 + // Expected head fast block: C6 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 3, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + setheadBlock: 6, + expCanonicalBlocks: 6, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 6, + expHeadFastBlock: 6, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks and a shorter +// side chain, where the fast sync pivot point - older than the ancient limit - +// was already committed to disk and then sethead was called. In this test scenario +// the side chain is below the committed block. In this case we expect the canonical +// full chain to be rolled back to the committed block. Since the ancient limit was +// underflown, everything needs to be deleted onwards to avoid creating a gap. The +// side chain is nuked by the freezer. +func TestLongOldForkedFastSyncedDeepSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // â””->S1->S2->S3 + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G, C4 + // Pivot : C4 + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4->C5->C6 + // + // Expected in leveldb: none + // + // Expected head header : C6 + // Expected head fast block: C6 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 3, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + setheadBlock: 6, + expCanonicalBlocks: 4, + expSidechainBlocks: 0, + expFrozen: 5, + expHeadHeader: 4, + expHeadFastBlock: 4, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks and a shorter +// side chain, where the fast sync pivot point - newer than the ancient limit - +// was not yet committed, but sethead was called. In this test scenario the side +// chain is below the committed block. In this case we expect the chain to detect +// that it was fast syncing and delete everything from the new head, since we can +// just pick up fast syncing from there. The side chain is completely nuked by the +// freezer. +func TestLongOldForkedFastSyncingShallowSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // â””->S1->S2->S3 + // + // Frozen: + // G->C1->C2 + // + // Commit: G + // Pivot : C4 + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6 + // + // Expected head header : C6 + // Expected head fast block: C6 + // Expected head block : G + testSetHead(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 3, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + setheadBlock: 6, + expCanonicalBlocks: 6, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 6, + expHeadFastBlock: 6, + expHeadBlock: 0, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks and a shorter +// side chain, where the fast sync pivot point - older than the ancient limit - +// was not yet committed, but sethead was called. In this test scenario the side +// chain is below the committed block. In this case we expect the chain to detect +// that it was fast syncing and delete everything from the new head, since we can +// just pick up fast syncing from there. The side chain is completely nuked by the +// freezer. +func TestLongOldForkedFastSyncingDeepSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // â””->S1->S2->S3 + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G + // Pivot : C4 + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4->C5->C6 + // + // Expected in leveldb: none + // + // Expected head header : C6 + // Expected head fast block: C6 + // Expected head block : G + testSetHead(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 3, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + setheadBlock: 6, + expCanonicalBlocks: 6, + expSidechainBlocks: 0, + expFrozen: 7, + expHeadHeader: 6, + expHeadFastBlock: 6, + expHeadBlock: 0, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks and a shorter +// side chain, where a recent block - newer than the ancient limit - was already +// committed to disk and then sethead was called. In this test scenario the side +// chain is above the committed block. In this case the freezer will delete the +// sidechain since it's dangling, reverting to TestLongShallowSetHead. +func TestLongNewerForkedShallowSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12 + // + // Frozen: + // G->C1->C2 + // + // Commit: G, C4 + // Pivot : none + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6 + // + // Expected head header : C6 + // Expected head fast block: C6 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 12, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + setheadBlock: 6, + expCanonicalBlocks: 6, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 6, + expHeadFastBlock: 6, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks and a shorter +// side chain, where a recent block - older than the ancient limit - was already +// committed to disk and then sethead was called. In this test scenario the side +// chain is above the committed block. In this case the freezer will delete the +// sidechain since it's dangling, reverting to TestLongDeepSetHead. +func TestLongNewerForkedDeepSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12 + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G, C4 + // Pivot : none + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4 + // + // Expected in leveldb: none + // + // Expected head header : C4 + // Expected head fast block: C4 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 12, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + setheadBlock: 6, + expCanonicalBlocks: 4, + expSidechainBlocks: 0, + expFrozen: 5, + expHeadHeader: 4, + expHeadFastBlock: 4, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks and a shorter +// side chain, where the fast sync pivot point - newer than the ancient limit - +// was already committed to disk and then sethead was called. In this test scenario +// the side chain is above the committed block. In this case the freezer will delete +// the sidechain since it's dangling, reverting to TestLongFastSyncedShallowSetHead. +func TestLongNewerForkedFastSyncedShallowSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12 + // + // Frozen: + // G->C1->C2 + // + // Commit: G, C4 + // Pivot : C4 + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6 + // + // Expected head header : C6 + // Expected head fast block: C6 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 12, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + setheadBlock: 6, + expCanonicalBlocks: 6, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 6, + expHeadFastBlock: 6, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks and a shorter +// side chain, where the fast sync pivot point - older than the ancient limit - +// was already committed to disk and then sethead was called. In this test scenario +// the side chain is above the committed block. In this case the freezer will delete +// the sidechain since it's dangling, reverting to TestLongFastSyncedDeepSetHead. +func TestLongNewerForkedFastSyncedDeepSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12 + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G, C4 + // Pivot : C4 + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4 + // + // Expected in leveldb: none + // + // Expected head header : C4 + // Expected head fast block: C4 + // Expected head block : C + testSetHead(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 12, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + setheadBlock: 6, + expCanonicalBlocks: 4, + expSidechainBlocks: 0, + expFrozen: 5, + expHeadHeader: 4, + expHeadFastBlock: 4, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks and a shorter +// side chain, where the fast sync pivot point - newer than the ancient limit - +// was not yet committed, but sethead was called. In this test scenario the side +// chain is above the committed block. In this case the freezer will delete the +// sidechain since it's dangling, reverting to TestLongFastSyncinghallowSetHead. +func TestLongNewerForkedFastSyncingShallowSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12 + // + // Frozen: + // G->C1->C2 + // + // Commit: G + // Pivot : C4 + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6 + // + // Expected head header : C6 + // Expected head fast block: C6 + // Expected head block : G + testSetHead(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 12, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + setheadBlock: 6, + expCanonicalBlocks: 6, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 6, + expHeadFastBlock: 6, + expHeadBlock: 0, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks and a shorter +// side chain, where the fast sync pivot point - older than the ancient limit - +// was not yet committed, but sethead was called. In this test scenario the side +// chain is above the committed block. In this case the freezer will delete the +// sidechain since it's dangling, reverting to TestLongFastSyncingDeepSetHead. +func TestLongNewerForkedFastSyncingDeepSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12 + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G + // Pivot : C4 + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4->C5->C6 + // + // Expected in leveldb: none + // + // Expected head header : C6 + // Expected head fast block: C6 + // Expected head block : G + testSetHead(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 12, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + setheadBlock: 6, + expCanonicalBlocks: 6, + expSidechainBlocks: 0, + expFrozen: 7, + expHeadHeader: 6, + expHeadFastBlock: 6, + expHeadBlock: 0, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks and a longer side +// chain, where a recent block - newer than the ancient limit - was already committed +// to disk and then sethead was called. In this case the freezer will delete the +// sidechain since it's dangling, reverting to TestLongShallowSetHead. +func TestLongReorgedShallowSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12->S13->S14->S15->S16->S17->S18->S19->S20->S21->S22->S23->S24->S25->S26 + // + // Frozen: + // G->C1->C2 + // + // Commit: G, C4 + // Pivot : none + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6 + // + // Expected head header : C6 + // Expected head fast block: C6 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 26, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + setheadBlock: 6, + expCanonicalBlocks: 6, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 6, + expHeadFastBlock: 6, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks and a longer side +// chain, where a recent block - older than the ancient limit - was already committed +// to disk and then sethead was called. In this case the freezer will delete the +// sidechain since it's dangling, reverting to TestLongDeepSetHead. +func TestLongReorgedDeepSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12->S13->S14->S15->S16->S17->S18->S19->S20->S21->S22->S23->S24->S25->S26 + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G, C4 + // Pivot : none + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4 + // + // Expected in leveldb: none + // + // Expected head header : C4 + // Expected head fast block: C4 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 26, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: nil, + setheadBlock: 6, + expCanonicalBlocks: 4, + expSidechainBlocks: 0, + expFrozen: 5, + expHeadHeader: 4, + expHeadFastBlock: 4, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks and a longer +// side chain, where the fast sync pivot point - newer than the ancient limit - +// was already committed to disk and then sethead was called. In this case the +// freezer will delete the sidechain since it's dangling, reverting to +// TestLongFastSyncedShallowSetHead. +func TestLongReorgedFastSyncedShallowSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12->S13->S14->S15->S16->S17->S18->S19->S20->S21->S22->S23->S24->S25->S26 + // + // Frozen: + // G->C1->C2 + // + // Commit: G, C4 + // Pivot : C4 + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6 + // + // Expected head header : C6 + // Expected head fast block: C6 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 26, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + setheadBlock: 6, + expCanonicalBlocks: 6, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 6, + expHeadFastBlock: 6, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks and a longer +// side chain, where the fast sync pivot point - older than the ancient limit - +// was already committed to disk and then sethead was called. In this case the +// freezer will delete the sidechain since it's dangling, reverting to +// TestLongFastSyncedDeepSetHead. +func TestLongReorgedFastSyncedDeepSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12->S13->S14->S15->S16->S17->S18->S19->S20->S21->S22->S23->S24->S25->S26 + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G, C4 + // Pivot : C4 + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4 + // + // Expected in leveldb: none + // + // Expected head header : C4 + // Expected head fast block: C4 + // Expected head block : C4 + testSetHead(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 26, + freezeThreshold: 16, + commitBlock: 4, + pivotBlock: uint64ptr(4), + setheadBlock: 6, + expCanonicalBlocks: 4, + expSidechainBlocks: 0, + expFrozen: 5, + expHeadHeader: 4, + expHeadFastBlock: 4, + expHeadBlock: 4, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks and a longer +// side chain, where the fast sync pivot point - newer than the ancient limit - +// was not yet committed, but sethead was called. In this case we expect the +// chain to detect that it was fast syncing and delete everything from the new +// head, since we can just pick up fast syncing from there. The side chain is +// completely nuked by the freezer. +func TestLongReorgedFastSyncingShallowSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12->S13->S14->S15->S16->S17->S18->S19->S20->S21->S22->S23->S24->S25->S26 + // + // Frozen: + // G->C1->C2 + // + // Commit: G + // Pivot : C4 + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2 + // + // Expected in leveldb: + // C2)->C3->C4->C5->C6 + // + // Expected head header : C6 + // Expected head fast block: C6 + // Expected head block : G + testSetHead(t, &rewindTest{ + canonicalBlocks: 18, + sidechainBlocks: 26, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + setheadBlock: 6, + expCanonicalBlocks: 6, + expSidechainBlocks: 0, + expFrozen: 3, + expHeadHeader: 6, + expHeadFastBlock: 6, + expHeadBlock: 0, + }) +} + +// Tests a sethead for a long canonical chain with frozen blocks and a longer +// side chain, where the fast sync pivot point - older than the ancient limit - +// was not yet committed, but sethead was called. In this case we expect the +// chain to detect that it was fast syncing and delete everything from the new +// head, since we can just pick up fast syncing from there. The side chain is +// completely nuked by the freezer. +func TestLongReorgedFastSyncingDeepSetHead(t *testing.T) { + // Chain: + // G->C1->C2->C3->C4->C5->C6->C7->C8->C9->C10->C11->C12->C13->C14->C15->C16->C17->C18->C19->C20->C21->C22->C23->C24 (HEAD) + // â””->S1->S2->S3->S4->S5->S6->S7->S8->S9->S10->S11->S12->S13->S14->S15->S16->S17->S18->S19->S20->S21->S22->S23->S24->S25->S26 + // + // Frozen: + // G->C1->C2->C3->C4->C5->C6->C7->C8 + // + // Commit: G + // Pivot : C4 + // + // SetHead(6) + // + // ------------------------------ + // + // Expected in freezer: + // G->C1->C2->C3->C4->C5->C6 + // + // Expected in leveldb: none + // + // Expected head header : C6 + // Expected head fast block: C6 + // Expected head block : G + testSetHead(t, &rewindTest{ + canonicalBlocks: 24, + sidechainBlocks: 26, + freezeThreshold: 16, + commitBlock: 0, + pivotBlock: uint64ptr(4), + setheadBlock: 6, + expCanonicalBlocks: 6, + expSidechainBlocks: 0, + expFrozen: 7, + expHeadHeader: 6, + expHeadFastBlock: 6, + expHeadBlock: 0, + }) +} + +func testSetHead(t *testing.T, tt *rewindTest) { + // It's hard to follow the test case, visualize the input + //log.Root().SetHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true)))) + //fmt.Println(tt.dump(false)) + + // Create a temporary persistent database + datadir, err := os.MkdirTemp("", "") + if err != nil { + t.Fatalf("Failed to create temporary datadir: %v", err) + } + os.RemoveAll(datadir) + + db, err := rawdb.NewLevelDBDatabaseWithFreezer(datadir, 0, 0, datadir, "", false) + if err != nil { + t.Fatalf("Failed to create persistent database: %v", err) + } + defer db.Close() + + // Initialize a fresh chain + var ( + genesis = (&Genesis{ + Config: params.TestChainConfig, + }).MustCommit(db) + engine = ethash.NewFullFaker() + ) + chain, err := NewBlockChain(db, nil, params.AllEthashProtocolChanges, engine, vm.Config{}, nil) + if err != nil { + t.Fatalf("Failed to create chain: %v", err) + } + // If sidechain blocks are needed, make a light chain and import it + var sideblocks types.Blocks + if tt.sidechainBlocks > 0 { + sideblocks, _ = GenerateChain(params.TestChainConfig, genesis, engine, rawdb.NewMemoryDatabase(), tt.sidechainBlocks, func(i int, b *BlockGen) { + b.SetCoinbase(common.Address{0x01}) + }) + if _, err := chain.InsertChain(sideblocks); err != nil { + t.Fatalf("Failed to import side chain: %v", err) + } + } + canonblocks, _ := GenerateChain(params.TestChainConfig, genesis, engine, rawdb.NewMemoryDatabase(), tt.canonicalBlocks, func(i int, b *BlockGen) { + b.SetCoinbase(common.Address{0x02}) + b.SetDifficulty(big.NewInt(1000000)) + }) + if _, err := chain.InsertChain(canonblocks[:tt.commitBlock]); err != nil { + t.Fatalf("Failed to import canonical chain start: %v", err) + } + if tt.commitBlock > 0 { + chain.stateCache.TrieDB().Commit(canonblocks[tt.commitBlock-1].Root(), true) + } + if _, err := chain.InsertChain(canonblocks[tt.commitBlock:]); err != nil { + t.Fatalf("Failed to import canonical chain tail: %v", err) + } + // Manually dereference anything not committed to not have to work with 128+ tries + for _, block := range sideblocks { + chain.stateCache.TrieDB().Dereference(block.Root()) + } + for _, block := range canonblocks { + chain.stateCache.TrieDB().Dereference(block.Root()) + } + // Force run a freeze cycle + type freezer interface { + Freeze(threshold uint64) error + Ancients() (uint64, error) + } + db.(freezer).Freeze(tt.freezeThreshold) + + // Set the simulated pivot block + if tt.pivotBlock != nil { + rawdb.WriteLastPivotNumber(db, *tt.pivotBlock) + } + // Set the head of the chain back to the requested number + chain.SetHead(tt.setheadBlock) + + // Iterate over all the remaining blocks and ensure there are no gaps + verifyNoGaps(t, chain, true, canonblocks) + verifyNoGaps(t, chain, false, sideblocks) + verifyCutoff(t, chain, true, canonblocks, tt.expCanonicalBlocks) + verifyCutoff(t, chain, false, sideblocks, tt.expSidechainBlocks) + + if head := chain.CurrentHeader(); head.Number.Uint64() != tt.expHeadHeader { + t.Errorf("Head header mismatch: have %d, want %d", head.Number, tt.expHeadHeader) + } + if head := chain.CurrentFastBlock(); head.NumberU64() != tt.expHeadFastBlock { + t.Errorf("Head fast block mismatch: have %d, want %d", head.NumberU64(), tt.expHeadFastBlock) + } + if head := chain.CurrentBlock(); head.NumberU64() != tt.expHeadBlock { + t.Errorf("Head block mismatch: have %d, want %d", head.NumberU64(), tt.expHeadBlock) + } + if frozen, err := db.(freezer).Ancients(); err != nil { + t.Errorf("Failed to retrieve ancient count: %v\n", err) + } else if int(frozen) != tt.expFrozen { + t.Errorf("Frozen block count mismatch: have %d, want %d", frozen, tt.expFrozen) + } +} + +// verifyNoGaps checks that there are no gaps after the initial set of blocks in +// the database and errors if found. +func verifyNoGaps(t *testing.T, chain *BlockChain, canonical bool, inserted types.Blocks) { + t.Helper() + + var end uint64 + for i := uint64(0); i <= uint64(len(inserted)); i++ { + header := chain.GetHeaderByNumber(i) + if header == nil && end == 0 { + end = i + } + if header != nil && end > 0 { + if canonical { + t.Errorf("Canonical header gap between #%d-#%d", end, i-1) + } else { + t.Errorf("Sidechain header gap between #%d-#%d", end, i-1) + } + end = 0 // Reset for further gap detection + } + } + end = 0 + for i := uint64(0); i <= uint64(len(inserted)); i++ { + block := chain.GetBlockByNumber(i) + if block == nil && end == 0 { + end = i + } + if block != nil && end > 0 { + if canonical { + t.Errorf("Canonical block gap between #%d-#%d", end, i-1) + } else { + t.Errorf("Sidechain block gap between #%d-#%d", end, i-1) + } + end = 0 // Reset for further gap detection + } + } + end = 0 + for i := uint64(1); i <= uint64(len(inserted)); i++ { + receipts := chain.GetReceiptsByHash(inserted[i-1].Hash()) + if receipts == nil && end == 0 { + end = i + } + if receipts != nil && end > 0 { + if canonical { + t.Errorf("Canonical receipt gap between #%d-#%d", end, i-1) + } else { + t.Errorf("Sidechain receipt gap between #%d-#%d", end, i-1) + } + end = 0 // Reset for further gap detection + } + } +} + +// verifyCutoff checks that there are no chain data available in the chain after +// the specified limit, but that it is available before. +func verifyCutoff(t *testing.T, chain *BlockChain, canonical bool, inserted types.Blocks, head int) { + t.Helper() + + for i := 1; i <= len(inserted); i++ { + if i <= head { + if header := chain.GetHeader(inserted[i-1].Hash(), uint64(i)); header == nil { + if canonical { + t.Errorf("Canonical header #%2d [%x...] missing before cap %d", inserted[i-1].Number(), inserted[i-1].Hash().Bytes()[:3], head) + } else { + t.Errorf("Sidechain header #%2d [%x...] missing before cap %d", inserted[i-1].Number(), inserted[i-1].Hash().Bytes()[:3], head) + } + } + if block := chain.GetBlock(inserted[i-1].Hash(), uint64(i)); block == nil { + if canonical { + t.Errorf("Canonical block #%2d [%x...] missing before cap %d", inserted[i-1].Number(), inserted[i-1].Hash().Bytes()[:3], head) + } else { + t.Errorf("Sidechain block #%2d [%x...] missing before cap %d", inserted[i-1].Number(), inserted[i-1].Hash().Bytes()[:3], head) + } + } + if receipts := chain.GetReceiptsByHash(inserted[i-1].Hash()); receipts == nil { + if canonical { + t.Errorf("Canonical receipts #%2d [%x...] missing before cap %d", inserted[i-1].Number(), inserted[i-1].Hash().Bytes()[:3], head) + } else { + t.Errorf("Sidechain receipts #%2d [%x...] missing before cap %d", inserted[i-1].Number(), inserted[i-1].Hash().Bytes()[:3], head) + } + } + } else { + if header := chain.GetHeader(inserted[i-1].Hash(), uint64(i)); header != nil { + if canonical { + t.Errorf("Canonical header #%2d [%x...] present after cap %d", inserted[i-1].Number(), inserted[i-1].Hash().Bytes()[:3], head) + } else { + t.Errorf("Sidechain header #%2d [%x...] present after cap %d", inserted[i-1].Number(), inserted[i-1].Hash().Bytes()[:3], head) + } + } + if block := chain.GetBlock(inserted[i-1].Hash(), uint64(i)); block != nil { + if canonical { + t.Errorf("Canonical block #%2d [%x...] present after cap %d", inserted[i-1].Number(), inserted[i-1].Hash().Bytes()[:3], head) + } else { + t.Errorf("Sidechain block #%2d [%x...] present after cap %d", inserted[i-1].Number(), inserted[i-1].Hash().Bytes()[:3], head) + } + } + if receipts := chain.GetReceiptsByHash(inserted[i-1].Hash()); receipts != nil { + if canonical { + t.Errorf("Canonical receipts #%2d [%x...] present after cap %d", inserted[i-1].Number(), inserted[i-1].Hash().Bytes()[:3], head) + } else { + t.Errorf("Sidechain receipts #%2d [%x...] present after cap %d", inserted[i-1].Number(), inserted[i-1].Hash().Bytes()[:3], head) + } + } + } + } +} + +// uint64ptr is a weird helper to allow 1-line constant pointer creation. +func uint64ptr(n uint64) *uint64 { + return &n +} diff --git a/core/blockchain_test.go b/core/blockchain_test.go index ecd3def6762..e27d68835ec 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -19,8 +19,10 @@ package core import ( "errors" "fmt" + "io/ioutil" "math/big" "math/rand" + "os" "sync" "testing" "time" @@ -53,7 +55,7 @@ func newCanonical(engine consensus.Engine, n int, full bool) (ethdb.Database, *B genesis := gspec.MustCommit(db) // Initialize a fresh chain with only a genesis block - blockchain, _ := NewBlockChain(db, nil, params.AllEthashProtocolChanges, engine, vm.Config{}) + blockchain, _ := NewBlockChain(db, nil, params.AllEthashProtocolChanges, engine, vm.Config{}, nil) // Create and inject the requested chain if n == 0 { @@ -517,7 +519,7 @@ func testReorgBadHashes(t *testing.T, full bool) { blockchain.Stop() // Create a new BlockChain and check that it rolled back the state. - ncm, err := NewBlockChain(blockchain.db, nil, blockchain.chainConfig, ethash.NewFaker(), vm.Config{}) + ncm, err := NewBlockChain(blockchain.db, nil, blockchain.chainConfig, ethash.NewFaker(), vm.Config{}, nil) if err != nil { t.Fatalf("failed to create new chain manager: %v", err) } @@ -620,7 +622,7 @@ func TestFastVsFullChains(t *testing.T) { // Import the chain as an archive node for the comparison baseline archiveDb := rawdb.NewMemoryDatabase() gspec.MustCommit(archiveDb) - archive, _ := NewBlockChain(archiveDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}) + archive, _ := NewBlockChain(archiveDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) defer archive.Stop() if n, err := archive.InsertChain(blocks); err != nil { @@ -629,7 +631,7 @@ func TestFastVsFullChains(t *testing.T) { // Fast import the chain as a non-archive node to test fastDb := rawdb.NewMemoryDatabase() gspec.MustCommit(fastDb) - fast, _ := NewBlockChain(fastDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}) + fast, _ := NewBlockChain(fastDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) defer fast.Stop() headers := make([]*types.Header, len(blocks)) @@ -639,34 +641,81 @@ func TestFastVsFullChains(t *testing.T) { if n, err := fast.InsertHeaderChain(headers, 1); err != nil { t.Fatalf("failed to insert header %d: %v", n, err) } - if n, err := fast.InsertReceiptChain(blocks, receipts); err != nil { + if n, err := fast.InsertReceiptChain(blocks, receipts, 0); err != nil { t.Fatalf("failed to insert receipt %d: %v", n, err) } + // Freezer style fast import the chain. + frdir, err := os.MkdirTemp("", "") + if err != nil { + t.Fatalf("failed to create temp freezer dir: %v", err) + } + defer os.Remove(frdir) + ancientDb, err := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), frdir, "", false) + if err != nil { + t.Fatalf("failed to create temp freezer db: %v", err) + } + gspec.MustCommit(ancientDb) + ancient, _ := NewBlockChain(ancientDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) + defer ancient.Stop() + + if n, err := ancient.InsertHeaderChain(headers, 1); err != nil { + t.Fatalf("failed to insert header %d: %v", n, err) + } + if n, err := ancient.InsertReceiptChain(blocks, receipts, uint64(len(blocks)/2)); err != nil { + t.Fatalf("failed to insert receipt %d: %v", n, err) + } + // Iterate over all chain data components, and cross reference for i := 0; i < len(blocks); i++ { num, hash := blocks[i].NumberU64(), blocks[i].Hash() if ftd, atd := fast.GetTdByHash(hash), archive.GetTdByHash(hash); ftd.Cmp(atd) != 0 { - t.Errorf("block #%d [%x]: td mismatch: have %v, want %v", num, hash, ftd, atd) + t.Errorf("block #%d [%x]: td mismatch: fastdb %v, archivedb %v", num, hash, ftd, atd) + } + if antd, artd := ancient.GetTdByHash(hash), archive.GetTdByHash(hash); antd.Cmp(artd) != 0 { + t.Errorf("block #%d [%x]: td mismatch: ancientdb %v, archivedb %v", num, hash, antd, artd) } if fheader, aheader := fast.GetHeaderByHash(hash), archive.GetHeaderByHash(hash); fheader.Hash() != aheader.Hash() { - t.Errorf("block #%d [%x]: header mismatch: have %v, want %v", num, hash, fheader, aheader) + t.Errorf("block #%d [%x]: header mismatch: fastdb %v, archivedb %v", num, hash, fheader, aheader) + } + if anheader, arheader := ancient.GetHeaderByHash(hash), archive.GetHeaderByHash(hash); anheader.Hash() != arheader.Hash() { + t.Errorf("block #%d [%x]: header mismatch: ancientdb %v, archivedb %v", num, hash, anheader, arheader) + } + if fblock, arblock, anblock := fast.GetBlockByHash(hash), archive.GetBlockByHash(hash), ancient.GetBlockByHash(hash); fblock.Hash() != arblock.Hash() || anblock.Hash() != arblock.Hash() { + t.Errorf("block #%d [%x]: block mismatch: fastdb %v, ancientdb %v, archivedb %v", num, hash, fblock, anblock, arblock) + } else if types.DeriveSha(fblock.Transactions(), trie.NewStackTrie(nil)) != types.DeriveSha(arblock.Transactions(), trie.NewStackTrie(nil)) || types.DeriveSha(anblock.Transactions(), trie.NewStackTrie(nil)) != types.DeriveSha(arblock.Transactions(), trie.NewStackTrie(nil)) { + t.Errorf("block #%d [%x]: transactions mismatch: fastdb %v, ancientdb %v, archivedb %v", num, hash, fblock.Transactions(), anblock.Transactions(), arblock.Transactions()) + } else if types.CalcUncleHash(fblock.Uncles()) != types.CalcUncleHash(arblock.Uncles()) || types.CalcUncleHash(anblock.Uncles()) != types.CalcUncleHash(arblock.Uncles()) { + t.Errorf("block #%d [%x]: uncles mismatch: fastdb %v, ancientdb %v, archivedb %v", num, hash, fblock.Uncles(), anblock, arblock.Uncles()) } - if fblock, ablock := fast.GetBlockByHash(hash), archive.GetBlockByHash(hash); fblock.Hash() != ablock.Hash() { - t.Errorf("block #%d [%x]: block mismatch: have %v, want %v", num, hash, fblock, ablock) - } else if types.DeriveSha(fblock.Transactions(), trie.NewStackTrie(nil)) != types.DeriveSha(ablock.Transactions(), trie.NewStackTrie(nil)) { - t.Errorf("block #%d [%x]: transactions mismatch: have %v, want %v", num, hash, fblock.Transactions(), ablock.Transactions()) - } else if types.CalcUncleHash(fblock.Uncles()) != types.CalcUncleHash(ablock.Uncles()) { - t.Errorf("block #%d [%x]: uncles mismatch: have %v, want %v", num, hash, fblock.Uncles(), ablock.Uncles()) + + // Check receipts. + freceipts := rawdb.ReadReceipts(fastDb, hash, num, fast.Config()) + anreceipts := rawdb.ReadReceipts(ancientDb, hash, num, fast.Config()) + areceipts := rawdb.ReadReceipts(archiveDb, hash, num, fast.Config()) + if types.DeriveSha(freceipts, trie.NewStackTrie(nil)) != types.DeriveSha(areceipts, trie.NewStackTrie(nil)) { + t.Errorf("block #%d [%x]: receipts mismatch: fastdb %v, ancientdb %v, archivedb %v", num, hash, freceipts, anreceipts, areceipts) + } + + // Check that hash-to-number mappings are present in all databases. + if m := rawdb.ReadHeaderNumber(fastDb, hash); m == nil || *m != num { + t.Errorf("block #%d [%x]: wrong hash-to-number mapping in fastdb: %v", num, hash, m) + } + if m := rawdb.ReadHeaderNumber(ancientDb, hash); m == nil || *m != num { + t.Errorf("block #%d [%x]: wrong hash-to-number mapping in ancientdb: %v", num, hash, m) } - if freceipts, areceipts := rawdb.ReadReceipts(fastDb, hash, *rawdb.ReadHeaderNumber(fastDb, hash), fast.Config()), rawdb.ReadReceipts(archiveDb, hash, *rawdb.ReadHeaderNumber(archiveDb, hash), fast.Config()); types.DeriveSha(freceipts, trie.NewStackTrie(nil)) != types.DeriveSha(areceipts, trie.NewStackTrie(nil)) { - t.Errorf("block #%d [%x]: receipts mismatch: have %v, want %v", num, hash, freceipts, areceipts) + if m := rawdb.ReadHeaderNumber(archiveDb, hash); m == nil || *m != num { + t.Errorf("block #%d [%x]: wrong hash-to-number mapping in archivedb: %v", num, hash, m) } } + // Check that the canonical chains are the same between the databases for i := 0; i < len(blocks)+1; i++ { if fhash, ahash := rawdb.ReadCanonicalHash(fastDb, uint64(i)), rawdb.ReadCanonicalHash(archiveDb, uint64(i)); fhash != ahash { - t.Errorf("block #%d: canonical hash mismatch: have %v, want %v", i, fhash, ahash) + t.Errorf("block #%d: canonical hash mismatch: fastdb %v, archivedb %v", i, fhash, ahash) + } + if anhash, arhash := rawdb.ReadCanonicalHash(ancientDb, uint64(i)), rawdb.ReadCanonicalHash(archiveDb, uint64(i)); anhash != arhash { + t.Errorf("block #%d: canonical hash mismatch: ancientdb %v, archivedb %v", i, anhash, arhash) } } } @@ -690,13 +739,27 @@ func TestLightVsFastVsFullChainHeads(t *testing.T) { height := uint64(1024) blocks, receipts := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), gendb, int(height), nil) - // Configure a subchain to roll back - remove := []common.Hash{} - for _, block := range blocks[height/2:] { - remove = append(remove, block.Hash()) + // makeDb creates a db instance for testing. + makeDb := func() (ethdb.Database, func()) { + dir, err := os.MkdirTemp("", "") + if err != nil { + t.Fatalf("failed to create temp freezer dir: %v", err) + } + defer os.Remove(dir) + db, err := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), dir, "", false) + if err != nil { + t.Fatalf("failed to create temp freezer db: %v", err) + } + gspec.MustCommit(db) + return db, func() { os.RemoveAll(dir) } } + // Configure a subchain to roll back + remove := blocks[height/2].NumberU64() + // Create a small assertion method to check the three heads assert := func(t *testing.T, kind string, chain *BlockChain, header uint64, fast uint64, block uint64) { + t.Helper() + if num := chain.CurrentBlock().NumberU64(); num != block { t.Errorf("%s head block mismatch: have #%v, want #%v", kind, num, block) } @@ -708,23 +771,26 @@ func TestLightVsFastVsFullChainHeads(t *testing.T) { } } // Import the chain as an archive node and ensure all pointers are updated - archiveDb := rawdb.NewMemoryDatabase() - gspec.MustCommit(archiveDb) + archiveDb, delfn := makeDb() + defer delfn() + + archiveCaching := *defaultCacheConfig + archiveCaching.Disabled = true - archive, _ := NewBlockChain(archiveDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}) + archive, _ := NewBlockChain(archiveDb, &archiveCaching, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) if n, err := archive.InsertChain(blocks); err != nil { t.Fatalf("failed to process block %d: %v", n, err) } defer archive.Stop() assert(t, "archive", archive, height, height, height) - archive.Rollback(remove) + archive.SetHead(remove - 1) assert(t, "archive", archive, height/2, height/2, height/2) // Import the chain as a non-archive node and ensure all pointers are updated - fastDb := rawdb.NewMemoryDatabase() - gspec.MustCommit(fastDb) - fast, _ := NewBlockChain(fastDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}) + fastDb, delfn := makeDb() + defer delfn() + fast, _ := NewBlockChain(fastDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) defer fast.Stop() headers := make([]*types.Header, len(blocks)) @@ -734,25 +800,44 @@ func TestLightVsFastVsFullChainHeads(t *testing.T) { if n, err := fast.InsertHeaderChain(headers, 1); err != nil { t.Fatalf("failed to insert header %d: %v", n, err) } - if n, err := fast.InsertReceiptChain(blocks, receipts); err != nil { + if n, err := fast.InsertReceiptChain(blocks, receipts, 0); err != nil { t.Fatalf("failed to insert receipt %d: %v", n, err) } assert(t, "fast", fast, height, height, 0) - fast.Rollback(remove) + fast.SetHead(remove - 1) assert(t, "fast", fast, height/2, height/2, 0) // Import the chain as a light node and ensure all pointers are updated - lightDb := rawdb.NewMemoryDatabase() - gspec.MustCommit(lightDb) + ancientDb, delfn := makeDb() + defer delfn() + ancient, _ := NewBlockChain(ancientDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) + defer ancient.Stop() + + if n, err := ancient.InsertHeaderChain(headers, 1); err != nil { + t.Fatalf("failed to insert header %d: %v", n, err) + } + if n, err := ancient.InsertReceiptChain(blocks, receipts, uint64(3*len(blocks)/4)); err != nil { + t.Fatalf("failed to insert receipt %d: %v", n, err) + } + assert(t, "ancient", ancient, height, height, 0) + ancient.SetHead(remove - 1) + assert(t, "ancient", ancient, 0, 0, 0) + + if frozen, err := ancientDb.Ancients(); err != nil || frozen != 1 { + t.Fatalf("failed to truncate ancient store, want %v, have %v", 1, frozen) + } - light, _ := NewBlockChain(lightDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}) + // Import the chain as a light node and ensure all pointers are updated + lightDb, delfn := makeDb() + defer delfn() + light, _ := NewBlockChain(lightDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) if n, err := light.InsertHeaderChain(headers, 1); err != nil { t.Fatalf("failed to insert header %d: %v", n, err) } defer light.Stop() assert(t, "light", light, height, 0, 0) - light.Rollback(remove) + light.SetHead(remove - 1) assert(t, "light", light, height/2, 0, 0) } @@ -814,7 +899,7 @@ func TestChainTxReorgs(t *testing.T) { } }) // Import the chain. This runs all block validation rules. - blockchain, _ := NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}) + blockchain, _ := NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) if i, err := blockchain.InsertChain(chain); err != nil { t.Fatalf("failed to insert original chain[%d]: %v", i, err) } @@ -885,7 +970,7 @@ func TestLogReorgs(t *testing.T) { signer = types.LatestSigner(gspec.Config) ) - blockchain, _ := NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}) + blockchain, _ := NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) defer blockchain.Stop() rmLogsCh := make(chan RemovedLogsEvent) @@ -919,88 +1004,90 @@ func TestLogReorgs(t *testing.T) { } } -//func TestReorgSideEvent(t *testing.T) { -// var ( -// db, _ = rawdb.NewMemoryDatabase() -// key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") -// addr1 = crypto.PubkeyToAddress(key1.PublicKey) -// gspec = &Genesis{ -// Config: params.TestChainConfig, -// Alloc: GenesisAlloc{addr1: {Balance: big.NewInt(10000000000000000)}}, -// } -// genesis = gspec.MustCommit(db) -// signer = types.LatestSigner(gspec.Config) -// ) -// -// blockchain, _ := NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}) -// defer blockchain.Stop() -// -// chain, _ := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), db, 3, func(i int, gen *BlockGen) {}) -// if _, err := blockchain.InsertChain(chain); err != nil { -// t.Fatalf("failed to insert chain: %v", err) -// } -// -// replacementBlocks, _ := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), db, 4, func(i int, gen *BlockGen) { -// tx, err := types.SignTx(types.NewContractCreation(gen.TxNonce(addr1), new(big.Int), 1000000, gen.header.BaseFee, nil), signer, key1) -// if i == 2 { -// gen.OffsetTime(-9) -// } -// if err != nil { -// t.Fatalf("failed to create tx: %v", err) -// } -// gen.AddTx(tx) -// }) -// chainSideCh := make(chan ChainSideEvent, 64) -// blockchain.SubscribeChainSideEvent(chainSideCh) -// if _, err := blockchain.InsertChain(replacementBlocks); err != nil { -// t.Fatalf("failed to insert chain: %v", err) -// } -// -// // first two block of the secondary chain are for a brief moment considered -// // side chains because up to that point the first one is considered the -// // heavier chain. -// expectedSideHashes := map[common.Hash]bool{ -// replacementBlocks[0].Hash(): true, -// replacementBlocks[1].Hash(): true, -// chain[0].Hash(): true, -// chain[1].Hash(): true, -// chain[2].Hash(): true, -// } -// -// i := 0 -// -// const timeoutDura = 10 * time.Second -// timeout := time.NewTimer(timeoutDura) -//done: -// for { -// select { -// case ev := <-chainSideCh: -// block := ev.Block -// if _, ok := expectedSideHashes[block.Hash()]; !ok { -// t.Errorf("%d: didn't expect %x to be in side chain", i, block.Hash()) -// } -// i++ -// -// if i == len(expectedSideHashes) { -// timeout.Stop() -// -// break done -// } -// timeout.Reset(timeoutDura) -// -// case <-timeout.C: -// t.Fatal("Timeout. Possibly not all blocks were triggered for sideevent") -// } -// } -// -// // make sure no more events are fired -// select { -// case e := <-chainSideCh: -// t.Errorf("unexpected event fired: %v", e) -// case <-time.After(250 * time.Millisecond): -// } -// -//} +func TestSideLogRebirth(t *testing.T) { + var ( + key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + addr1 = crypto.PubkeyToAddress(key1.PublicKey) + db = rawdb.NewMemoryDatabase() + + // this code generates a log + code = common.Hex2Bytes("60606040525b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405180905060405180910390a15b600a8060416000396000f360606040526008565b00") + gspec = &Genesis{Config: params.TestChainConfig, Alloc: GenesisAlloc{addr1: {Balance: big.NewInt(10000000000000)}}} + genesis = gspec.MustCommit(db) + signer = types.NewEIP155Signer(gspec.Config.ChainId) + newLogCh = make(chan bool) + ) + + // listenNewLog checks whether the received logs number is equal with expected. + listenNewLog := func(sink chan []*types.Log, expect int) { + cnt := 0 + for { + select { + case logs := <-sink: + cnt += len(logs) + case <-time.NewTimer(5 * time.Second).C: + // new logs timeout + newLogCh <- false + return + } + if cnt == expect { + break + } else if cnt > expect { + // redundant logs received + newLogCh <- false + return + } + } + select { + case <-sink: + // redundant logs received + newLogCh <- false + case <-time.NewTimer(100 * time.Millisecond).C: + newLogCh <- true + } + } + + blockchain, _ := NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) + defer blockchain.Stop() + + logsCh := make(chan []*types.Log) + blockchain.SubscribeLogsEvent(logsCh) + + chain, _ := GenerateChain(params.TestChainConfig, genesis, ethash.NewFaker(), db, 2, func(i int, gen *BlockGen) { + if i == 1 { + // Higher block difficulty + gen.OffsetTime(-9) + } + }) + if _, err := blockchain.InsertChain(chain); err != nil { + t.Fatalf("failed to insert forked chain: %v", err) + } + + // Generate side chain with lower difficulty + sideChain, _ := GenerateChain(params.TestChainConfig, genesis, ethash.NewFaker(), db, 2, func(i int, gen *BlockGen) { + if i == 1 { + tx, err := types.SignTx(types.NewContractCreation(gen.TxNonce(addr1), new(big.Int), 1000000, new(big.Int), code), signer, key1) + if err != nil { + t.Fatalf("failed to create tx: %v", err) + } + gen.AddTx(tx) + } + }) + if _, err := blockchain.InsertChain(sideChain); err != nil { + t.Fatalf("failed to insert forked chain: %v", err) + } + + // Generate a new block based on side chain + newBlocks, _ := GenerateChain(params.TestChainConfig, sideChain[len(sideChain)-1], ethash.NewFaker(), db, 1, func(i int, gen *BlockGen) {}) + go listenNewLog(logsCh, 1) + if _, err := blockchain.InsertChain(newBlocks); err != nil { + t.Fatalf("failed to insert forked chain: %v", err) + } + // Rebirth logs should omit a newLogEvent + if !<-newLogCh { + t.Fatalf("failed to receive new log event") + } +} // Tests if the canonical block can be fetched from the database during chain insertion. func TestCanonicalBlockRetrieval(t *testing.T) { @@ -1064,7 +1151,7 @@ func TestEIP155Transition(t *testing.T) { genesis = gspec.MustCommit(db) ) - blockchain, _ := NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}) + blockchain, _ := NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) defer blockchain.Stop() blocks, _ := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), db, 4, func(i int, block *BlockGen) { @@ -1171,7 +1258,7 @@ func TestEIP161AccountRemoval(t *testing.T) { } genesis = gspec.MustCommit(db) ) - blockchain, _ := NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}) + blockchain, _ := NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) defer blockchain.Stop() blocks, _ := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), db, 3, func(i int, block *BlockGen) { @@ -1250,7 +1337,7 @@ func TestBlockchainHeaderchainReorgConsistency(t *testing.T) { diskdb := rawdb.NewMemoryDatabase() gspec.MustCommit(diskdb) - chain, err := NewBlockChain(diskdb, nil, params.TestChainConfig, engine, vm.Config{}) + chain, err := NewBlockChain(diskdb, nil, params.TestChainConfig, engine, vm.Config{}, nil) if err != nil { t.Fatalf("failed to create tester chain: %v", err) } @@ -1300,7 +1387,7 @@ func TestTrieForkGC(t *testing.T) { BaseFee: big.NewInt(params.InitialBaseFee), }).MustCommit(diskdb) - chain, err := NewBlockChain(diskdb, nil, params.TestChainConfig, engine, vm.Config{}) + chain, err := NewBlockChain(diskdb, nil, params.TestChainConfig, engine, vm.Config{}, nil) if err != nil { t.Fatalf("failed to create tester chain: %v", err) } @@ -1343,7 +1430,7 @@ func TestLargeReorgTrieGC(t *testing.T) { diskdb := rawdb.NewMemoryDatabase() gspec.MustCommit(diskdb) - chain, err := NewBlockChain(diskdb, nil, params.TestChainConfig, engine, vm.Config{}) + chain, err := NewBlockChain(diskdb, nil, params.TestChainConfig, engine, vm.Config{}, nil) if err != nil { t.Fatalf("failed to create tester chain: %v", err) } @@ -1379,16 +1466,604 @@ func TestLargeReorgTrieGC(t *testing.T) { } } -/* - Collection test for BlocksHashCache - cases - 1. When init new chain - 2. when insertChain - 3. when insertFork - 4. When adding new block by mining - 5. When adding new block by syncing with other nodes +func TestBlockchainRecovery(t *testing.T) { + // Configure and generate a sample block chain + var ( + gendb = rawdb.NewMemoryDatabase() + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + address = crypto.PubkeyToAddress(key.PublicKey) + funds = big.NewInt(1000000000) + gspec = &Genesis{Config: params.TestChainConfig, Alloc: GenesisAlloc{address: {Balance: funds}}} + genesis = gspec.MustCommit(gendb) + ) + height := uint64(1024) + blocks, receipts := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), gendb, int(height), nil) + + // Import the chain as a ancient-first node and ensure all pointers are updated + frdir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("failed to create temp freezer dir: %v", err) + } + defer os.Remove(frdir) + + ancientDb, err := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), frdir, "", false) + if err != nil { + t.Fatalf("failed to create temp freezer db: %v", err) + } + gspec.MustCommit(ancientDb) + ancient, _ := NewBlockChain(ancientDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) + + headers := make([]*types.Header, len(blocks)) + for i, block := range blocks { + headers[i] = block.Header() + } + if n, err := ancient.InsertHeaderChain(headers, 1); err != nil { + t.Fatalf("failed to insert header %d: %v", n, err) + } + if n, err := ancient.InsertReceiptChain(blocks, receipts, uint64(3*len(blocks)/4)); err != nil { + t.Fatalf("failed to insert receipt %d: %v", n, err) + } + rawdb.WriteLastPivotNumber(ancientDb, blocks[len(blocks)-1].NumberU64()) // Force fast sync behavior + ancient.Stop() + + // Destroy head fast block manually + midBlock := blocks[len(blocks)/2] + rawdb.WriteHeadFastBlockHash(ancientDb, midBlock.Hash()) + + // Reopen broken blockchain again + ancient, _ = NewBlockChain(ancientDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) + defer ancient.Stop() + if num := ancient.CurrentBlock().NumberU64(); num != 0 { + t.Errorf("head block mismatch: have #%v, want #%v", num, 0) + } + if num := ancient.CurrentFastBlock().NumberU64(); num != midBlock.NumberU64() { + t.Errorf("head fast-block mismatch: have #%v, want #%v", num, midBlock.NumberU64()) + } + if num := ancient.CurrentHeader().Number.Uint64(); num != midBlock.NumberU64() { + t.Errorf("head header mismatch: have #%v, want #%v", num, midBlock.NumberU64()) + } +} + +// This test checks that InsertReceiptChain will roll back correctly when attempting to insert a side chain. +func TestInsertReceiptChainRollback(t *testing.T) { + // TODO(daniel): make TestInsertReceiptChainRollback pass test + t.Skip() + // Generate forked chain. The returned BlockChain object is used to process the side chain blocks. + tmpChain, sideblocks, canonblocks, err := getLongAndShortChains() + if err != nil { + t.Fatal(err) + } + defer tmpChain.Stop() + // Get the side chain receipts. + if _, err := tmpChain.InsertChain(sideblocks); err != nil { + t.Fatal("processing side chain failed:", err) + } + t.Log("sidechain head:", tmpChain.CurrentBlock().Number(), tmpChain.CurrentBlock().Hash()) + sidechainReceipts := make([]types.Receipts, len(sideblocks)) + for i, block := range sideblocks { + sidechainReceipts[i] = tmpChain.GetReceiptsByHash(block.Hash()) + } + // Get the canon chain receipts. + if _, err := tmpChain.InsertChain(canonblocks); err != nil { + t.Fatal("processing canon chain failed:", err) + } + t.Log("canon head:", tmpChain.CurrentBlock().Number(), tmpChain.CurrentBlock().Hash()) + canonReceipts := make([]types.Receipts, len(canonblocks)) + for i, block := range canonblocks { + canonReceipts[i] = tmpChain.GetReceiptsByHash(block.Hash()) + } + + // Set up a BlockChain that uses the ancient store. + frdir, err := os.MkdirTemp("", "") + if err != nil { + t.Fatalf("failed to create temp freezer dir: %v", err) + } + defer os.Remove(frdir) + ancientDb, err := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), frdir, "", false) + if err != nil { + t.Fatalf("failed to create temp freezer db: %v", err) + } + gspec := Genesis{Config: params.AllEthashProtocolChanges} + gspec.MustCommit(ancientDb) + ancientChain, _ := NewBlockChain(ancientDb, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) + defer ancientChain.Stop() + + // Import the canonical header chain. + canonHeaders := make([]*types.Header, len(canonblocks)) + for i, block := range canonblocks { + canonHeaders[i] = block.Header() + } + if _, err = ancientChain.InsertHeaderChain(canonHeaders, 1); err != nil { + t.Fatal("can't import canon headers:", err) + } + + // Try to insert blocks/receipts of the side chain. + _, err = ancientChain.InsertReceiptChain(sideblocks, sidechainReceipts, uint64(len(sideblocks))) + if err == nil { + t.Fatal("expected error from InsertReceiptChain.") + } + if ancientChain.CurrentFastBlock().NumberU64() != 0 { + t.Fatalf("failed to rollback ancient data, want %d, have %d", 0, ancientChain.CurrentFastBlock().NumberU64()) + } + if frozen, err := ancientChain.db.Ancients(); err != nil || frozen != 1 { + t.Fatalf("failed to truncate ancient data, frozen index is %d", frozen) + } + + // Insert blocks/receipts of the canonical chain. + _, err = ancientChain.InsertReceiptChain(canonblocks, canonReceipts, uint64(len(canonblocks))) + if err != nil { + t.Fatalf("can't import canon chain receipts: %v", err) + } + if ancientChain.CurrentFastBlock().NumberU64() != canonblocks[len(canonblocks)-1].NumberU64() { + t.Fatalf("failed to insert ancient recept chain after rollback") + } + if frozen, _ := ancientChain.db.Ancients(); frozen != uint64(len(canonblocks))+1 { + t.Fatalf("wrong ancients count %d", frozen) + } +} + +// Tests that importing a very large side fork, which is larger than the canon chain, +// but where the difficulty per block is kept low: this means that it will not +// overtake the 'canon' chain until after it's passed canon by about 200 blocks. +// +// Details at: +// - https://github.com/ethereum/go-ethereum/issues/18977 +// - https://github.com/ethereum/go-ethereum/pull/18988 +func TestLowDiffLongChain(t *testing.T) { + // Generate a canonical chain to act as the main dataset + engine := ethash.NewFaker() + db := rawdb.NewMemoryDatabase() + genesis := (&Genesis{ + Config: params.TestChainConfig, + }).MustCommit(db) + + // We must use a pretty long chain to ensure that the fork doesn't overtake us + // until after at least 128 blocks post tip + blocks, _ := GenerateChain(params.TestChainConfig, genesis, engine, db, 6*triesInMemory, func(i int, b *BlockGen) { + b.SetCoinbase(common.Address{1}) + b.OffsetTime(-9) + }) + + // Import the canonical chain + diskdb := rawdb.NewMemoryDatabase() + (&Genesis{ + Config: params.TestChainConfig, + }).MustCommit(diskdb) + + chain, err := NewBlockChain(diskdb, nil, params.TestChainConfig, engine, vm.Config{}, nil) + if err != nil { + t.Fatalf("failed to create tester chain: %v", err) + } + if n, err := chain.InsertChain(blocks); err != nil { + t.Fatalf("block %d: failed to insert into chain: %v", n, err) + } + // Generate fork chain, starting from an early block + parent := blocks[10] + fork, _ := GenerateChain(params.TestChainConfig, parent, engine, db, 8*triesInMemory, func(i int, b *BlockGen) { + b.SetCoinbase(common.Address{2}) + }) + + // And now import the fork + if i, err := chain.InsertChain(fork); err != nil { + t.Fatalf("block %d: failed to insert into chain: %v", i, err) + } + head := chain.CurrentBlock() + if got := fork[len(fork)-1].Hash(); got != head.Hash() { + t.Fatalf("head wrong, expected %x got %x", head.Hash(), got) + } + // Sanity check that all the canonical numbers are present + header := chain.CurrentHeader() + for number := head.NumberU64(); number > 0; number-- { + if hash := chain.GetHeaderByNumber(number).Hash(); hash != header.Hash() { + t.Fatalf("header %d: canonical hash mismatch: have %x, want %x", number, hash, header.Hash()) + } + header = chain.GetHeader(header.ParentHash, number-1) + } +} + +func TestInsertKnownHeaders(t *testing.T) { testInsertKnownChainData(t, "headers") } +func TestInsertKnownReceiptChain(t *testing.T) { testInsertKnownChainData(t, "receipts") } +func TestInsertKnownBlocks(t *testing.T) { testInsertKnownChainData(t, "blocks") } + +func testInsertKnownChainData(t *testing.T, typ string) { + engine := ethash.NewFaker() + + db := rawdb.NewMemoryDatabase() + genesis := (&Genesis{ + Config: params.TestChainConfig, + }).MustCommit(db) + + blocks, receipts := GenerateChain(params.TestChainConfig, genesis, engine, db, 32, func(i int, b *BlockGen) { b.SetCoinbase(common.Address{1}) }) + // A longer chain but total difficulty is lower. + blocks2, receipts2 := GenerateChain(params.TestChainConfig, blocks[len(blocks)-1], engine, db, 65, func(i int, b *BlockGen) { b.SetCoinbase(common.Address{1}) }) + // A shorter chain but total difficulty is higher. + blocks3, receipts3 := GenerateChain(params.TestChainConfig, blocks[len(blocks)-1], engine, db, 64, func(i int, b *BlockGen) { + b.SetCoinbase(common.Address{1}) + b.OffsetTime(-9) // A higher difficulty + }) + // Import the shared chain and the original canonical one + dir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("failed to create temp freezer dir: %v", err) + } + defer os.Remove(dir) + chaindb, err := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), dir, "", false) + if err != nil { + t.Fatalf("failed to create temp freezer db: %v", err) + } + (&Genesis{ + Config: params.TestChainConfig, + }).MustCommit(chaindb) + defer os.RemoveAll(dir) + + chain, err := NewBlockChain(chaindb, nil, params.TestChainConfig, engine, vm.Config{}, nil) + if err != nil { + t.Fatalf("failed to create tester chain: %v", err) + } + + var ( + inserter func(blocks []*types.Block, receipts []types.Receipts) error + asserter func(t *testing.T, block *types.Block) + ) + headers, headers2 := make([]*types.Header, 0, len(blocks)), make([]*types.Header, 0, len(blocks2)) + for _, block := range blocks { + headers = append(headers, block.Header()) + } + for _, block := range blocks2 { + headers2 = append(headers2, block.Header()) + } + if typ == "headers" { + inserter = func(blocks []*types.Block, receipts []types.Receipts) error { + headers := make([]*types.Header, 0, len(blocks)) + for _, block := range blocks { + headers = append(headers, block.Header()) + } + _, err := chain.InsertHeaderChain(headers, 1) + return err + } + asserter = func(t *testing.T, block *types.Block) { + if chain.CurrentHeader().Hash() != block.Hash() { + t.Fatalf("current head header mismatch, have %v, want %v", chain.CurrentHeader().Hash().Hex(), block.Hash().Hex()) + } + } + } else if typ == "receipts" { + inserter = func(blocks []*types.Block, receipts []types.Receipts) error { + headers := make([]*types.Header, 0, len(blocks)) + for _, block := range blocks { + headers = append(headers, block.Header()) + } + _, err := chain.InsertHeaderChain(headers, 1) + if err != nil { + return err + } + _, err = chain.InsertReceiptChain(blocks, receipts, 0) + return err + } + asserter = func(t *testing.T, block *types.Block) { + if chain.CurrentFastBlock().Hash() != block.Hash() { + t.Fatalf("current head fast block mismatch, have %v, want %v", chain.CurrentFastBlock().Hash().Hex(), block.Hash().Hex()) + } + } + } else { + inserter = func(blocks []*types.Block, receipts []types.Receipts) error { + _, err := chain.InsertChain(blocks) + return err + } + asserter = func(t *testing.T, block *types.Block) { + if chain.CurrentBlock().Hash() != block.Hash() { + t.Fatalf("current head block mismatch, have %v, want %v", chain.CurrentBlock().Hash().Hex(), block.Hash().Hex()) + } + } + } + + if err := inserter(blocks, receipts); err != nil { + t.Fatalf("failed to insert chain data: %v", err) + } + + // Reimport the chain data again. All the imported + // chain data are regarded "known" data. + if err := inserter(blocks, receipts); err != nil { + t.Fatalf("failed to insert chain data: %v", err) + } + asserter(t, blocks[len(blocks)-1]) + + // Import a long canonical chain with some known data as prefix. + rollback := blocks[len(blocks)/2].NumberU64() + + chain.SetHead(rollback - 1) + if err := inserter(append(blocks, blocks2...), append(receipts, receipts2...)); err != nil { + t.Fatalf("failed to insert chain data: %v", err) + } + asserter(t, blocks2[len(blocks2)-1]) + + // Import a heavier shorter but higher total difficulty chain with some known data as prefix. + if err := inserter(append(blocks, blocks3...), append(receipts, receipts3...)); err != nil { + t.Fatalf("failed to insert chain data: %v", err) + } + asserter(t, blocks3[len(blocks3)-1]) + + // Import a longer but lower total difficulty chain with some known data as prefix. + if err := inserter(append(blocks, blocks2...), append(receipts, receipts2...)); err != nil { + t.Fatalf("failed to insert chain data: %v", err) + } + // The head shouldn't change. + asserter(t, blocks3[len(blocks3)-1]) + + // Rollback the heavier chain and re-insert the longer chain again + chain.SetHead(rollback - 1) + if err := inserter(append(blocks, blocks2...), append(receipts, receipts2...)); err != nil { + t.Fatalf("failed to insert chain data: %v", err) + } + asserter(t, blocks2[len(blocks2)-1]) +} + +// getLongAndShortChains returns two chains: A is longer, B is heavier. +func getLongAndShortChains() (bc *BlockChain, longChain []*types.Block, heavyChain []*types.Block, err error) { + // Generate a canonical chain to act as the main dataset + engine := ethash.NewFaker() + db := rawdb.NewMemoryDatabase() + genesis := (&Genesis{BaseFee: big.NewInt(params.InitialBaseFee)}).MustCommit(db) + + // Generate and import the canonical chain, + // Offset the time, to keep the difficulty low + longChain, _ = GenerateChain(params.TestChainConfig, genesis, engine, db, 80, func(i int, b *BlockGen) { + b.SetCoinbase(common.Address{1}) + }) + diskdb := rawdb.NewMemoryDatabase() + (&Genesis{BaseFee: big.NewInt(params.InitialBaseFee)}).MustCommit(diskdb) + + chain, err := NewBlockChain(diskdb, nil, params.TestChainConfig, engine, vm.Config{}, nil) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to create tester chain: %v", err) + } + + // Generate fork chain, make it shorter than canon, with common ancestor pretty early + parentIndex := 3 + parent := longChain[parentIndex] + heavyChainExt, _ := GenerateChain(params.TestChainConfig, parent, engine, db, 75, func(i int, b *BlockGen) { + b.SetCoinbase(common.Address{2}) + b.OffsetTime(-9) + }) + heavyChain = append(heavyChain, longChain[:parentIndex+1]...) + heavyChain = append(heavyChain, heavyChainExt...) + + // Verify that the test is sane + var ( + longerTd = new(big.Int) + shorterTd = new(big.Int) + ) + for index, b := range longChain { + longerTd.Add(longerTd, b.Difficulty()) + if index <= parentIndex { + shorterTd.Add(shorterTd, b.Difficulty()) + } + } + for _, b := range heavyChain { + shorterTd.Add(shorterTd, b.Difficulty()) + } + if shorterTd.Cmp(longerTd) <= 0 { + return nil, nil, nil, fmt.Errorf("Test is moot, heavyChain td (%v) must be larger than canon td (%v)", shorterTd, longerTd) + } + longerNum := longChain[len(longChain)-1].NumberU64() + shorterNum := heavyChain[len(heavyChain)-1].NumberU64() + if shorterNum >= longerNum { + return nil, nil, nil, fmt.Errorf("Test is moot, heavyChain num (%v) must be lower than canon num (%v)", shorterNum, longerNum) + } + return chain, longChain, heavyChain, nil +} + +func TestTransactionIndices(t *testing.T) { + // Configure and generate a sample block chain + var ( + gendb = rawdb.NewMemoryDatabase() + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + address = crypto.PubkeyToAddress(key.PublicKey) + funds = big.NewInt(1000000000) + gspec = &Genesis{Config: params.TestChainConfig, Alloc: GenesisAlloc{address: {Balance: funds}}} + genesis = gspec.MustCommit(gendb) + signer = types.NewEIP155Signer(gspec.Config.ChainId) + ) + height := uint64(128) + blocks, receipts := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), gendb, int(height), func(i int, block *BlockGen) { + tx, err := types.SignTx(types.NewTransaction(block.TxNonce(address), common.Address{0x00}, big.NewInt(1000), params.TxGas, nil, nil), signer, key) + if err != nil { + panic(err) + } + block.AddTx(tx) + }) + blocks2, _ := GenerateChain(gspec.Config, blocks[len(blocks)-1], ethash.NewFaker(), gendb, 10, nil) + + check := func(tail *uint64, chain *BlockChain) { + stored := rawdb.ReadTxIndexTail(chain.db) + if tail == nil && stored != nil { + t.Fatalf("Oldest indexded block mismatch, want nil, have %d", *stored) + } + if tail != nil && *stored != *tail { + t.Fatalf("Oldest indexded block mismatch, want %d, have %d", *tail, *stored) + } + if tail != nil { + for i := *tail; i <= chain.CurrentBlock().NumberU64(); i++ { + block := rawdb.ReadBlock(chain.db, rawdb.ReadCanonicalHash(chain.db, i), i) + if block.Transactions().Len() == 0 { + continue + } + for _, tx := range block.Transactions() { + if index := rawdb.ReadTxLookupEntry(chain.db, tx.Hash()); index == nil { + t.Fatalf("Miss transaction indice, number %d hash %s", i, tx.Hash().Hex()) + } + } + } + for i := uint64(0); i < *tail; i++ { + block := rawdb.ReadBlock(chain.db, rawdb.ReadCanonicalHash(chain.db, i), i) + if block.Transactions().Len() == 0 { + continue + } + for _, tx := range block.Transactions() { + if index := rawdb.ReadTxLookupEntry(chain.db, tx.Hash()); index != nil { + t.Fatalf("Transaction indice should be deleted, number %d hash %s", i, tx.Hash().Hex()) + } + } + } + } + } + frdir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("failed to create temp freezer dir: %v", err) + } + defer os.Remove(frdir) + ancientDb, err := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), frdir, "", false) + if err != nil { + t.Fatalf("failed to create temp freezer db: %v", err) + } + gspec.MustCommit(ancientDb) + + // Import all blocks into ancient db + l := uint64(0) + chain, err := NewBlockChain(ancientDb, nil, params.TestChainConfig, ethash.NewFaker(), vm.Config{}, &l) + if err != nil { + t.Fatalf("failed to create tester chain: %v", err) + } + headers := make([]*types.Header, len(blocks)) + for i, block := range blocks { + headers[i] = block.Header() + } + if n, err := chain.InsertHeaderChain(headers, 0); err != nil { + t.Fatalf("failed to insert header %d: %v", n, err) + } + if n, err := chain.InsertReceiptChain(blocks, receipts, 128); err != nil { + t.Fatalf("block %d: failed to insert into chain: %v", n, err) + } + chain.Stop() + ancientDb.Close() + + // Init block chain with external ancients, check all needed indices has been indexed. + limit := []uint64{0, 32, 64, 128} + for _, l := range limit { + ancientDb, err = rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), frdir, "", false) + if err != nil { + t.Fatalf("failed to create temp freezer db: %v", err) + } + gspec.MustCommit(ancientDb) + chain, err = NewBlockChain(ancientDb, nil, params.TestChainConfig, ethash.NewFaker(), vm.Config{}, &l) + if err != nil { + t.Fatalf("failed to create tester chain: %v", err) + } + time.Sleep(50 * time.Millisecond) // Wait for indices initialisation + var tail uint64 + if l != 0 { + tail = uint64(128) - l + 1 + } + check(&tail, chain) + chain.Stop() + ancientDb.Close() + } -*/ + // Reconstruct a block chain which only reserves HEAD-64 tx indices + ancientDb, err = rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), frdir, "", false) + if err != nil { + t.Fatalf("failed to create temp freezer db: %v", err) + } + gspec.MustCommit(ancientDb) + + limit = []uint64{0, 64 /* drop stale */, 32 /* shorten history */, 64 /* extend history */, 0 /* restore all */} + tails := []uint64{0, 67 /* 130 - 64 + 1 */, 100 /* 131 - 32 + 1 */, 69 /* 132 - 64 + 1 */, 0} + for i, l := range limit { + chain, err = NewBlockChain(ancientDb, nil, params.TestChainConfig, ethash.NewFaker(), vm.Config{}, &l) + if err != nil { + t.Fatalf("failed to create tester chain: %v", err) + } + chain.InsertChain(blocks2[i : i+1]) // Feed chain a higher block to trigger indices updater. + time.Sleep(50 * time.Millisecond) // Wait for indices initialisation + check(&tails[i], chain) + chain.Stop() + } +} + +func TestSkipStaleTxIndicesInFastSync(t *testing.T) { + // Configure and generate a sample block chain + var ( + gendb = rawdb.NewMemoryDatabase() + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + address = crypto.PubkeyToAddress(key.PublicKey) + funds = big.NewInt(1000000000) + gspec = &Genesis{Config: params.TestChainConfig, Alloc: GenesisAlloc{address: {Balance: funds}}} + genesis = gspec.MustCommit(gendb) + signer = types.NewEIP155Signer(gspec.Config.ChainId) + ) + height := uint64(128) + blocks, receipts := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), gendb, int(height), func(i int, block *BlockGen) { + tx, err := types.SignTx(types.NewTransaction(block.TxNonce(address), common.Address{0x00}, big.NewInt(1000), params.TxGas, nil, nil), signer, key) + if err != nil { + panic(err) + } + block.AddTx(tx) + }) + + check := func(tail *uint64, chain *BlockChain) { + stored := rawdb.ReadTxIndexTail(chain.db) + if tail == nil && stored != nil { + t.Fatalf("Oldest indexded block mismatch, want nil, have %d", *stored) + } + if tail != nil && *stored != *tail { + t.Fatalf("Oldest indexded block mismatch, want %d, have %d", *tail, *stored) + } + if tail != nil { + for i := *tail; i <= chain.CurrentBlock().NumberU64(); i++ { + block := rawdb.ReadBlock(chain.db, rawdb.ReadCanonicalHash(chain.db, i), i) + if block.Transactions().Len() == 0 { + continue + } + for _, tx := range block.Transactions() { + if index := rawdb.ReadTxLookupEntry(chain.db, tx.Hash()); index == nil { + t.Fatalf("Miss transaction indice, number %d hash %s", i, tx.Hash().Hex()) + } + } + } + for i := uint64(0); i < *tail; i++ { + block := rawdb.ReadBlock(chain.db, rawdb.ReadCanonicalHash(chain.db, i), i) + if block.Transactions().Len() == 0 { + continue + } + for _, tx := range block.Transactions() { + if index := rawdb.ReadTxLookupEntry(chain.db, tx.Hash()); index != nil { + t.Fatalf("Transaction indice should be deleted, number %d hash %s", i, tx.Hash().Hex()) + } + } + } + } + } + + frdir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatalf("failed to create temp freezer dir: %v", err) + } + defer os.Remove(frdir) + ancientDb, err := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), frdir, "", false) + if err != nil { + t.Fatalf("failed to create temp freezer db: %v", err) + } + gspec.MustCommit(ancientDb) + + // Import all blocks into ancient db, only HEAD-32 indices are kept. + l := uint64(32) + chain, err := NewBlockChain(ancientDb, nil, params.TestChainConfig, ethash.NewFaker(), vm.Config{}, &l) + if err != nil { + t.Fatalf("failed to create tester chain: %v", err) + } + headers := make([]*types.Header, len(blocks)) + for i, block := range blocks { + headers[i] = block.Header() + } + if n, err := chain.InsertHeaderChain(headers, 0); err != nil { + t.Fatalf("failed to insert header %d: %v", n, err) + } + // The indices before ancient-N(32) should be ignored. After that all blocks should be indexed. + if n, err := chain.InsertReceiptChain(blocks, receipts, 64); err != nil { + t.Fatalf("block %d: failed to insert into chain: %v", n, err) + } + tail := uint64(32) + check(&tail, chain) +} func TestBlocksHashCacheUpdate(t *testing.T) { _, chain, err := newCanonical(ethash.NewFaker(), 0, true) @@ -1543,7 +2218,7 @@ func TestEIP2718Transition(t *testing.T) { diskdb := rawdb.NewMemoryDatabase() gspec.MustCommit(diskdb) - chain, err := NewBlockChain(diskdb, nil, gspec.Config, engine, vm.Config{}) + chain, err := NewBlockChain(diskdb, nil, gspec.Config, engine, vm.Config{}, nil) if err != nil { t.Fatalf("failed to create tester chain: %v", err) } @@ -1638,7 +2313,7 @@ func TestTransientStorageReset(t *testing.T) { gspec.MustCommit(diskdb) // Initialize the blockchain with 1153 enabled. - chain, err := NewBlockChain(diskdb, nil, gspec.Config, engine, vmConfig) + chain, err := NewBlockChain(diskdb, nil, gspec.Config, engine, vmConfig, nil) if err != nil { t.Fatalf("failed to create tester chain: %v", err) } @@ -1733,7 +2408,7 @@ func TestEIP3651(t *testing.T) { diskdb := rawdb.NewMemoryDatabase() gspec.MustCommit(diskdb) - chain, err := NewBlockChain(diskdb, nil, gspec.Config, engine, vm.Config{}) + chain, err := NewBlockChain(diskdb, nil, gspec.Config, engine, vm.Config{}, nil) if err != nil { t.Fatalf("failed to create tester chain: %v", err) } @@ -1836,7 +2511,7 @@ func TestDeleteCreateRevert(t *testing.T) { diskdb := rawdb.NewMemoryDatabase() gspec.MustCommit(diskdb) - chain, err := NewBlockChain(diskdb, nil, params.TestChainConfig, engine, vm.Config{}) + chain, err := NewBlockChain(diskdb, nil, params.TestChainConfig, engine, vm.Config{}, nil) if err != nil { t.Fatalf("failed to create tester chain: %v", err) } @@ -1844,3 +2519,80 @@ func TestDeleteCreateRevert(t *testing.T) { t.Fatalf("block %d: failed to insert into chain: %v", n, err) } } + +func TestCreateThenDeletePostByzantium(t *testing.T) { + testCreateThenDelete(t, params.TestChainConfig) +} + +// testCreateThenDelete tests a creation and subsequent deletion of a contract, happening +// within the same block. +func testCreateThenDelete(t *testing.T, config *params.ChainConfig) { + var ( + engine = ethash.NewFaker() + // A sender who makes transactions, has some funds + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + address = crypto.PubkeyToAddress(key.PublicKey) + destAddress = crypto.CreateAddress(address, 0) + funds = big.NewInt(1000000000000000000) + ) + + // runtime code is 0x60ffff : PUSH1 0xFF SELFDESTRUCT, a.k.a SELFDESTRUCT(0xFF) + code := append([]byte{0x60, 0xff, 0xff}, make([]byte, 32-3)...) + initCode := []byte{ + // SSTORE 1:1 + byte(vm.PUSH1), 0x1, + byte(vm.PUSH1), 0x1, + byte(vm.SSTORE), + // Get the runtime-code on the stack + byte(vm.PUSH32)} + initCode = append(initCode, code...) + initCode = append(initCode, []byte{ + byte(vm.PUSH1), 0x0, // offset + byte(vm.MSTORE), + byte(vm.PUSH1), 0x3, // size + byte(vm.PUSH1), 0x0, // offset + byte(vm.RETURN), // return 3 bytes of zero-code + }...) + gspec := &Genesis{ + Config: config, + Alloc: GenesisAlloc{ + address: {Balance: funds}, + }, + } + nonce := uint64(0) + signer := types.HomesteadSigner{} + db, blocks, _ := GenerateChainWithGenesis(gspec, engine, 2, func(i int, b *BlockGen) { + fee := big.NewInt(1) + if b.header.BaseFee != nil { + fee = b.header.BaseFee + } + b.SetCoinbase(common.Address{1}) + tx, _ := types.SignNewTx(key, signer, &types.LegacyTx{ + Nonce: nonce, + GasPrice: new(big.Int).Set(fee), + Gas: 100000, + Data: initCode, + }) + nonce++ + b.AddTx(tx) + tx, _ = types.SignNewTx(key, signer, &types.LegacyTx{ + Nonce: nonce, + GasPrice: new(big.Int).Set(fee), + Gas: 100000, + To: &destAddress, + }) + b.AddTx(tx) + nonce++ + }) + // Import the canonical chain + chain, err := NewBlockChain(db, nil, gspec.Config, engine, vm.Config{}, nil) + if err != nil { + t.Fatalf("failed to create tester chain: %v", err) + } + // Import the blocks + for _, block := range blocks { + if _, err := chain.InsertChain([]*types.Block{block}); err != nil { + t.Fatalf("block %d: failed to insert into chain: %v", block.NumberU64(), err) + } + } +} diff --git a/core/chain_makers.go b/core/chain_makers.go index 5e2fcfcfea3..f01390b6759 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -74,6 +74,13 @@ func (b *BlockGen) SetExtra(data []byte) { b.header.Extra = data } +// SetDifficulty sets the difficulty field of the generated block. This method is +// useful for Clique tests where the difficulty does not depend on time. For the +// ethash tests, please use OffsetTime, which implicitly recalculates the diff. +func (b *BlockGen) SetDifficulty(diff *big.Int) { + b.header.Difficulty = diff +} + // addTx adds a transaction to the generated block. If no coinbase has // been set, the block's coinbase is set to the zero address. // diff --git a/core/chain_makers_test.go b/core/chain_makers_test.go index d24cd2ec9e4..11557d51814 100644 --- a/core/chain_makers_test.go +++ b/core/chain_makers_test.go @@ -18,9 +18,10 @@ package core import ( "fmt" - "github.com/XinFinOrg/XDPoSChain/core/rawdb" "math/big" + "github.com/XinFinOrg/XDPoSChain/core/rawdb" + "github.com/XinFinOrg/XDPoSChain/consensus/ethash" "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/core/vm" @@ -78,7 +79,7 @@ func ExampleGenerateChain() { }) // Import the chain. This runs all block validation rules. - blockchain, _ := NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}) + blockchain, _ := NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) defer blockchain.Stop() if i, err := blockchain.InsertChain(chain); err != nil { diff --git a/core/dao_test.go b/core/dao_test.go index e115eddf54e..cb58d5e9210 100644 --- a/core/dao_test.go +++ b/core/dao_test.go @@ -48,7 +48,7 @@ func TestDAOForkRangeExtradata(t *testing.T) { proConf.DAOForkBlock = forkBlock proConf.DAOForkSupport = true - proBc, _ := NewBlockChain(proDb, nil, &proConf, ethash.NewFaker(), vm.Config{}) + proBc, _ := NewBlockChain(proDb, nil, &proConf, ethash.NewFaker(), vm.Config{}, nil) defer proBc.Stop() conDb := rawdb.NewMemoryDatabase() @@ -58,7 +58,7 @@ func TestDAOForkRangeExtradata(t *testing.T) { conConf.DAOForkBlock = forkBlock conConf.DAOForkSupport = false - conBc, _ := NewBlockChain(conDb, nil, &conConf, ethash.NewFaker(), vm.Config{}) + conBc, _ := NewBlockChain(conDb, nil, &conConf, ethash.NewFaker(), vm.Config{}, nil) defer conBc.Stop() if _, err := proBc.InsertChain(prefix); err != nil { @@ -72,7 +72,7 @@ func TestDAOForkRangeExtradata(t *testing.T) { // Create a pro-fork block, and try to feed into the no-fork chain db = rawdb.NewMemoryDatabase() gspec.MustCommit(db) - bc, _ := NewBlockChain(db, nil, &conConf, ethash.NewFaker(), vm.Config{}) + bc, _ := NewBlockChain(db, nil, &conConf, ethash.NewFaker(), vm.Config{}, nil) defer bc.Stop() blocks := conBc.GetBlocksFromHash(conBc.CurrentBlock().Hash(), int(conBc.CurrentBlock().NumberU64())) @@ -97,7 +97,7 @@ func TestDAOForkRangeExtradata(t *testing.T) { // Create a no-fork block, and try to feed into the pro-fork chain db = rawdb.NewMemoryDatabase() gspec.MustCommit(db) - bc, _ = NewBlockChain(db, nil, &proConf, ethash.NewFaker(), vm.Config{}) + bc, _ = NewBlockChain(db, nil, &proConf, ethash.NewFaker(), vm.Config{}, nil) defer bc.Stop() blocks = proBc.GetBlocksFromHash(proBc.CurrentBlock().Hash(), int(proBc.CurrentBlock().NumberU64())) @@ -123,7 +123,7 @@ func TestDAOForkRangeExtradata(t *testing.T) { // Verify that contra-forkers accept pro-fork extra-datas after forking finishes db = rawdb.NewMemoryDatabase() gspec.MustCommit(db) - bc, _ := NewBlockChain(db, nil, &conConf, ethash.NewFaker(), vm.Config{}) + bc, _ := NewBlockChain(db, nil, &conConf, ethash.NewFaker(), vm.Config{}, nil) defer bc.Stop() blocks := conBc.GetBlocksFromHash(conBc.CurrentBlock().Hash(), int(conBc.CurrentBlock().NumberU64())) @@ -143,7 +143,7 @@ func TestDAOForkRangeExtradata(t *testing.T) { // Verify that pro-forkers accept contra-fork extra-datas after forking finishes db = rawdb.NewMemoryDatabase() gspec.MustCommit(db) - bc, _ = NewBlockChain(db, nil, &proConf, ethash.NewFaker(), vm.Config{}) + bc, _ = NewBlockChain(db, nil, &proConf, ethash.NewFaker(), vm.Config{}, nil) defer bc.Stop() blocks = proBc.GetBlocksFromHash(proBc.CurrentBlock().Hash(), int(proBc.CurrentBlock().NumberU64())) diff --git a/core/error.go b/core/error.go index 67bb75730e8..5ea6fff6c56 100644 --- a/core/error.go +++ b/core/error.go @@ -31,6 +31,8 @@ var ( // ErrNoGenesis is returned when there is no Genesis Block. ErrNoGenesis = errors.New("genesis not found in chain") + + errSideChainReceipts = errors.New("side blocks can't be accepted as ancient chain data") ) // List of evm-call-message pre-checking errors. All state transtion messages will diff --git a/core/genesis.go b/core/genesis.go index 75e019bfd64..2584dc2f0db 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -103,6 +103,10 @@ func (e *GenesisMismatchError) Error() string { // // The returned chain configuration is never nil. func SetupGenesisBlock(db ethdb.Database, genesis *Genesis) (*params.ChainConfig, common.Hash, error) { + return SetupGenesisBlockWithOverride(db, genesis, nil) +} + +func SetupGenesisBlockWithOverride(db ethdb.Database, genesis *Genesis, overrideBerlin *big.Int) (*params.ChainConfig, common.Hash, error) { if genesis != nil && genesis.Config == nil { return params.AllEthashProtocolChanges, common.Hash{}, errGenesisNoConfig } @@ -120,6 +124,22 @@ func SetupGenesisBlock(db ethdb.Database, genesis *Genesis) (*params.ChainConfig return genesis.Config, block.Hash(), err } + // We have the genesis block in database(perhaps in ancient database) + // but the corresponding state is missing. + header := rawdb.ReadHeader(db, stored, 0) + if _, err := state.New(header.Root, state.NewDatabaseWithCache(db, 0)); err != nil { + if genesis == nil { + genesis = DefaultGenesisBlock() + } + // Ensure the stored genesis matches with the given one. + hash := genesis.ToBlock(nil).Hash() + if hash != stored { + return genesis.Config, hash, &GenesisMismatchError{stored, hash} + } + block, err := genesis.Commit(db) + return genesis.Config, block.Hash(), err + } + // Check whether the genesis block is already written. if genesis != nil { hash := genesis.ToBlock(nil).Hash() diff --git a/core/genesis_test.go b/core/genesis_test.go index 483fd50629d..fabfc0fe38b 100644 --- a/core/genesis_test.go +++ b/core/genesis_test.go @@ -121,7 +121,7 @@ func TestSetupGenesis(t *testing.T) { // Advance to block #4, past the homestead transition block of customg. genesis := oldcustomg.MustCommit(db) - bc, _ := NewBlockChain(db, nil, oldcustomg.Config, ethash.NewFullFaker(), vm.Config{}) + bc, _ := NewBlockChain(db, nil, oldcustomg.Config, ethash.NewFullFaker(), vm.Config{}, nil) defer bc.Stop() blocks, _ := GenerateChain(oldcustomg.Config, genesis, ethash.NewFaker(), db, 4, nil) diff --git a/core/headerchain.go b/core/headerchain.go index af64b5cb957..c23832f0bd0 100644 --- a/core/headerchain.go +++ b/core/headerchain.go @@ -288,9 +288,14 @@ func (hc *HeaderChain) InsertHeaderChain(chain []*types.Header, writeHeader WhCa return i, errors.New("aborted") } // If the header's already known, skip it, otherwise store - if hc.HasHeader(header.Hash(), header.Number.Uint64()) { - stats.ignored++ - continue + hash := header.Hash() + if hc.HasHeader(hash, header.Number.Uint64()) { + externTd := hc.GetTd(hash, header.Number.Uint64()) + localTd := hc.GetTd(hc.currentHeaderHash, hc.CurrentHeader().Number.Uint64()) + if externTd == nil || externTd.Cmp(localTd) <= 0 { + stats.ignored++ + continue + } } if err := writeHeader(header); err != nil { return i, err @@ -420,25 +425,26 @@ func (hc *HeaderChain) SetCurrentHeader(head *types.Header) { type ( // UpdateHeadBlocksCallback is a callback function that is called by SetHead - // before head header is updated. - UpdateHeadBlocksCallback func(ethdb.KeyValueWriter, *types.Header) + // before head header is updated. The method will return the actual block it + // updated the head to (missing state) and a flag if setHead should continue + // rewinding till that forcefully (exceeded ancient limits) + UpdateHeadBlocksCallback func(ethdb.KeyValueWriter, *types.Header) (uint64, bool) // DeleteBlockContentCallback is a callback function that is called by SetHead // before each header is deleted. DeleteBlockContentCallback func(ethdb.KeyValueWriter, common.Hash, uint64) ) -// SetHead rewinds the local chain to a new head. In the case of headers, everything -// above the new head will be deleted and the new one set. In the case of blocks -// though, the head may be further rewound if block bodies are missing (non-archive -// nodes after a fast sync). +// SetHead rewinds the local chain to a new head. Everything above the new head +// will be deleted and the new one set. func (hc *HeaderChain) SetHead(head uint64, updateFn UpdateHeadBlocksCallback, delFn DeleteBlockContentCallback) { var ( parentHash common.Hash batch = hc.chainDb.NewBatch() + origin = true ) for hdr := hc.CurrentHeader(); hdr != nil && hdr.Number.Uint64() > head; hdr = hc.CurrentHeader() { - hash, num := hdr.Hash(), hdr.Number.Uint64() + num := hdr.Number.Uint64() // Rewind block chain to new head. parent := hc.GetHeader(hdr.ParentHash, num-1) @@ -446,16 +452,21 @@ func (hc *HeaderChain) SetHead(head uint64, updateFn UpdateHeadBlocksCallback, d parent = hc.genesisHeader } parentHash = hdr.ParentHash + // Notably, since geth has the possibility for setting the head to a low // height which is even lower than ancient head. // In order to ensure that the head is always no higher than the data in - // the database(ancient store or active store), we need to update head + // the database (ancient store or active store), we need to update head // first then remove the relative data from the database. // // Update head first(head fast block, head full block) before deleting the data. markerBatch := hc.chainDb.NewBatch() if updateFn != nil { - updateFn(markerBatch, parent) + newHead, force := updateFn(markerBatch, parent) + if force && newHead < head { + log.Warn("Force rewinding till ancient limit", "head", newHead) + head = newHead + } } // Update head header then. rawdb.WriteHeadHeaderHash(markerBatch, parentHash) @@ -466,14 +477,34 @@ func (hc *HeaderChain) SetHead(head uint64, updateFn UpdateHeadBlocksCallback, d hc.currentHeaderHash = parentHash headHeaderGauge.Update(parent.Number.Int64()) - // Remove the relative data from the database. - if delFn != nil { - delFn(batch, hash, num) + // If this is the first iteration, wipe any leftover data upwards too so + // we don't end up with dangling daps in the database + var nums []uint64 + if origin { + for n := num + 1; len(rawdb.ReadAllHashes(hc.chainDb, n)) > 0; n++ { + nums = append([]uint64{n}, nums...) // suboptimal, but we don't really expect this path + } + origin = false + } + nums = append(nums, num) + + // Remove the related data from the database on all sidechains + for _, num := range nums { + // Gather all the side fork hashes + hashes := rawdb.ReadAllHashes(hc.chainDb, num) + if len(hashes) == 0 { + // No hashes in the database whatsoever, probably frozen already + hashes = append(hashes, hdr.Hash()) + } + for _, hash := range hashes { + if delFn != nil { + delFn(batch, hash, num) + } + rawdb.DeleteHeader(batch, hash, num) + rawdb.DeleteTd(batch, hash, num) + } + rawdb.DeleteCanonicalHash(batch, num) } - // Rewind header chain to new head. - rawdb.DeleteHeader(batch, hash, num) - rawdb.DeleteTd(batch, hash, num) - rawdb.DeleteCanonicalHash(batch, num) } // Flush all accumulated deletions. if err := batch.Write(); err != nil { diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index 071d69445bf..4cf3c295ac5 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -20,6 +20,7 @@ import ( "bytes" "encoding/binary" "errors" + "fmt" "math/big" "github.com/XinFinOrg/XDPoSChain/common" @@ -33,20 +34,15 @@ import ( // ReadCanonicalHash retrieves the hash assigned to a canonical block number. func ReadCanonicalHash(db ethdb.Reader, number uint64) common.Hash { - data, _ := db.Ancient(freezerHashTable, number) - if len(data) == 0 { - data, _ = db.Get(headerHashKey(number)) - // In the background freezer is moving data from leveldb to flatten files. - // So during the first check for ancient db, the data is not yet in there, - // but when we reach into leveldb, the data was already moved. That would - // result in a not found error. + var data []byte + db.ReadAncients(func(reader ethdb.AncientReaderOp) error { + data, _ = reader.Ancient(chainFreezerHashTable, number) if len(data) == 0 { - data, _ = db.Ancient(freezerHashTable, number) + // Get it by hash from leveldb + data, _ = db.Get(headerHashKey(number)) } - } - if len(data) == 0 { - return common.Hash{} - } + return nil + }) return common.BytesToHash(data) } @@ -64,6 +60,87 @@ func DeleteCanonicalHash(db ethdb.KeyValueWriter, number uint64) { } } +// ReadAllHashes retrieves all the hashes assigned to blocks at a certain heights, +// both canonical and reorged forks included. +func ReadAllHashes(db ethdb.Iteratee, number uint64) []common.Hash { + prefix := headerKeyPrefix(number) + + hashes := make([]common.Hash, 0, 1) + it := db.NewIterator(prefix, nil) + defer it.Release() + + for it.Next() { + if key := it.Key(); len(key) == len(prefix)+32 { + hashes = append(hashes, common.BytesToHash(key[len(key)-32:])) + } + } + return hashes +} + +type NumberHash struct { + Number uint64 + Hash common.Hash +} + +// ReadAllHashesInRange retrieves all the hashes assigned to blocks at certain +// heights, both canonical and reorged forks included. +// This method considers both limits to be _inclusive_. +func ReadAllHashesInRange(db ethdb.Iteratee, first, last uint64) []*NumberHash { + var ( + start = encodeBlockNumber(first) + keyLength = len(headerPrefix) + 8 + 32 + hashes = make([]*NumberHash, 0, 1+last-first) + it = db.NewIterator(headerPrefix, start) + ) + defer it.Release() + for it.Next() { + key := it.Key() + if len(key) != keyLength { + continue + } + num := binary.BigEndian.Uint64(key[len(headerPrefix) : len(headerPrefix)+8]) + if num > last { + break + } + hash := common.BytesToHash(key[len(key)-32:]) + hashes = append(hashes, &NumberHash{num, hash}) + } + return hashes +} + +// ReadAllCanonicalHashes retrieves all canonical number and hash mappings at the +// certain chain range. If the accumulated entries reaches the given threshold, +// abort the iteration and return the semi-finish result. +func ReadAllCanonicalHashes(db ethdb.Iteratee, from uint64, to uint64, limit int) ([]uint64, []common.Hash) { + // Short circuit if the limit is 0. + if limit == 0 { + return nil, nil + } + var ( + numbers []uint64 + hashes []common.Hash + ) + // Construct the key prefix of start point. + start, end := headerHashKey(from), headerHashKey(to) + it := db.NewIterator(nil, start) + defer it.Release() + + for it.Next() { + if bytes.Compare(it.Key(), end) >= 0 { + break + } + if key := it.Key(); len(key) == len(headerPrefix)+8+1 && bytes.Equal(key[len(key)-1:], headerHashSuffix) { + numbers = append(numbers, binary.BigEndian.Uint64(key[len(headerPrefix):len(headerPrefix)+8])) + hashes = append(hashes, common.BytesToHash(it.Value())) + // If the accumulated entries reaches the limit threshold, return. + if len(numbers) >= limit { + break + } + } + } + return numbers, hashes +} + // ReadHeaderNumber returns the header number assigned to a hash. func ReadHeaderNumber(db ethdb.KeyValueReader, hash common.Hash) *uint64 { data, _ := db.Get(headerNumberKey(hash)) @@ -145,6 +222,32 @@ func WriteHeadFastBlockHash(db ethdb.KeyValueWriter, hash common.Hash) { } } +// ReadLastPivotNumber retrieves the number of the last pivot block. If the node +// full synced, the last pivot will always be nil. +func ReadLastPivotNumber(db ethdb.KeyValueReader) *uint64 { + data, _ := db.Get(lastPivotKey) + if len(data) == 0 { + return nil + } + var pivot uint64 + if err := rlp.DecodeBytes(data, &pivot); err != nil { + log.Error("Invalid pivot block number in database", "err", err) + return nil + } + return &pivot +} + +// WriteLastPivotNumber stores the number of the last pivot block. +func WriteLastPivotNumber(db ethdb.KeyValueWriter, pivot uint64) { + enc, err := rlp.EncodeToBytes(pivot) + if err != nil { + log.Crit("Failed to encode pivot block number", "err", err) + } + if err := db.Put(lastPivotKey, enc); err != nil { + log.Crit("Failed to store pivot block number", "err", err) + } +} + // ReadFastTrieProgress retrieves the number of tries nodes fast synced to allow // reportinc correct numbers across restarts. func ReadFastTrieProgress(db ethdb.KeyValueReader) uint64 { @@ -164,34 +267,119 @@ func WriteFastTrieProgress(db ethdb.KeyValueWriter, count uint64) error { return nil } -// ReadHeaderRLP retrieves a block header in its raw RLP database encoding. -func ReadHeaderRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue { - // First try to look up the data in ancient database. Extra hash - // comparison is necessary since ancient database only maintains - // the canonical data. - data, _ := db.Ancient(freezerHeaderTable, number) - if len(data) > 0 && crypto.Keccak256Hash(data) == hash { - return data +// ReadTxIndexTail retrieves the number of oldest indexed block +// whose transaction indices has been indexed. If the corresponding entry +// is non-existent in database it means the indexing has been finished. +func ReadTxIndexTail(db ethdb.KeyValueReader) *uint64 { + data, _ := db.Get(txIndexTailKey) + if len(data) != 8 { + return nil + } + number := binary.BigEndian.Uint64(data) + return &number +} + +// WriteTxIndexTail stores the number of oldest indexed block +// into database. +func WriteTxIndexTail(db ethdb.KeyValueWriter, number uint64) { + if err := db.Put(txIndexTailKey, encodeBlockNumber(number)); err != nil { + log.Crit("Failed to store the transaction index tail", "err", err) + } +} + +// ReadFastTxLookupLimit retrieves the tx lookup limit used in fast sync. +func ReadFastTxLookupLimit(db ethdb.KeyValueReader) *uint64 { + data, _ := db.Get(fastTxLookupLimitKey) + if len(data) != 8 { + return nil + } + number := binary.BigEndian.Uint64(data) + return &number +} + +// WriteFastTxLookupLimit stores the txlookup limit used in fast sync into database. +func WriteFastTxLookupLimit(db ethdb.KeyValueWriter, number uint64) { + if err := db.Put(fastTxLookupLimitKey, encodeBlockNumber(number)); err != nil { + log.Crit("Failed to store transaction lookup limit for fast sync", "err", err) + } +} + +// ReadHeaderRange returns the rlp-encoded headers, starting at 'number', and going +// backwards towards genesis. This method assumes that the caller already has +// placed a cap on count, to prevent DoS issues. +// Since this method operates in head-towards-genesis mode, it will return an empty +// slice in case the head ('number') is missing. Hence, the caller must ensure that +// the head ('number') argument is actually an existing header. +// +// N.B: Since the input is a number, as opposed to a hash, it's implicit that +// this method only operates on canon headers. +func ReadHeaderRange(db ethdb.Reader, number uint64, count uint64) []rlp.RawValue { + var rlpHeaders []rlp.RawValue + if count == 0 { + return rlpHeaders + } + i := number + if count-1 > number { + // It's ok to request block 0, 1 item + count = number + 1 + } + limit, _ := db.Ancients() + // First read live blocks + if i >= limit { + // If we need to read live blocks, we need to figure out the hash first + hash := ReadCanonicalHash(db, number) + for ; i >= limit && count > 0; i-- { + if data, _ := db.Get(headerKey(i, hash)); len(data) > 0 { + rlpHeaders = append(rlpHeaders, data) + // Get the parent hash for next query + hash = types.HeaderParentHashFromRLP(data) + } else { + break // Maybe got moved to ancients + } + count-- + } + } + if count == 0 { + return rlpHeaders } - // Then try to look up the data in leveldb. - data, _ = db.Get(headerKey(number, hash)) - if len(data) > 0 { - return data + // read remaining from ancients + data, err := db.AncientRange(chainFreezerHeaderTable, i+1-count, count, 0) + if err != nil { + log.Error("Failed to read headers from freezer", "err", err) + return rlpHeaders + } + if uint64(len(data)) != count { + log.Warn("Incomplete read of headers from freezer", "wanted", count, "read", len(data)) + return rlpHeaders } - // In the background freezer is moving data from leveldb to flatten files. - // So during the first check for ancient db, the data is not yet in there, - // but when we reach into leveldb, the data was already moved. That would - // result in a not found error. - data, _ = db.Ancient(freezerHeaderTable, number) - if len(data) > 0 && crypto.Keccak256Hash(data) == hash { - return data + // The data is on the order [h, h+1, .., n] -- reordering needed + for i := range data { + rlpHeaders = append(rlpHeaders, data[len(data)-1-i]) } - return nil // Can't find the data anywhere. + return rlpHeaders +} + +// ReadHeaderRLP retrieves a block header in its raw RLP database encoding. +func ReadHeaderRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue { + var data []byte + db.ReadAncients(func(reader ethdb.AncientReaderOp) error { + // First try to look up the data in ancient database. Extra hash + // comparison is necessary since ancient database only maintains + // the canonical data. + data, _ = reader.Ancient(chainFreezerHeaderTable, number) + if len(data) > 0 && crypto.Keccak256Hash(data) == hash { + return nil + } + // If not, try reading from leveldb + data, _ = db.Get(headerKey(number, hash)) + return nil + }) + return data } // HasHeader verifies the existence of a block header corresponding to the hash. func HasHeader(db ethdb.Reader, hash common.Hash, number uint64) bool { - if has, err := db.Ancient(freezerHashTable, number); err == nil && common.BytesToHash(has) == hash { + if isCanon(db, number, hash) { return true } if has, err := db.Has(headerKey(number, hash)); !has || err != nil { @@ -251,35 +439,49 @@ func deleteHeaderWithoutNumber(db ethdb.KeyValueWriter, hash common.Hash, number } } +// isCanon is an internal utility method, to check whether the given number/hash +// is part of the ancient (canon) set. +func isCanon(reader ethdb.AncientReaderOp, number uint64, hash common.Hash) bool { + h, err := reader.Ancient(chainFreezerHashTable, number) + if err != nil { + return false + } + return bytes.Equal(h, hash[:]) +} + // ReadBodyRLP retrieves the block body (transactions and uncles) in RLP encoding. func ReadBodyRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue { // First try to look up the data in ancient database. Extra hash // comparison is necessary since ancient database only maintains // the canonical data. - data, _ := db.Ancient(freezerBodiesTable, number) - if len(data) > 0 { - h, _ := db.Ancient(freezerHashTable, number) - if common.BytesToHash(h) == hash { - return data + var data []byte + db.ReadAncients(func(reader ethdb.AncientReaderOp) error { + // Check if the data is in ancients + if isCanon(reader, number, hash) { + data, _ = reader.Ancient(chainFreezerBodiesTable, number) + return nil } - } - // Then try to look up the data in leveldb. - data, _ = db.Get(blockBodyKey(number, hash)) - if len(data) > 0 { - return data - } - // In the background freezer is moving data from leveldb to flatten files. - // So during the first check for ancient db, the data is not yet in there, - // but when we reach into leveldb, the data was already moved. That would - // result in a not found error. - data, _ = db.Ancient(freezerBodiesTable, number) - if len(data) > 0 { - h, _ := db.Ancient(freezerHashTable, number) - if common.BytesToHash(h) == hash { - return data + // If not, try reading from leveldb + data, _ = db.Get(blockBodyKey(number, hash)) + return nil + }) + return data +} + +// ReadCanonicalBodyRLP retrieves the block body (transactions and uncles) for the canonical +// block at number, in RLP encoding. +func ReadCanonicalBodyRLP(db ethdb.Reader, number uint64) rlp.RawValue { + var data []byte + db.ReadAncients(func(reader ethdb.AncientReaderOp) error { + data, _ = reader.Ancient(chainFreezerBodiesTable, number) + if len(data) > 0 { + return nil } - } - return nil // Can't find the data anywhere. + // Get it by hash from leveldb + data, _ = db.Get(blockBodyKey(number, ReadCanonicalHash(db, number))) + return nil + }) + return data } // WriteBodyRLP stores an RLP encoded block body into the database. @@ -291,10 +493,9 @@ func WriteBodyRLP(db ethdb.KeyValueWriter, hash common.Hash, number uint64, rlp // HasBody verifies the existence of a block body corresponding to the hash. func HasBody(db ethdb.Reader, hash common.Hash, number uint64) bool { - //TODO: need to add isCanon check - // if isCanon(db, number, hash) { - // return true - // } + if isCanon(db, number, hash) { + return true + } if has, err := db.Has(blockBodyKey(number, hash)); !has || err != nil { return false } @@ -333,7 +534,17 @@ func DeleteBody(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { // ReadTdRLP retrieves a block's total difficulty corresponding to the hash in RLP encoding. func ReadTdRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue { - data, _ := db.Get(headerTDKey(number, hash)) + var data []byte + db.ReadAncients(func(reader ethdb.AncientReaderOp) error { + // Check if the data is in ancients + if isCanon(reader, number, hash) { + data, _ = reader.Ancient(chainFreezerDifficultyTable, number) + return nil + } + // If not, try reading from leveldb + data, _ = db.Get(headerTDKey(number, hash)) + return nil + }) return data } @@ -373,7 +584,7 @@ func DeleteTd(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { // HasReceipts verifies the existence of all the transaction receipts belonging // to a block. func HasReceipts(db ethdb.Reader, hash common.Hash, number uint64) bool { - if has, err := db.Ancient(freezerHashTable, number); err == nil && common.BytesToHash(has) == hash { + if isCanon(db, number, hash) { return true } if has, err := db.Has(blockReceiptsKey(number, hash)); !has || err != nil { @@ -384,33 +595,18 @@ func HasReceipts(db ethdb.Reader, hash common.Hash, number uint64) bool { // ReadReceiptsRLP retrieves all the transaction receipts belonging to a block in RLP encoding. func ReadReceiptsRLP(db ethdb.Reader, hash common.Hash, number uint64) rlp.RawValue { - // First try to look up the data in ancient database. Extra hash - // comparison is necessary since ancient database only maintains - // the canonical data. - data, _ := db.Ancient(freezerReceiptTable, number) - if len(data) > 0 { - h, _ := db.Ancient(freezerHashTable, number) - if common.BytesToHash(h) == hash { - return data - } - } - // Then try to look up the data in leveldb. - data, _ = db.Get(blockReceiptsKey(number, hash)) - if len(data) > 0 { - return data - } - // In the background freezer is moving data from leveldb to flatten files. - // So during the first check for ancient db, the data is not yet in there, - // but when we reach into leveldb, the data was already moved. That would - // result in a not found error. - data, _ = db.Ancient(freezerReceiptTable, number) - if len(data) > 0 { - h, _ := db.Ancient(freezerHashTable, number) - if common.BytesToHash(h) == hash { - return data + var data []byte + db.ReadAncients(func(reader ethdb.AncientReaderOp) error { + // Check if the data is in ancients + if isCanon(reader, number, hash) { + data, _ = reader.Ancient(chainFreezerReceiptTable, number) + return nil } - } - return nil // Can't find the data anywhere. + // If not, try reading from leveldb + data, _ = db.Get(blockReceiptsKey(number, hash)) + return nil + }) + return data } // ReadRawReceipts retrieves all the transaction receipts belonging to a block. @@ -439,7 +635,7 @@ func ReadRawReceipts(db ethdb.Reader, hash common.Hash, number uint64) types.Rec } // ReadReceipts retrieves all the transaction receipts belonging to a block, including -// its correspoinding metadata fields. If it is unable to populate these metadata +// its corresponding metadata fields. If it is unable to populate these metadata // fields then nil is returned. // // The current implementation populates these metadata fields by reading the receipts' @@ -583,6 +779,51 @@ func WriteBlock(db ethdb.KeyValueWriter, block *types.Block) { WriteHeader(db, block.Header()) } +// WriteAncientBlocks writes entire block data into ancient store and returns the total written size. +func WriteAncientBlocks(db ethdb.AncientWriter, blocks []*types.Block, receipts []types.Receipts, td *big.Int) (int64, error) { + var ( + tdSum = new(big.Int).Set(td) + stReceipts []*types.ReceiptForStorage + ) + return db.ModifyAncients(func(op ethdb.AncientWriteOp) error { + for i, block := range blocks { + // Convert receipts to storage format and sum up total difficulty. + stReceipts = stReceipts[:0] + for _, receipt := range receipts[i] { + stReceipts = append(stReceipts, (*types.ReceiptForStorage)(receipt)) + } + header := block.Header() + if i > 0 { + tdSum.Add(tdSum, header.Difficulty) + } + if err := writeAncientBlock(op, block, header, stReceipts, tdSum); err != nil { + return err + } + } + return nil + }) +} + +func writeAncientBlock(op ethdb.AncientWriteOp, block *types.Block, header *types.Header, receipts []*types.ReceiptForStorage, td *big.Int) error { + num := block.NumberU64() + if err := op.AppendRaw(chainFreezerHashTable, num, block.Hash().Bytes()); err != nil { + return fmt.Errorf("can't add block %d hash: %v", num, err) + } + if err := op.Append(chainFreezerHeaderTable, num, header); err != nil { + return fmt.Errorf("can't append block header %d: %v", num, err) + } + if err := op.Append(chainFreezerBodiesTable, num, block.Body()); err != nil { + return fmt.Errorf("can't append block body %d: %v", num, err) + } + if err := op.Append(chainFreezerReceiptTable, num, receipts); err != nil { + return fmt.Errorf("can't append block %d receipts: %v", num, err) + } + if err := op.Append(chainFreezerDifficultyTable, num, td); err != nil { + return fmt.Errorf("can't append block %d total difficulty: %v", num, err) + } + return nil +} + // DeleteBlock removes all block data associated with a hash. func DeleteBlock(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { DeleteReceipts(db, hash, number) @@ -591,11 +832,64 @@ func DeleteBlock(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { DeleteTd(db, hash, number) } -// deleteBlockWithoutNumber removes all block data associated with a hash, except +// DeleteBlockWithoutNumber removes all block data associated with a hash, except // the hash to number mapping. -func deleteBlockWithoutNumber(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { +func DeleteBlockWithoutNumber(db ethdb.KeyValueWriter, hash common.Hash, number uint64) { DeleteReceipts(db, hash, number) deleteHeaderWithoutNumber(db, hash, number) DeleteBody(db, hash, number) DeleteTd(db, hash, number) } + +// FindCommonAncestor returns the last common ancestor of two block headers +func FindCommonAncestor(db ethdb.Reader, a, b *types.Header) *types.Header { + for bn := b.Number.Uint64(); a.Number.Uint64() > bn; { + a = ReadHeader(db, a.ParentHash, a.Number.Uint64()-1) + if a == nil { + return nil + } + } + for an := a.Number.Uint64(); an < b.Number.Uint64(); { + b = ReadHeader(db, b.ParentHash, b.Number.Uint64()-1) + if b == nil { + return nil + } + } + for a.Hash() != b.Hash() { + a = ReadHeader(db, a.ParentHash, a.Number.Uint64()-1) + if a == nil { + return nil + } + b = ReadHeader(db, b.ParentHash, b.Number.Uint64()-1) + if b == nil { + return nil + } + } + return a +} + +// ReadHeadHeader returns the current canonical head header. +func ReadHeadHeader(db ethdb.Reader) *types.Header { + headHeaderHash := ReadHeadHeaderHash(db) + if headHeaderHash == (common.Hash{}) { + return nil + } + headHeaderNumber := ReadHeaderNumber(db, headHeaderHash) + if headHeaderNumber == nil { + return nil + } + return ReadHeader(db, headHeaderHash, *headHeaderNumber) +} + +// ReadHeadHeader returns the current canonical head block. +func ReadHeadBlock(db ethdb.Reader) *types.Block { + headBlockHash := ReadHeadBlockHash(db) + if headBlockHash == (common.Hash{}) { + return nil + } + headBlockNumber := ReadHeaderNumber(db, headBlockHash) + if headBlockNumber == nil { + return nil + } + return ReadBlock(db, headBlockHash, *headBlockNumber) +} diff --git a/core/rawdb/accessors_chain_test.go b/core/rawdb/accessors_chain_test.go index 344c0f21e34..e6ef33fabcf 100644 --- a/core/rawdb/accessors_chain_test.go +++ b/core/rawdb/accessors_chain_test.go @@ -22,10 +22,12 @@ import ( "fmt" "math/big" "os" + "reflect" "testing" "github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/core/types" + "github.com/XinFinOrg/XDPoSChain/crypto" "github.com/XinFinOrg/XDPoSChain/params" "github.com/XinFinOrg/XDPoSChain/rlp" "golang.org/x/crypto/sha3" @@ -236,7 +238,7 @@ func TestTdStorage(t *testing.T) { func TestCanonicalMappingStorage(t *testing.T) { db := NewMemoryDatabase() - // Create a test canonical number and assinged hash to move around + // Create a test canonical number and assigned hash to move around hash, number := common.Hash{0: 0xff}, uint64(314) if entry := ReadCanonicalHash(db, number); entry != (common.Hash{}) { t.Fatalf("Non existent canonical mapping returned: %v", entry) @@ -382,6 +384,69 @@ func checkReceiptsRLP(have, want types.Receipts) error { return nil } +func TestAncientStorage(t *testing.T) { + // Freezer style fast import the chain. + frdir := t.TempDir() + + db, err := NewDatabaseWithFreezer(NewMemoryDatabase(), frdir, "", false) + if err != nil { + t.Fatalf("failed to create database with ancient backend") + } + // Create a test block + block := types.NewBlockWithHeader(&types.Header{ + Number: big.NewInt(0), + Extra: []byte("test block"), + UncleHash: types.EmptyUncleHash, + TxHash: types.EmptyRootHash, + ReceiptHash: types.EmptyRootHash, + }) + // Ensure nothing non-existent will be read + hash, number := block.Hash(), block.NumberU64() + if blob := ReadHeaderRLP(db, hash, number); len(blob) > 0 { + t.Fatalf("non existent header returned") + } + if blob := ReadBodyRLP(db, hash, number); len(blob) > 0 { + t.Fatalf("non existent body returned") + } + if blob := ReadReceiptsRLP(db, hash, number); len(blob) > 0 { + t.Fatalf("non existent receipts returned") + } + if blob := ReadTdRLP(db, hash, number); len(blob) > 0 { + t.Fatalf("non existent td returned") + } + + // Write and verify the header in the database + WriteAncientBlocks(db, []*types.Block{block}, []types.Receipts{nil}, big.NewInt(100)) + + if blob := ReadHeaderRLP(db, hash, number); len(blob) == 0 { + t.Fatalf("no header returned") + } + if blob := ReadBodyRLP(db, hash, number); len(blob) == 0 { + t.Fatalf("no body returned") + } + if blob := ReadReceiptsRLP(db, hash, number); len(blob) == 0 { + t.Fatalf("no receipts returned") + } + if blob := ReadTdRLP(db, hash, number); len(blob) == 0 { + t.Fatalf("no td returned") + } + + // Use a fake hash for data retrieval, nothing should be returned. + fakeHash := common.BytesToHash([]byte{0x01, 0x02, 0x03}) + if blob := ReadHeaderRLP(db, fakeHash, number); len(blob) != 0 { + t.Fatalf("invalid header returned") + } + if blob := ReadBodyRLP(db, fakeHash, number); len(blob) != 0 { + t.Fatalf("invalid body returned") + } + if blob := ReadReceiptsRLP(db, fakeHash, number); len(blob) != 0 { + t.Fatalf("invalid receipts returned") + } + if blob := ReadTdRLP(db, fakeHash, number); len(blob) != 0 { + t.Fatalf("invalid td returned") + } +} + // Tests that logs associated with a single block can be retrieved. func TestReadLogs(t *testing.T) { db := NewMemoryDatabase() @@ -566,3 +631,231 @@ func BenchmarkDecodeRLPLogs(b *testing.B) { } }) } + +func TestCanonicalHashIteration(t *testing.T) { + var cases = []struct { + from, to uint64 + limit int + expect []uint64 + }{ + {1, 8, 0, nil}, + {1, 8, 1, []uint64{1}}, + {1, 8, 10, []uint64{1, 2, 3, 4, 5, 6, 7}}, + {1, 9, 10, []uint64{1, 2, 3, 4, 5, 6, 7, 8}}, + {2, 9, 10, []uint64{2, 3, 4, 5, 6, 7, 8}}, + {9, 10, 10, nil}, + } + // Test empty db iteration + db := NewMemoryDatabase() + numbers, _ := ReadAllCanonicalHashes(db, 0, 10, 10) + if len(numbers) != 0 { + t.Fatalf("No entry should be returned to iterate an empty db") + } + // Fill database with testing data. + for i := uint64(1); i <= 8; i++ { + WriteCanonicalHash(db, common.Hash{}, i) + WriteTd(db, common.Hash{}, i, big.NewInt(10)) // Write some interferential data + } + for i, c := range cases { + numbers, _ := ReadAllCanonicalHashes(db, c.from, c.to, c.limit) + if !reflect.DeepEqual(numbers, c.expect) { + t.Fatalf("Case %d failed, want %v, got %v", i, c.expect, numbers) + } + } +} + +func TestHashesInRange(t *testing.T) { + mkHeader := func(number, seq int) *types.Header { + h := types.Header{ + Difficulty: new(big.Int), + Number: big.NewInt(int64(number)), + GasLimit: uint64(seq), + } + return &h + } + db := NewMemoryDatabase() + // For each number, write N versions of that particular number + total := 0 + for i := 0; i < 15; i++ { + for ii := 0; ii < i; ii++ { + WriteHeader(db, mkHeader(i, ii)) + total++ + } + } + if have, want := len(ReadAllHashesInRange(db, 10, 10)), 10; have != want { + t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have) + } + if have, want := len(ReadAllHashesInRange(db, 10, 9)), 0; have != want { + t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have) + } + if have, want := len(ReadAllHashesInRange(db, 0, 100)), total; have != want { + t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have) + } + if have, want := len(ReadAllHashesInRange(db, 9, 10)), 9+10; have != want { + t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have) + } + if have, want := len(ReadAllHashes(db, 10)), 10; have != want { + t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have) + } + if have, want := len(ReadAllHashes(db, 16)), 0; have != want { + t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have) + } + if have, want := len(ReadAllHashes(db, 1)), 1; have != want { + t.Fatalf("Wrong number of hashes read, want %d, got %d", want, have) + } +} + +// This measures the write speed of the WriteAncientBlocks operation. +func BenchmarkWriteAncientBlocks(b *testing.B) { + // Open freezer database. + frdir := b.TempDir() + db, err := NewDatabaseWithFreezer(NewMemoryDatabase(), frdir, "", false) + if err != nil { + b.Fatalf("failed to create database with ancient backend") + } + defer db.Close() + + // Create the data to insert. The blocks must have consecutive numbers, so we create + // all of them ahead of time. However, there is no need to create receipts + // individually for each block, just make one batch here and reuse it for all writes. + const batchSize = 128 + const blockTxs = 20 + allBlocks := makeTestBlocks(b.N, blockTxs) + batchReceipts := makeTestReceipts(batchSize, blockTxs) + b.ResetTimer() + + // The benchmark loop writes batches of blocks, but note that the total block count is + // b.N. This means the resulting ns/op measurement is the time it takes to write a + // single block and its associated data. + var td = big.NewInt(55) + var totalSize int64 + for i := 0; i < b.N; i += batchSize { + length := batchSize + if i+batchSize > b.N { + length = b.N - i + } + + blocks := allBlocks[i : i+length] + receipts := batchReceipts[:length] + writeSize, err := WriteAncientBlocks(db, blocks, receipts, td) + if err != nil { + b.Fatal(err) + } + totalSize += writeSize + } + + // Enable MB/s reporting. + b.SetBytes(totalSize / int64(b.N)) +} + +// makeTestBlocks creates fake blocks for the ancient write benchmark. +func makeTestBlocks(nblock int, txsPerBlock int) []*types.Block { + key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + signer := types.LatestSignerForChainID(big.NewInt(8)) + + // Create transactions. + txs := make([]*types.Transaction, txsPerBlock) + for i := 0; i < len(txs); i++ { + var err error + to := common.Address{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} + txs[i], err = types.SignNewTx(key, signer, &types.LegacyTx{ + Nonce: 2, + GasPrice: big.NewInt(30000), + Gas: 0x45454545, + To: &to, + }) + if err != nil { + panic(err) + } + } + + // Create the blocks. + blocks := make([]*types.Block, nblock) + for i := 0; i < nblock; i++ { + header := &types.Header{ + Number: big.NewInt(int64(i)), + Extra: []byte("test block"), + } + blocks[i] = types.NewBlockWithHeader(header).WithBody(txs, nil) + blocks[i].Hash() // pre-cache the block hash + } + return blocks +} + +// makeTestReceipts creates fake receipts for the ancient write benchmark. +func makeTestReceipts(n int, nPerBlock int) []types.Receipts { + receipts := make([]*types.Receipt, nPerBlock) + for i := 0; i < len(receipts); i++ { + receipts[i] = &types.Receipt{ + Status: types.ReceiptStatusSuccessful, + CumulativeGasUsed: 0x888888888, + Logs: make([]*types.Log, 5), + } + } + allReceipts := make([]types.Receipts, n) + for i := 0; i < n; i++ { + allReceipts[i] = receipts + } + return allReceipts +} + +func TestHeadersRLPStorage(t *testing.T) { + // Have N headers in the freezer + frdir := t.TempDir() + + db, err := NewDatabaseWithFreezer(NewMemoryDatabase(), frdir, "", false) + if err != nil { + t.Fatalf("failed to create database with ancient backend") + } + defer db.Close() + + // Create blocks + var chain []*types.Block + var pHash common.Hash + for i := 0; i < 100; i++ { + block := types.NewBlockWithHeader(&types.Header{ + Number: big.NewInt(int64(i)), + Extra: []byte("test block"), + UncleHash: types.EmptyUncleHash, + TxHash: types.EmptyRootHash, + ReceiptHash: types.EmptyRootHash, + ParentHash: pHash, + }) + chain = append(chain, block) + pHash = block.Hash() + } + receipts := make([]types.Receipts, 100) + // Write first half to ancients + WriteAncientBlocks(db, chain[:50], receipts[:50], big.NewInt(100)) + // Write second half to db + for i := 50; i < 100; i++ { + WriteCanonicalHash(db, chain[i].Hash(), chain[i].NumberU64()) + WriteBlock(db, chain[i]) + } + checkSequence := func(from, amount int) { + headersRlp := ReadHeaderRange(db, uint64(from), uint64(amount)) + if have, want := len(headersRlp), amount; have != want { + t.Fatalf("have %d headers, want %d", have, want) + } + for i, headerRlp := range headersRlp { + var header types.Header + if err := rlp.DecodeBytes(headerRlp, &header); err != nil { + t.Fatal(err) + } + if have, want := header.Number.Uint64(), uint64(from-i); have != want { + t.Fatalf("wrong number, have %d want %d", have, want) + } + } + } + checkSequence(99, 20) // Latest block and 19 parents + checkSequence(99, 50) // Latest block -> all db blocks + checkSequence(99, 51) // Latest block -> one from ancients + checkSequence(99, 52) // Latest blocks -> two from ancients + checkSequence(50, 2) // One from db, one from ancients + checkSequence(49, 1) // One from ancients + checkSequence(49, 50) // All ancient ones + checkSequence(99, 100) // All blocks + checkSequence(0, 1) // Only genesis + checkSequence(1, 1) // Only block 1 + checkSequence(1, 2) // Genesis + block 1 +} diff --git a/core/rawdb/accessors_indexes.go b/core/rawdb/accessors_indexes.go index 7197fc5d52a..a205a751314 100644 --- a/core/rawdb/accessors_indexes.go +++ b/core/rawdb/accessors_indexes.go @@ -17,6 +17,7 @@ package rawdb import ( + "bytes" "math/big" "github.com/XinFinOrg/XDPoSChain/common" @@ -55,8 +56,20 @@ func ReadTxLookupEntry(db ethdb.Reader, hash common.Hash) *uint64 { // WriteTxLookupEntriesByBlock stores a positional metadata for every transaction from // a block, enabling hash based transaction and receipt lookups. func WriteTxLookupEntriesByBlock(db ethdb.KeyValueWriter, block *types.Block) { + number := block.Number().Bytes() for _, tx := range block.Transactions() { - if err := db.Put(txLookupKey(tx.Hash()), block.Number().Bytes()); err != nil { + if err := db.Put(txLookupKey(tx.Hash()), number); err != nil { + log.Crit("Failed to store transaction lookup entry", "err", err) + } + } +} + +// WriteTxLookupEntriesByHash is identical to WriteTxLookupEntries, but does not +// require a full types.Block as input. +func WriteTxLookupEntriesByHash(db ethdb.KeyValueWriter, number uint64, hashes []common.Hash) { + numberBytes := new(big.Int).SetUint64(number).Bytes() + for _, hash := range hashes { + if err := db.Put(txLookupKey(hash), numberBytes); err != nil { log.Crit("Failed to store transaction lookup entry", "err", err) } } @@ -64,7 +77,18 @@ func WriteTxLookupEntriesByBlock(db ethdb.KeyValueWriter, block *types.Block) { // DeleteTxLookupEntry removes all transaction data associated with a hash. func DeleteTxLookupEntry(db ethdb.KeyValueWriter, hash common.Hash) { - db.Delete(txLookupKey(hash)) + if err := db.Delete(txLookupKey(hash)); err != nil { + log.Crit("Failed to delete transaction lookup entry", "err", err) + } +} + +// DeleteTxLookupEntries removes all transaction lookups for a given block. +func DeleteTxLookupEntriesByHash(db ethdb.KeyValueWriter, hashes []common.Hash) { + for _, hash := range hashes { + if err := db.Delete(txLookupKey(hash)); err != nil { + log.Crit("Failed to delete transaction lookup entry", "err", err) + } + } } // ReadTransaction retrieves a specific transaction from the database, along with @@ -162,29 +186,23 @@ func WriteBloomBits(db ethdb.KeyValueWriter, bit uint, section uint64, head comm } } -// FindCommonAncestor returns the last common ancestor of two block headers -func FindCommonAncestor(db ethdb.Reader, a, b *types.Header) *types.Header { - for bn := b.Number.Uint64(); a.Number.Uint64() > bn; { - a = ReadHeader(db, a.ParentHash, a.Number.Uint64()-1) - if a == nil { - return nil +// DeleteBloombits removes all compressed bloom bits vector belonging to the +// given section range and bit index. +func DeleteBloombits(db ethdb.Database, bit uint, from uint64, to uint64) { + start, end := bloomBitsKey(bit, from, common.Hash{}), bloomBitsKey(bit, to, common.Hash{}) + it := db.NewIterator(nil, start) + defer it.Release() + + for it.Next() { + if bytes.Compare(it.Key(), end) >= 0 { + break } - } - for an := a.Number.Uint64(); an < b.Number.Uint64(); { - b = ReadHeader(db, b.ParentHash, b.Number.Uint64()-1) - if b == nil { - return nil + if len(it.Key()) != len(bloomBitsPrefix)+2+8+32 { + continue } + db.Delete(it.Key()) } - for a.Hash() != b.Hash() { - a = ReadHeader(db, a.ParentHash, a.Number.Uint64()-1) - if a == nil { - return nil - } - b = ReadHeader(db, b.ParentHash, b.Number.Uint64()-1) - if b == nil { - return nil - } + if it.Error() != nil { + log.Crit("Failed to delete bloom bits", "err", it.Error()) } - return a } diff --git a/core/rawdb/accessors_indexes_test.go b/core/rawdb/accessors_indexes_test.go index 52e625e219d..3aca8edf3ee 100644 --- a/core/rawdb/accessors_indexes_test.go +++ b/core/rawdb/accessors_indexes_test.go @@ -18,12 +18,14 @@ package rawdb import ( "hash" + "bytes" "math/big" "testing" "github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/ethdb" + "github.com/XinFinOrg/XDPoSChain/params" "github.com/XinFinOrg/XDPoSChain/rlp" "golang.org/x/crypto/sha3" ) @@ -56,17 +58,17 @@ func (h *testHasher) Hash() common.Hash { func TestLookupStorage(t *testing.T) { tests := []struct { name string - writeTxLookupEntries func(ethdb.Writer, *types.Block) + writeTxLookupEntries func(ethdb.KeyValueWriter, *types.Block) }{ { "DatabaseV6", - func(db ethdb.Writer, block *types.Block) { + func(db ethdb.KeyValueWriter, block *types.Block) { WriteTxLookupEntriesByBlock(db, block) }, }, { "DatabaseV4-V5", - func(db ethdb.Writer, block *types.Block) { + func(db ethdb.KeyValueWriter, block *types.Block) { for _, tx := range block.Transactions() { db.Put(txLookupKey(tx.Hash()), block.Hash().Bytes()) } @@ -74,7 +76,7 @@ func TestLookupStorage(t *testing.T) { }, { "DatabaseV3", - func(db ethdb.Writer, block *types.Block) { + func(db ethdb.KeyValueWriter, block *types.Block) { for index, tx := range block.Transactions() { entry := LegacyTxLookupEntry{ BlockHash: block.Hash(), @@ -132,3 +134,46 @@ func TestLookupStorage(t *testing.T) { }) } } + +func TestDeleteBloomBits(t *testing.T) { + // Prepare testing data + db := NewMemoryDatabase() + for i := uint(0); i < 2; i++ { + for s := uint64(0); s < 2; s++ { + WriteBloomBits(db, i, s, params.MainnetGenesisHash, []byte{0x01, 0x02}) + WriteBloomBits(db, i, s, params.TestnetGenesisHash, []byte{0x01, 0x02}) + } + } + check := func(bit uint, section uint64, head common.Hash, exist bool) { + bits, _ := ReadBloomBits(db, bit, section, head) + if exist && !bytes.Equal(bits, []byte{0x01, 0x02}) { + t.Fatalf("Bloombits mismatch") + } + if !exist && len(bits) > 0 { + t.Fatalf("Bloombits should be removed") + } + } + // Check the existence of written data. + check(0, 0, params.MainnetGenesisHash, true) + check(0, 0, params.TestnetGenesisHash, true) + + // Check the existence of deleted data. + DeleteBloombits(db, 0, 0, 1) + check(0, 0, params.MainnetGenesisHash, false) + check(0, 0, params.TestnetGenesisHash, false) + check(0, 1, params.MainnetGenesisHash, true) + check(0, 1, params.TestnetGenesisHash, true) + + // Check the existence of deleted data. + DeleteBloombits(db, 0, 0, 2) + check(0, 0, params.MainnetGenesisHash, false) + check(0, 0, params.TestnetGenesisHash, false) + check(0, 1, params.MainnetGenesisHash, false) + check(0, 1, params.TestnetGenesisHash, false) + + // Bit1 shouldn't be affect. + check(1, 0, params.MainnetGenesisHash, true) + check(1, 0, params.TestnetGenesisHash, true) + check(1, 1, params.MainnetGenesisHash, true) + check(1, 1, params.TestnetGenesisHash, true) +} diff --git a/core/rawdb/ancient_scheme.go b/core/rawdb/ancient_scheme.go new file mode 100644 index 00000000000..03f292e4e22 --- /dev/null +++ b/core/rawdb/ancient_scheme.go @@ -0,0 +1,61 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rawdb + +// The list of table names of chain freezer. +const ( + // chainFreezerHeaderTable indicates the name of the freezer header table. + chainFreezerHeaderTable = "headers" + + // chainFreezerHashTable indicates the name of the freezer canonical hash table. + chainFreezerHashTable = "hashes" + + // chainFreezerBodiesTable indicates the name of the freezer block body table. + chainFreezerBodiesTable = "bodies" + + // chainFreezerReceiptTable indicates the name of the freezer receipts table. + chainFreezerReceiptTable = "receipts" + + // chainFreezerDifficultyTable indicates the name of the freezer total difficulty table. + chainFreezerDifficultyTable = "diffs" +) + +// chainFreezerTableConfigs configures the settings for tables in the chain freezer. +// Compression is disabled for hashes as they don't compress well. Additionally, +// tail truncation is disabled for the header and hash tables, as these are intended +// to be retained long-term. +var chainFreezerTableConfigs = map[string]freezerTableConfig{ + chainFreezerHeaderTable: {noSnappy: false, prunable: false}, + chainFreezerHashTable: {noSnappy: true, prunable: false}, + chainFreezerBodiesTable: {noSnappy: false, prunable: true}, + chainFreezerReceiptTable: {noSnappy: false, prunable: true}, + chainFreezerDifficultyTable: {noSnappy: true, prunable: true}, +} + +// freezerTableConfig contains the settings for a freezer table. +type freezerTableConfig struct { + noSnappy bool // disables item compression + prunable bool // true for tables that can be pruned by TruncateTail +} + +// The list of identifiers of ancient stores. +var ( + chainFreezerName = "chain" // the folder name of chain segment ancient store. +) + +// freezers the collections of all builtin freezers. +var freezers = []string{chainFreezerName} diff --git a/core/rawdb/ancient_utils.go b/core/rawdb/ancient_utils.go new file mode 100644 index 00000000000..d416aa811e5 --- /dev/null +++ b/core/rawdb/ancient_utils.go @@ -0,0 +1,121 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rawdb + +import ( + "fmt" + + "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/ethdb" +) + +type tableSize struct { + name string + size common.StorageSize +} + +// freezerInfo contains the basic information of the freezer. +type freezerInfo struct { + name string // The identifier of freezer + head uint64 // The number of last stored item in the freezer + tail uint64 // The number of first stored item in the freezer + sizes []tableSize // The storage size per table +} + +// count returns the number of stored items in the freezer. +func (info *freezerInfo) count() uint64 { + return info.head - info.tail + 1 +} + +// size returns the storage size of the entire freezer. +func (info *freezerInfo) size() common.StorageSize { + var total common.StorageSize + for _, table := range info.sizes { + total += table.size + } + return total +} + +// inspectFreezers inspects all freezers registered in the system. +func inspectFreezers(db ethdb.Database) ([]freezerInfo, error) { + var infos []freezerInfo + for _, freezer := range freezers { + switch freezer { + case chainFreezerName: + // Chain ancient store is a bit special. It's always opened along + // with the key-value store, inspect the chain store directly. + info := freezerInfo{name: freezer} + // Retrieve storage size of every contained table. + for table := range chainFreezerTableConfigs { + size, err := db.AncientSize(table) + if err != nil { + return nil, err + } + info.sizes = append(info.sizes, tableSize{name: table, size: common.StorageSize(size)}) + } + // Retrieve the number of last stored item + ancients, err := db.Ancients() + if err != nil { + return nil, err + } + info.head = ancients - 1 + + // Retrieve the number of first stored item + tail, err := db.Tail() + if err != nil { + return nil, err + } + info.tail = tail + infos = append(infos, info) + + 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. +// Note this function can only be used for debugging purposes. +func InspectFreezerTable(ancient string, freezerName string, tableName string, start, end int64) error { + var ( + path string + tables map[string]freezerTableConfig + ) + switch freezerName { + case chainFreezerName: + path, tables = resolveChainFreezerDir(ancient), chainFreezerTableConfigs + default: + return fmt.Errorf("unknown freezer, supported ones: %v", freezers) + } + noSnappy, exist := tables[tableName] + if !exist { + var names []string + for name := range tables { + names = append(names, name) + } + return fmt.Errorf("unknown table, supported ones: %v", names) + } + table, err := newFreezerTable(path, tableName, noSnappy, true) + if err != nil { + return err + } + table.dumpIndexStdout(start, end) + return nil +} diff --git a/core/rawdb/chain_freezer.go b/core/rawdb/chain_freezer.go new file mode 100644 index 00000000000..24b589782cb --- /dev/null +++ b/core/rawdb/chain_freezer.go @@ -0,0 +1,303 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rawdb + +import ( + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/ethdb" + "github.com/XinFinOrg/XDPoSChain/log" + "github.com/XinFinOrg/XDPoSChain/params" +) + +const ( + // freezerRecheckInterval is the frequency to check the key-value database for + // chain progression that might permit new blocks to be frozen into immutable + // storage. + freezerRecheckInterval = time.Minute + + // freezerBatchLimit is the maximum number of blocks to freeze in one batch + // before doing an fsync and deleting it from the key-value store. + freezerBatchLimit = 30000 +) + +// chainFreezer is a wrapper of freezer with additional chain freezing feature. +// The background thread will keep moving ancient chain segments from key-value +// database to flat files for saving space on live database. +type chainFreezer struct { + threshold atomic.Uint64 // Number of recent blocks not to freeze (params.FullImmutabilityThreshold apart from tests) + + *Freezer + quit chan struct{} + wg sync.WaitGroup + trigger chan chan struct{} // Manual blocking freeze trigger, test determinism +} + +// newChainFreezer initializes the freezer for ancient chain data. +func newChainFreezer(datadir string, namespace string, readonly bool) (*chainFreezer, error) { + freezer, err := NewChainFreezer(datadir, namespace, readonly) + if err != nil { + return nil, err + } + cf := chainFreezer{ + Freezer: freezer, + quit: make(chan struct{}), + trigger: make(chan chan struct{}), + } + cf.threshold.Store(params.FullImmutabilityThreshold) + return &cf, nil +} + +// Close closes the chain freezer instance and terminates the background thread. +func (f *chainFreezer) Close() error { + select { + case <-f.quit: + default: + close(f.quit) + } + f.wg.Wait() + return f.Freezer.Close() +} + +// freeze is a background thread that periodically checks the blockchain for any +// import progress and moves ancient data from the fast database into the freezer. +// +// This functionality is deliberately broken off from block importing to avoid +// incurring additional data shuffling delays on block propagation. +func (f *chainFreezer) freeze(db ethdb.KeyValueStore) { + var ( + backoff bool + triggered chan struct{} // Used in tests + nfdb = &nofreezedb{KeyValueStore: db} + ) + timer := time.NewTimer(freezerRecheckInterval) + defer timer.Stop() + + for { + select { + case <-f.quit: + log.Info("Freezer shutting down") + return + default: + } + if backoff { + // If we were doing a manual trigger, notify it + if triggered != nil { + triggered <- struct{}{} + triggered = nil + } + select { + case <-timer.C: + backoff = false + timer.Reset(freezerRecheckInterval) + case triggered = <-f.trigger: + backoff = false + case <-f.quit: + return + } + } + // Retrieve the freezing threshold. + hash := ReadHeadBlockHash(nfdb) + if hash == (common.Hash{}) { + log.Debug("Current full block hash unavailable") // new chain, empty database + backoff = true + continue + } + number := ReadHeaderNumber(nfdb, hash) + threshold := f.threshold.Load() + frozen := f.frozen.Load() + switch { + case number == nil: + log.Error("Current full block number unavailable", "hash", hash) + backoff = true + continue + + case *number < threshold: + log.Debug("Current full block not old enough", "number", *number, "hash", hash, "delay", threshold) + backoff = true + continue + + case *number-threshold <= frozen: + log.Debug("Ancient blocks frozen already", "number", *number, "hash", hash, "frozen", frozen) + backoff = true + continue + } + head := ReadHeader(nfdb, hash, *number) + if head == nil { + log.Error("Current full block unavailable", "number", *number, "hash", hash) + backoff = true + continue + } + + // Seems we have data ready to be frozen, process in usable batches + var ( + start = time.Now() + first, _ = f.Ancients() + limit = *number - threshold + ) + if limit-first > freezerBatchLimit { + limit = first + freezerBatchLimit + } + ancients, err := f.freezeRange(nfdb, first, limit) + if err != nil { + log.Error("Error in block freeze operation", "err", err) + backoff = true + continue + } + + // Batch of blocks have been frozen, flush them before wiping from leveldb + if err := f.Sync(); err != nil { + log.Crit("Failed to flush frozen tables", "err", err) + } + + // Wipe out all data from the active database + batch := db.NewBatch() + for i := 0; i < len(ancients); i++ { + // Always keep the genesis block in active database + if first+uint64(i) != 0 { + DeleteBlockWithoutNumber(batch, ancients[i], first+uint64(i)) + DeleteCanonicalHash(batch, first+uint64(i)) + } + } + if err := batch.Write(); err != nil { + log.Crit("Failed to delete frozen canonical blocks", "err", err) + } + batch.Reset() + + // Wipe out side chains also and track dangling side chains + var dangling []common.Hash + frozen = f.frozen.Load() // Needs reload after during freezeRange + for number := first; number < frozen; number++ { + // Always keep the genesis block in active database + if number != 0 { + dangling = ReadAllHashes(db, number) + for _, hash := range dangling { + log.Trace("Deleting side chain", "number", number, "hash", hash) + DeleteBlock(batch, hash, number) + } + } + } + if err := batch.Write(); err != nil { + log.Crit("Failed to delete frozen side blocks", "err", err) + } + batch.Reset() + + // Step into the future and delete and dangling side chains + if frozen > 0 { + tip := frozen + for len(dangling) > 0 { + drop := make(map[common.Hash]struct{}) + for _, hash := range dangling { + log.Debug("Dangling parent from Freezer", "number", tip-1, "hash", hash) + drop[hash] = struct{}{} + } + children := ReadAllHashes(db, tip) + for i := 0; i < len(children); i++ { + // Dig up the child and ensure it's dangling + child := ReadHeader(nfdb, children[i], tip) + if child == nil { + log.Error("Missing dangling header", "number", tip, "hash", children[i]) + continue + } + if _, ok := drop[child.ParentHash]; !ok { + children = append(children[:i], children[i+1:]...) + i-- + continue + } + // Delete all block data associated with the child + log.Debug("Deleting dangling block", "number", tip, "hash", children[i], "parent", child.ParentHash) + DeleteBlock(batch, children[i], tip) + } + dangling = children + tip++ + } + if err := batch.Write(); err != nil { + log.Crit("Failed to delete dangling side blocks", "err", err) + } + } + + // Log something friendly for the user + context := []interface{}{ + "blocks", frozen - first, "elapsed", common.PrettyDuration(time.Since(start)), "number", frozen - 1, + } + if n := len(ancients); n > 0 { + context = append(context, []interface{}{"hash", ancients[n-1]}...) + } + log.Info("Deep froze chain segment", context...) + + // Avoid database thrashing with tiny writes + if frozen-first < freezerBatchLimit { + backoff = true + } + } +} + +func (f *chainFreezer) freezeRange(nfdb *nofreezedb, number, limit uint64) (hashes []common.Hash, err error) { + hashes = make([]common.Hash, 0, limit-number) + + _, err = f.ModifyAncients(func(op ethdb.AncientWriteOp) error { + for ; number <= limit; number++ { + // Retrieve all the components of the canonical block. + hash := ReadCanonicalHash(nfdb, number) + if hash == (common.Hash{}) { + return fmt.Errorf("canonical hash missing, can't freeze block %d", number) + } + header := ReadHeaderRLP(nfdb, hash, number) + if len(header) == 0 { + return fmt.Errorf("block header missing, can't freeze block %d", number) + } + body := ReadBodyRLP(nfdb, hash, number) + if len(body) == 0 { + return fmt.Errorf("block body missing, can't freeze block %d", number) + } + receipts := ReadReceiptsRLP(nfdb, hash, number) + if len(receipts) == 0 { + return fmt.Errorf("block receipts missing, can't freeze block %d", number) + } + td := ReadTdRLP(nfdb, hash, number) + if len(td) == 0 { + return fmt.Errorf("total difficulty missing, can't freeze block %d", number) + } + + // Write to the batch. + if err := op.AppendRaw(chainFreezerHashTable, number, hash[:]); err != nil { + return fmt.Errorf("can't write hash to Freezer: %v", err) + } + if err := op.AppendRaw(chainFreezerHeaderTable, number, header); err != nil { + return fmt.Errorf("can't write header to Freezer: %v", err) + } + if err := op.AppendRaw(chainFreezerBodiesTable, number, body); err != nil { + return fmt.Errorf("can't write body to Freezer: %v", err) + } + if err := op.AppendRaw(chainFreezerReceiptTable, number, receipts); err != nil { + return fmt.Errorf("can't write receipts to Freezer: %v", err) + } + if err := op.AppendRaw(chainFreezerDifficultyTable, number, td); err != nil { + return fmt.Errorf("can't write td to Freezer: %v", err) + } + + hashes = append(hashes, hash) + } + return nil + }) + + return hashes, err +} diff --git a/core/rawdb/chain_iterator.go b/core/rawdb/chain_iterator.go new file mode 100644 index 00000000000..5e1b0ff9c56 --- /dev/null +++ b/core/rawdb/chain_iterator.go @@ -0,0 +1,311 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rawdb + +import ( + "math" + "runtime" + "sync/atomic" + "time" + + "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/common/prque" + "github.com/XinFinOrg/XDPoSChain/ethdb" + "github.com/XinFinOrg/XDPoSChain/log" + "github.com/XinFinOrg/XDPoSChain/rlp" + "golang.org/x/crypto/sha3" +) + +// InitDatabaseFromFreezer reinitializes an empty database from a previous batch +// of frozen ancient blocks. The method iterates over all the frozen blocks and +// injects into the database the block hash->number mappings. +func InitDatabaseFromFreezer(db ethdb.Database) { + // If we can't access the freezer or it's empty, abort + frozen, err := db.Ancients() + if err != nil || frozen == 0 { + return + } + var ( + batch = db.NewBatch() + start = time.Now() + logged = start.Add(-7 * time.Second) // Unindex during import is fast, don't double log + hash common.Hash + ) + for i := uint64(0); i < frozen; { + // We read 100K hashes at a time, for a total of 3.2M + count := uint64(100_000) + if i+count > frozen { + count = frozen - i + } + data, err := db.AncientRange(chainFreezerHashTable, i, count, 32*count) + if err != nil { + log.Crit("Failed to init database from freezer", "err", err) + } + for j, h := range data { + number := i + uint64(j) + hash = common.BytesToHash(h) + WriteHeaderNumber(batch, hash, number) + // If enough data was accumulated in memory or we're at the last block, dump to disk + if batch.ValueSize() > ethdb.IdealBatchSize { + if err := batch.Write(); err != nil { + log.Crit("Failed to write data to db", "err", err) + } + batch.Reset() + } + } + i += uint64(len(data)) + // If we've spent too much time already, notify the user of what we're doing + if time.Since(logged) > 8*time.Second { + log.Info("Initializing database from freezer", "total", frozen, "number", i, "hash", hash, "elapsed", common.PrettyDuration(time.Since(start))) + logged = time.Now() + } + } + if err := batch.Write(); err != nil { + log.Crit("Failed to write data to db", "err", err) + } + batch.Reset() + + WriteHeadHeaderHash(db, hash) + WriteHeadFastBlockHash(db, hash) + log.Info("Initialized database from freezer", "blocks", frozen, "elapsed", common.PrettyDuration(time.Since(start))) +} + +type blockTxHashes struct { + number uint64 + hashes []common.Hash +} + +// iterateTransactions iterates over all transactions in the (canon) block +// number(s) given, and yields the hashes on a channel +func iterateTransactions(db ethdb.Database, from uint64, to uint64, reverse bool) (chan *blockTxHashes, chan struct{}) { + // One thread sequentially reads data from db + type numberRlp struct { + number uint64 + rlp rlp.RawValue + } + if to == from { + return nil, nil + } + threads := to - from + if cpus := runtime.NumCPU(); threads > uint64(cpus) { + threads = uint64(cpus) + } + var ( + rlpCh = make(chan *numberRlp, threads*2) // we send raw rlp over this channel + hashesCh = make(chan *blockTxHashes, threads*2) // send hashes over hashesCh + abortCh = make(chan struct{}) + ) + // lookup runs in one instance + lookup := func() { + n, end := from, to + if reverse { + n, end = to-1, from-1 + } + defer close(rlpCh) + for n != end { + data := ReadCanonicalBodyRLP(db, n) + // Feed the block to the aggregator, or abort on interrupt + select { + case rlpCh <- &numberRlp{n, data}: + case <-abortCh: + return + } + if reverse { + n-- + } else { + n++ + } + } + } + // process runs in parallel + var nThreadsAlive atomic.Int32 + nThreadsAlive.Store(int32(threads)) + process := func() { + defer func() { + // Last processor closes the result channel + if nThreadsAlive.Add(-1) == 0 { + close(hashesCh) + } + }() + + var hasher = sha3.NewLegacyKeccak256() + for data := range rlpCh { + it, err := rlp.NewListIterator(data.rlp) + if err != nil { + log.Warn("tx iteration error", "error", err) + return + } + it.Next() + txs := it.Value() + txIt, err := rlp.NewListIterator(txs) + if err != nil { + log.Warn("tx iteration error", "error", err) + return + } + var hashes []common.Hash + for txIt.Next() { + if err := txIt.Err(); err != nil { + log.Warn("tx iteration error", "error", err) + return + } + var txHash common.Hash + hasher.Reset() + hasher.Write(txIt.Value()) + hasher.Sum(txHash[:0]) + hashes = append(hashes, txHash) + } + result := &blockTxHashes{ + hashes: hashes, + number: data.number, + } + // Feed the block to the aggregator, or abort on interrupt + select { + case hashesCh <- result: + case <-abortCh: + return + } + } + } + go lookup() // start the sequential db accessor + for i := 0; i < int(threads); i++ { + go process() + } + return hashesCh, abortCh +} + +// IndexTransactions creates txlookup indices of the specified block range. +// +// This function iterates canonical chain in reverse order, it has one main advantage: +// We can write tx index tail flag periodically even without the whole indexing +// procedure is finished. So that we can resume indexing procedure next time quickly. +func IndexTransactions(db ethdb.Database, from uint64, to uint64) { + // short circuit for invalid range + if from >= to { + return + } + var ( + hashesCh, abortCh = iterateTransactions(db, from, to, true) + batch = db.NewBatch() + start = time.Now() + logged = start.Add(-7 * time.Second) + // Since we iterate in reverse, we expect the first number to come + // in to be [to-1]. Therefore, setting lastNum to means that the + // prqueue gap-evaluation will work correctly + lastNum = to + queue = prque.New[int64, *blockTxHashes](nil) + // for stats reporting + blocks, txs = 0, 0 + ) + defer close(abortCh) + + for chanDelivery := range hashesCh { + // Push the delivery into the queue and process contiguous ranges. + // Since we iterate in reverse, so lower numbers have lower prio, and + // we can use the number directly as prio marker + queue.Push(chanDelivery, int64(chanDelivery.number)) + for !queue.Empty() { + // If the next available item is gapped, return + if _, priority := queue.Peek(); priority != int64(lastNum-1) { + break + } + // Next block available, pop it off and index it + delivery := queue.PopItem() + lastNum = delivery.number + WriteTxLookupEntriesByHash(batch, delivery.number, delivery.hashes) + blocks++ + txs += len(delivery.hashes) + // If enough data was accumulated in memory or we're at the last block, dump to disk + if batch.ValueSize() > ethdb.IdealBatchSize { + // Also write the tail there + WriteTxIndexTail(batch, lastNum) + if err := batch.Write(); err != nil { + log.Crit("Failed writing batch to db", "error", err) + return + } + batch.Reset() + } + // If we've spent too much time already, notify the user of what we're doing + if time.Since(logged) > 8*time.Second { + log.Info("Indexing transactions", "blocks", blocks, "txs", txs, "tail", lastNum, "total", to-from, "elapsed", common.PrettyDuration(time.Since(start))) + logged = time.Now() + } + } + } + if lastNum < to { + WriteTxIndexTail(batch, lastNum) + // No need to write the batch if we never entered the loop above... + if err := batch.Write(); err != nil { + log.Crit("Failed writing batch to db", "error", err) + return + } + } + log.Info("Indexed transactions", "blocks", blocks, "txs", txs, "tail", lastNum, "elapsed", common.PrettyDuration(time.Since(start))) +} + +// UnindexTransactions removes txlookup indices of the specified block range. +func UnindexTransactions(db ethdb.Database, from uint64, to uint64) { + // short circuit for invalid range + if from >= to { + return + } + // Write flag first and then unindex the transaction indices. Some indices + // will be left in the database if crash happens but it's fine. + WriteTxIndexTail(db, to) + // If only one block is unindexed, do it directly + //if from+1 == to { + // data := ReadCanonicalBodyRLP(db, uint64(from)) + // DeleteTxLookupEntries(db, ReadBlock(db, ReadCanonicalHash(db, from), from)) + // log.Info("Unindexed transactions", "blocks", 1, "tail", to) + // return + //} + // TODO @holiman, add this back (if we want it) + var ( + hashesCh, abortCh = iterateTransactions(db, from, to, false) + batch = db.NewBatch() + start = time.Now() + logged = start.Add(-7 * time.Second) + ) + defer close(abortCh) + // Otherwise spin up the concurrent iterator and unindexer + blocks, txs := 0, 0 + for delivery := range hashesCh { + DeleteTxLookupEntriesByHash(batch, delivery.hashes) + txs += len(delivery.hashes) + blocks++ + + // If enough data was accumulated in memory or we're at the last block, dump to disk + // A batch counts the size of deletion as '1', so we need to flush more + // often than that. + if blocks%1000 == 0 { + if err := batch.Write(); err != nil { + log.Crit("Failed writing batch to db", "error", err) + return + } + batch.Reset() + } + // If we've spent too much time already, notify the user of what we're doing + if time.Since(logged) > 8*time.Second { + log.Info("Unindexing transactions", "blocks", "txs", txs, int64(math.Abs(float64(delivery.number-from))), "total", to-from, "elapsed", common.PrettyDuration(time.Since(start))) + logged = time.Now() + } + } + if err := batch.Write(); err != nil { + log.Crit("Failed writing batch to db", "error", err) + return + } + log.Info("Unindexed transactions", "blocks", blocks, "txs", txs, "tail", to, "elapsed", common.PrettyDuration(time.Since(start))) +} diff --git a/core/rawdb/chain_iterator_test.go b/core/rawdb/chain_iterator_test.go new file mode 100644 index 00000000000..179f9f3655d --- /dev/null +++ b/core/rawdb/chain_iterator_test.go @@ -0,0 +1,82 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rawdb + +import ( + "math/big" + "reflect" + "sort" + "testing" + + "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/core/types" +) + +func TestChainIterator(t *testing.T) { + // Construct test chain db + chainDb := NewMemoryDatabase() + + var block *types.Block + var txs []*types.Transaction + for i := uint64(0); i <= 10; i++ { + if i == 0 { + block = types.NewBlock(&types.Header{Number: big.NewInt(int64(i))}, nil, nil, nil, newHasher()) // Empty genesis block + } else { + tx := types.NewTransaction(i, common.BytesToAddress([]byte{0x11}), big.NewInt(111), 1111, big.NewInt(11111), []byte{0x11, 0x11, 0x11}) + txs = append(txs, tx) + block = types.NewBlock(&types.Header{Number: big.NewInt(int64(i))}, []*types.Transaction{tx}, nil, nil, newHasher()) + } + WriteBlock(chainDb, block) + WriteCanonicalHash(chainDb, block.Hash(), block.NumberU64()) + } + + var cases = []struct { + from, to uint64 + reverse bool + expect []int + }{ + {0, 11, true, []int{10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}}, + {0, 0, true, nil}, + {0, 5, true, []int{4, 3, 2, 1, 0}}, + {10, 11, true, []int{10}}, + {0, 11, false, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}}, + {0, 0, false, nil}, + {10, 11, false, []int{10}}, + } + for i, c := range cases { + var numbers []int + hashCh, _ := iterateTransactions(chainDb, c.from, c.to, c.reverse) + if hashCh != nil { + for h := range hashCh { + numbers = append(numbers, int(h.number)) + if len(h.hashes) > 0 { + if got, exp := h.hashes[0], txs[h.number-1].Hash(); got != exp { + t.Fatalf("hash wrong, got %x exp %x", got, exp) + } + } + } + } + if !c.reverse { + sort.Ints(numbers) + } else { + sort.Sort(sort.Reverse(sort.IntSlice(numbers))) + } + if !reflect.DeepEqual(numbers, c.expect) { + t.Fatalf("Case %d failed, visit element mismatch, want %v, got %v", i, c.expect, numbers) + } + } +} diff --git a/core/rawdb/database.go b/core/rawdb/database.go index cbacd19824e..2e41cc9f8db 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -18,8 +18,11 @@ package rawdb import ( "bytes" + "errors" "fmt" "os" + "path" + "strings" "time" "github.com/XinFinOrg/XDPoSChain/common" @@ -32,10 +35,16 @@ import ( // freezerdb is a database wrapper that enabled freezer data retrievals. type freezerdb struct { + ancientRoot string ethdb.KeyValueStore ethdb.AncientStore } +// AncientDatadir returns the path of root ancient directory. +func (frdb *freezerdb) AncientDatadir() (string, error) { + return frdb.ancientRoot, nil +} + // Close implements io.Closer, closing both the fast key-value store as well as // the slow ancient tables. func (frdb *freezerdb) Close() error { @@ -52,6 +61,26 @@ func (frdb *freezerdb) Close() error { return nil } +// Freeze is a helper method used for external testing to trigger and block until +// a freeze cycle completes, without having to sleep for a minute to trigger the +// automatic background run. +func (frdb *freezerdb) Freeze(threshold uint64) error { + if frdb.AncientStore.(*chainFreezer).readonly { + return errReadOnly + } + // Set the freezer threshold to a temporary value + defer func(old uint64) { + frdb.AncientStore.(*chainFreezer).threshold.Store(old) + }(frdb.AncientStore.(*chainFreezer).threshold.Load()) + frdb.AncientStore.(*chainFreezer).threshold.Store(threshold) + + // Trigger a freeze cycle and block until it's done + trigger := make(chan struct{}, 1) + frdb.AncientStore.(*chainFreezer).trigger <- trigger + <-trigger + return nil +} + // nofreezedb is a database wrapper that disables freezer data retrievals. type nofreezedb struct { ethdb.KeyValueStore @@ -67,23 +96,38 @@ func (db *nofreezedb) Ancient(kind string, number uint64) ([]byte, error) { return nil, errNotSupported } +// AncientRange returns an error as we don't have a backing chain freezer. +func (db *nofreezedb) AncientRange(kind string, start, max, maxByteSize uint64) ([][]byte, error) { + return nil, errNotSupported +} + // Ancients returns an error as we don't have a backing chain freezer. func (db *nofreezedb) Ancients() (uint64, error) { return 0, errNotSupported } +// Tail returns an error as we don't have a backing chain freezer. +func (db *nofreezedb) Tail() (uint64, error) { + return 0, errNotSupported +} + // AncientSize returns an error as we don't have a backing chain freezer. func (db *nofreezedb) AncientSize(kind string) (uint64, error) { return 0, errNotSupported } -// AppendAncient returns an error as we don't have a backing chain freezer. -func (db *nofreezedb) AppendAncient(number uint64, hash, header, body, receipts, td []byte) error { +// ModifyAncients is not supported. +func (db *nofreezedb) ModifyAncients(func(ethdb.AncientWriteOp) error) (int64, error) { + return 0, errNotSupported +} + +// TruncateHead returns an error as we don't have a backing chain freezer. +func (db *nofreezedb) TruncateHead(items uint64) error { return errNotSupported } -// TruncateAncients returns an error as we don't have a backing chain freezer. -func (db *nofreezedb) TruncateAncients(items uint64) error { +// TruncateTail returns an error as we don't have a backing chain freezer. +func (db *nofreezedb) TruncateTail(items uint64) error { return errNotSupported } @@ -92,12 +136,170 @@ func (db *nofreezedb) Sync() error { return errNotSupported } +func (db *nofreezedb) ReadAncients(fn func(reader ethdb.AncientReaderOp) error) (err error) { + // Unlike other ancient-related methods, this method does not return + // errNotSupported when invoked. + // The reason for this is that the caller might want to do several things: + // 1. Check if something is in freezer, + // 2. If not, check leveldb. + // + // This will work, since the ancient-checks inside 'fn' will return errors, + // and the leveldb work will continue. + // + // If we instead were to return errNotSupported here, then the caller would + // have to explicitly check for that, having an extra clause to do the + // non-ancient operations. + return fn(db) +} + +// MigrateTable processes the entries in a given table in sequence +// converting them to a new format if they're of an old format. +func (db *nofreezedb) MigrateTable(kind string, convert convertLegacyFn) error { + return errNotSupported +} + +// AncientDatadir returns an error as we don't have a backing chain freezer. +func (db *nofreezedb) AncientDatadir() (string, error) { + return "", errNotSupported +} + // 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. func NewDatabase(db ethdb.KeyValueStore) ethdb.Database { - return &nofreezedb{ - KeyValueStore: db, + return &nofreezedb{KeyValueStore: db} +} + +// resolveChainFreezerDir is a helper function which resolves the absolute path +// of chain freezer by considering backward compatibility. +func resolveChainFreezerDir(ancient string) string { + // Check if the chain freezer is already present in the specified + // sub folder, if not then two possibilities: + // - chain freezer is not initialized + // - chain freezer exists in legacy location (root ancient folder) + freezer := path.Join(ancient, chainFreezerName) + if !common.FileExist(freezer) { + if !common.FileExist(ancient) { + // The entire ancient store is not initialized, still use the sub + // folder for initialization. + } else { + // Ancient root is already initialized, then we hold the assumption + // that chain freezer is also initialized and located in root folder. + // In this case fallback to legacy location. + freezer = ancient + log.Info("Found legacy ancient chain path", "location", ancient) + } + } + return freezer +} + +// NewDatabaseWithFreezer creates a high level database on top of a given key- +// value data store with a freezer moving immutable chain segments into cold +// storage. The passed ancient indicates the path of root ancient directory +// where the chain freezer can be opened. +func NewDatabaseWithFreezer(db ethdb.KeyValueStore, ancient string, namespace string, readonly bool) (ethdb.Database, error) { + // Create the idle freezer instance. If the given ancient directory is empty, + // in-memory chain freezer is used (e.g. dev mode); otherwise the regular + // file-based freezer is created. + chainFreezerDir := ancient + if chainFreezerDir != "" { + chainFreezerDir = resolveChainFreezerDir(chainFreezerDir) + } + frdb, err := newChainFreezer(chainFreezerDir, namespace, readonly) + if err != nil { + printChainMetadata(db) + return nil, err } + // Since the freezer can be stored separately from the user's key-value database, + // there's a fairly high probability that the user requests invalid combinations + // of the freezer and database. Ensure that we don't shoot ourselves in the foot + // by serving up conflicting data, leading to both datastores getting corrupted. + // + // - If both the freezer and key-value store is empty (no genesis), we just + // initialized a new empty freezer, so everything's fine. + // - If the key-value store is empty, but the freezer is not, we need to make + // sure the user's genesis matches the freezer. That will be checked in the + // blockchain, since we don't have the genesis block here (nor should we at + // this point care, the key-value/freezer combo is valid). + // - If neither the key-value store nor the freezer is empty, cross validate + // the genesis hashes to make sure they are compatible. If they are, also + // ensure that there's no gap between the freezer and subsequently leveldb. + // - If the key-value store is not empty, but the freezer is we might just be + // upgrading to the freezer release, or we might have had a small chain and + // not frozen anything yet. Ensure that no blocks are missing yet from the + // key-value store, since that would mean we already had an old freezer. + + // If the genesis hash is empty, we have a new key-value store, so nothing to + // validate in this method. If, however, the genesis hash is not nil, compare + // it to the freezer content. + if kvgenesis, _ := db.Get(headerHashKey(0)); len(kvgenesis) > 0 { + if frozen, _ := frdb.Ancients(); frozen > 0 { + // If the freezer already contains something, ensure that the genesis blocks + // match, otherwise we might mix up freezers across chains and destroy both + // the freezer and the key-value store. + frgenesis, err := frdb.Ancient(chainFreezerHashTable, 0) + if err != nil { + printChainMetadata(db) + return nil, fmt.Errorf("failed to retrieve genesis from ancient %v", err) + } else if !bytes.Equal(kvgenesis, frgenesis) { + printChainMetadata(db) + return nil, fmt.Errorf("genesis mismatch: %#x (leveldb) != %#x (ancients)", kvgenesis, frgenesis) + } + // Key-value store and freezer belong to the same network. Ensure that they + // are contiguous, otherwise we might end up with a non-functional freezer. + if kvhash, _ := db.Get(headerHashKey(frozen)); len(kvhash) == 0 { + // Subsequent header after the freezer limit is missing from the database. + // Reject startup if the database has a more recent head. + if head := *ReadHeaderNumber(db, ReadHeadHeaderHash(db)); head > frozen-1 { + // Find the smallest block stored in the key-value store + // in range of [frozen, head] + var number uint64 + for number = frozen; number <= head; number++ { + if present, _ := db.Has(headerHashKey(number)); present { + break + } + } + // We are about to exit on error. Print database metdata beore exiting + printChainMetadata(db) + return nil, fmt.Errorf("gap in the chain between ancients [0 - #%d] and leveldb [#%d - #%d] ", + frozen-1, number, head) + } + // Database contains only older data than the freezer, this happens if the + // state was wiped and reinited from an existing freezer. + } + // Otherwise, key-value store continues where the freezer left off, all is fine. + // We might have duplicate blocks (crash after freezer write but before key-value + // store deletion, but that's fine). + } else { + // If the freezer is empty, ensure nothing was moved yet from the key-value + // store, otherwise we'll end up missing data. We check block #1 to decide + // if we froze anything previously or not, but do take care of databases with + // only the genesis block. + if ReadHeadHeaderHash(db) != common.BytesToHash(kvgenesis) { + // Key-value store contains more data than the genesis block, make sure we + // didn't freeze anything yet. + if kvblob, _ := db.Get(headerHashKey(1)); len(kvblob) == 0 { + printChainMetadata(db) + return nil, errors.New("ancient chain segments already extracted, please set --datadir.ancient to the correct path") + } + // Block #1 is still in the database, we're allowed to init a new freezer + } + // Otherwise, the head header is still the genesis, we're allowed to init a new + // freezer. + } + } + // Freezer is consistent with the key-value database, permit combining the two + if !frdb.readonly { + frdb.wg.Add(1) + go func() { + frdb.freeze(db) + frdb.wg.Done() + }() + } + return &freezerdb{ + ancientRoot: ancient, + KeyValueStore: db, + AncientStore: frdb, + }, nil } // NewMemoryDatabase creates an ephemeral in-memory key-value database without a @@ -106,9 +308,9 @@ func NewMemoryDatabase() ethdb.Database { return NewDatabase(memorydb.New()) } -// NewMemoryDatabaseWithCap creates an ephemeral in-memory key-value database with -// an initial starting capacity, but without a freezer moving immutable chain -// segments into cold storage. +// NewMemoryDatabaseWithCap creates an ephemeral in-memory key-value database +// with an initial starting capacity, but without a freezer moving immutable +// chain segments into cold storage. func NewMemoryDatabaseWithCap(size int) ethdb.Database { return NewDatabase(memorydb.NewWithCap(size)) } @@ -123,6 +325,53 @@ func NewLevelDBDatabase(file string, cache int, handles int, namespace string, r return NewDatabase(db), nil } +// NewLevelDBDatabaseWithFreezer creates a persistent key-value database with a +// freezer moving immutable chain segments into cold storage. The passed ancient +// indicates the path of root ancient directory where the chain freezer can be +// opened. +func NewLevelDBDatabaseWithFreezer(file string, cache int, handles int, ancient string, namespace string, readonly bool) (ethdb.Database, error) { + kvdb, err := leveldb.New(file, cache, handles, namespace, readonly) + if err != nil { + return nil, err + } + frdb, err := NewDatabaseWithFreezer(kvdb, ancient, namespace, readonly) + if err != nil { + kvdb.Close() + return nil, err + } + return frdb, nil +} + +type counter uint64 + +func (c counter) String() string { + return fmt.Sprintf("%d", c) +} + +func (c counter) Percentage(current uint64) string { + return fmt.Sprintf("%d", current*100/uint64(c)) +} + +// stat stores sizes and count for a parameter +type stat struct { + size common.StorageSize + count counter +} + +// Add size to the stat and increase the counter by 1 +func (s *stat) Add(size common.StorageSize) { + s.size += size + s.count++ +} + +func (s *stat) Size() string { + return s.size.String() +} + +func (s *stat) Count() string { + return s.count.String() +} + // InspectDatabase traverses the entire database and checks the size // of all different categories of data. func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { @@ -135,34 +384,26 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { logged = time.Now() // Key-value store statistics - total common.StorageSize - headerSize common.StorageSize - bodySize common.StorageSize - receiptSize common.StorageSize - tdSize common.StorageSize - numHashPairing common.StorageSize - hashNumPairing common.StorageSize - trieSize common.StorageSize - codeSize common.StorageSize - txlookupSize common.StorageSize - preimageSize common.StorageSize - bloomBitsSize common.StorageSize - cliqueSnapsSize common.StorageSize - - // Ancient store statistics - ancientHeaders common.StorageSize - ancientBodies common.StorageSize - ancientReceipts common.StorageSize - ancientHashes common.StorageSize - ancientTds common.StorageSize - - // Les statistic - chtTrieNodes common.StorageSize - bloomTrieNodes common.StorageSize + headers stat + bodies stat + receipts stat + tds stat + numHashPairings stat + hashNumPairings stat + tries stat + codes stat + txLookups stat + accountSnaps stat + storageSnaps stat + preimages stat + bloomBits stat // Meta- and unaccounted data - metadata common.StorageSize - unaccounted common.StorageSize + metadata stat + unaccounted stat + + // Totals + total common.StorageSize ) // Inspect key-value database first. for it.Next() { @@ -172,92 +413,127 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { ) total += size switch { - case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerTDSuffix): - tdSize += size - case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerHashSuffix): - numHashPairing += size case bytes.HasPrefix(key, headerPrefix) && len(key) == (len(headerPrefix)+8+common.HashLength): - headerSize += size - case bytes.HasPrefix(key, headerNumberPrefix) && len(key) == (len(headerNumberPrefix)+common.HashLength): - hashNumPairing += size + headers.Add(size) case bytes.HasPrefix(key, blockBodyPrefix) && len(key) == (len(blockBodyPrefix)+8+common.HashLength): - bodySize += size + bodies.Add(size) case bytes.HasPrefix(key, blockReceiptsPrefix) && len(key) == (len(blockReceiptsPrefix)+8+common.HashLength): - receiptSize += size + receipts.Add(size) + case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerTDSuffix): + tds.Add(size) + case bytes.HasPrefix(key, headerPrefix) && bytes.HasSuffix(key, headerHashSuffix): + numHashPairings.Add(size) + case bytes.HasPrefix(key, headerNumberPrefix) && len(key) == (len(headerNumberPrefix)+common.HashLength): + hashNumPairings.Add(size) + case len(key) == common.HashLength: + tries.Add(size) + case bytes.HasPrefix(key, CodePrefix) && len(key) == len(CodePrefix)+common.HashLength: + codes.Add(size) case bytes.HasPrefix(key, txLookupPrefix) && len(key) == (len(txLookupPrefix)+common.HashLength): - txlookupSize += size + txLookups.Add(size) case bytes.HasPrefix(key, preimagePrefix) && len(key) == (len(preimagePrefix)+common.HashLength): - preimageSize += size + preimages.Add(size) + case bytes.HasPrefix(key, configPrefix) && len(key) == (len(configPrefix)+common.HashLength): + metadata.Add(size) case bytes.HasPrefix(key, bloomBitsPrefix) && len(key) == (len(bloomBitsPrefix)+10+common.HashLength): - bloomBitsSize += size - case bytes.HasPrefix(key, []byte("clique-")) && len(key) == 7+common.HashLength: - cliqueSnapsSize += size - case bytes.HasPrefix(key, []byte("cht-")) && len(key) == 4+common.HashLength: - chtTrieNodes += size - case bytes.HasPrefix(key, []byte("blt-")) && len(key) == 4+common.HashLength: - bloomTrieNodes += size - case bytes.HasPrefix(key, codePrefix) && len(key) == len(codePrefix)+common.HashLength: - codeSize += size - case len(key) == common.HashLength: - trieSize += size + bloomBits.Add(size) + case bytes.HasPrefix(key, BloomBitsIndexPrefix): + bloomBits.Add(size) default: var accounted bool - for _, meta := range [][]byte{databaseVersionKey, headHeaderKey, headBlockKey, headFastBlockKey, fastTrieProgressKey} { + for _, meta := range [][]byte{ + databaseVersionKey, headHeaderKey, headBlockKey, headFastBlockKey, + lastPivotKey, fastTrieProgressKey, txIndexTailKey, fastTxLookupLimitKey, + uncleanShutdownKey, badBlockKey, + } { if bytes.Equal(key, meta) { - metadata += size + metadata.Add(size) accounted = true break } } if !accounted { - unaccounted += size + unaccounted.Add(size) } } - count += 1 + count++ if count%1000 == 0 && time.Since(logged) > 8*time.Second { log.Info("Inspecting database", "count", count, "elapsed", common.PrettyDuration(time.Since(start))) logged = time.Now() } } - // Inspect append-only file store then. - ancients := []*common.StorageSize{&ancientHeaders, &ancientBodies, &ancientReceipts, &ancientHashes, &ancientTds} - for i, category := range []string{freezerHeaderTable, freezerBodiesTable, freezerReceiptTable, freezerHashTable, freezerDifficultyTable} { - if size, err := db.AncientSize(category); err == nil { - *ancients[i] += common.StorageSize(size) - total += common.StorageSize(size) - } - } - // Display the database statistic. + // Display the database statistic of key-value store. stats := [][]string{ - {"Key-Value store", "Headers", headerSize.String()}, - {"Key-Value store", "Bodies", bodySize.String()}, - {"Key-Value store", "Receipts", receiptSize.String()}, - {"Key-Value store", "Difficulties", tdSize.String()}, - {"Key-Value store", "Block number->hash", numHashPairing.String()}, - {"Key-Value store", "Block hash->number", hashNumPairing.String()}, - {"Key-Value store", "Transaction index", txlookupSize.String()}, - {"Key-Value store", "Bloombit index", bloomBitsSize.String()}, - {"Key-Value store", "Contract codes", codeSize.String()}, - {"Key-Value store", "Trie nodes", trieSize.String()}, - {"Key-Value store", "Trie preimages", preimageSize.String()}, - {"Key-Value store", "Clique snapshots", cliqueSnapsSize.String()}, - {"Key-Value store", "Singleton metadata", metadata.String()}, - {"Ancient store", "Headers", ancientHeaders.String()}, - {"Ancient store", "Bodies", ancientBodies.String()}, - {"Ancient store", "Receipts", ancientReceipts.String()}, - {"Ancient store", "Difficulties", ancientTds.String()}, - {"Ancient store", "Block number->hash", ancientHashes.String()}, - {"Light client", "CHT trie nodes", chtTrieNodes.String()}, - {"Light client", "Bloom trie nodes", bloomTrieNodes.String()}, + {"Key-Value store", "Headers", headers.Size(), headers.Count()}, + {"Key-Value store", "Bodies", bodies.Size(), bodies.Count()}, + {"Key-Value store", "Receipt lists", receipts.Size(), receipts.Count()}, + {"Key-Value store", "Difficulties", tds.Size(), tds.Count()}, + {"Key-Value store", "Block number->hash", numHashPairings.Size(), numHashPairings.Count()}, + {"Key-Value store", "Block hash->number", hashNumPairings.Size(), hashNumPairings.Count()}, + {"Key-Value store", "Transaction index", txLookups.Size(), txLookups.Count()}, + {"Key-Value store", "Bloombit index", bloomBits.Size(), bloomBits.Count()}, + {"Key-Value store", "Contract codes", codes.Size(), codes.Count()}, + {"Key-Value store", "Trie nodes", tries.Size(), tries.Count()}, + {"Key-Value store", "Trie preimages", preimages.Size(), preimages.Count()}, + {"Key-Value store", "Account snapshot", accountSnaps.Size(), accountSnaps.Count()}, + {"Key-Value store", "Storage snapshot", storageSnaps.Size(), storageSnaps.Count()}, + {"Key-Value store", "Singleton metadata", metadata.Size(), metadata.Count()}, + } + // Inspect all registered append-only file store then. + ancients, err := inspectFreezers(db) + if err != nil { + return err + } + for _, ancient := range ancients { + for _, table := range ancient.sizes { + stats = append(stats, []string{ + fmt.Sprintf("Ancient store (%s)", 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"}) - table.SetFooter([]string{"", "Total", total.String()}) + table.SetHeader([]string{"Database", "Category", "Size", "Items"}) + table.SetFooter([]string{"", "Total", total.String(), " "}) table.AppendBulk(stats) table.Render() - if unaccounted > 0 { - log.Error("Database contains unaccounted data", "size", unaccounted) + if unaccounted.size > 0 { + log.Error("Database contains unaccounted data", "size", unaccounted.size, "count", unaccounted.count) } return nil } + +// printChainMetadata prints out chain metadata to stderr. +func printChainMetadata(db ethdb.KeyValueStore) { + fmt.Fprintf(os.Stderr, "Chain metadata\n") + for _, v := range ReadChainMetadata(db) { + fmt.Fprintf(os.Stderr, " %s\n", strings.Join(v, ": ")) + } + fmt.Fprintf(os.Stderr, "\n\n") +} + +// ReadChainMetadata returns a set of key/value pairs that contains informatin +// about the database chain status. This can be used for diagnostic purposes +// when investigating the state of the node. +func ReadChainMetadata(db ethdb.KeyValueStore) [][]string { + pp := func(val *uint64) string { + if val == nil { + return "" + } + return fmt.Sprintf("%d (%#x)", *val, *val) + } + data := [][]string{ + {"databaseVersion", pp(ReadDatabaseVersion(db))}, + {"headBlockHash", fmt.Sprintf("%v", ReadHeadBlockHash(db))}, + {"headFastBlockHash", fmt.Sprintf("%v", ReadHeadFastBlockHash(db))}, + {"headHeaderHash", fmt.Sprintf("%v", ReadHeadHeaderHash(db))}, + {"lastPivotNumber", pp(ReadLastPivotNumber(db))}, + {"txIndexTail", pp(ReadTxIndexTail(db))}, + {"fastTxLookupLimit", pp(ReadFastTxLookupLimit(db))}, + } + return data +} diff --git a/core/rawdb/freezer.go b/core/rawdb/freezer.go new file mode 100644 index 00000000000..dc394a7ca5c --- /dev/null +++ b/core/rawdb/freezer.go @@ -0,0 +1,539 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rawdb + +import ( + "errors" + "fmt" + "math" + "os" + "path/filepath" + "sync" + "sync/atomic" + "time" + + "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/ethdb" + "github.com/XinFinOrg/XDPoSChain/log" + "github.com/XinFinOrg/XDPoSChain/metrics" + "github.com/gofrs/flock" +) + +var ( + // errReadOnly is returned if the freezer is opened in read only mode. All the + // mutations are disallowed. + errReadOnly = errors.New("read only") + + // errUnknownTable is returned if the user attempts to read from a table that is + // not tracked by the freezer. + errUnknownTable = errors.New("unknown table") + + // errOutOrderInsertion is returned if the user attempts to inject out-of-order + // binary blobs into the freezer. + errOutOrderInsertion = errors.New("the append operation is out-order") + + // errSymlinkDatadir is returned if the ancient directory specified by user + // is a symbolic link. + errSymlinkDatadir = errors.New("symbolic link datadir is not supported") +) + +// freezerTableSize defines the maximum size of freezer data files. +const freezerTableSize = 2 * 1000 * 1000 * 1000 + +// Freezer is an append-only database to store immutable ordered data into +// flat files: +// +// - The append-only nature ensures that disk writes are minimized. +// - The in-order data ensures that disk reads are always optimized. +type Freezer struct { + datadir string + frozen atomic.Uint64 // Number of blocks already frozen + tail atomic.Uint64 // Number of the first stored item in the freezer + + // This lock synchronizes writers and the truncate operation, as well as + // the "atomic" (batched) read operations. + writeLock sync.RWMutex + writeBatch *freezerBatch + + readonly bool + tables map[string]*freezerTable // Data tables for storing everything + instanceLock *flock.Flock // File-system lock to prevent double opens + closeOnce sync.Once +} + +// NewChainFreezer is a small utility method around NewFreezer that sets the +// default parameters for the chain storage. +func NewChainFreezer(datadir string, namespace string, readonly bool) (*Freezer, error) { + return NewFreezer(datadir, namespace, readonly, freezerTableSize, chainFreezerTableConfigs) +} + +// NewFreezer creates a freezer instance for maintaining immutable ordered +// data according to the given parameters. +// +// The 'tables' argument defines the data tables. If the value of a map +// entry is true, snappy compression is disabled for the table. +func NewFreezer(datadir string, namespace string, readonly bool, maxTableSize uint32, tables map[string]freezerTableConfig) (*Freezer, error) { + // Create the initial freezer object + var ( + readMeter = metrics.NewRegisteredMeter(namespace+"ancient/read", nil) + writeMeter = metrics.NewRegisteredMeter(namespace+"ancient/write", nil) + sizeGauge = metrics.NewRegisteredGauge(namespace+"ancient/size", nil) + ) + // Ensure the datadir is not a symbolic link if it exists. + if info, err := os.Lstat(datadir); !os.IsNotExist(err) { + if info == nil { + log.Warn("Could not Lstat the database", "path", datadir) + return nil, errors.New("lstat failed") + } + if info.Mode()&os.ModeSymlink != 0 { + log.Warn("Symbolic link ancient database is not supported", "path", datadir) + return nil, errSymlinkDatadir + } + } + flockFile := filepath.Join(datadir, "FLOCK") + if err := os.MkdirAll(filepath.Dir(flockFile), 0755); err != nil { + return nil, err + } + // Leveldb uses LOCK as the filelock filename. To prevent the + // name collision, we use FLOCK as the lock name. + lock := flock.New(flockFile) + tryLock := lock.TryLock + if readonly { + tryLock = lock.TryRLock + } + if locked, err := tryLock(); err != nil { + return nil, err + } else if !locked { + return nil, errors.New("locking failed") + } + // Open all the supported data tables + freezer := &Freezer{ + datadir: datadir, + readonly: readonly, + tables: make(map[string]*freezerTable), + instanceLock: lock, + } + + // Create the tables. + for name, config := range tables { + table, err := newTable(datadir, name, readMeter, writeMeter, sizeGauge, maxTableSize, config, readonly) + if err != nil { + for _, table := range freezer.tables { + table.Close() + } + lock.Unlock() + return nil, err + } + freezer.tables[name] = table + } + var err error + if freezer.readonly { + // In readonly mode only validate, don't truncate. + // validate also sets `freezer.frozen`. + err = freezer.validate() + } else { + // Truncate all tables to common length. + err = freezer.repair() + } + if err != nil { + for _, table := range freezer.tables { + table.Close() + } + lock.Unlock() + return nil, err + } + + // Create the write batch. + freezer.writeBatch = newFreezerBatch(freezer) + + log.Info("Opened ancient database", "database", datadir, "readonly", readonly) + return freezer, nil +} + +// Close terminates the chain freezer, closing all the data files. +func (f *Freezer) Close() error { + f.writeLock.Lock() + defer f.writeLock.Unlock() + + var errs []error + f.closeOnce.Do(func() { + for _, table := range f.tables { + if err := table.Close(); err != nil { + errs = append(errs, err) + } + } + if err := f.instanceLock.Unlock(); err != nil { + errs = append(errs, err) + } + }) + if errs != nil { + return fmt.Errorf("%v", errs) + } + return nil +} + +// AncientDatadir returns the path of the ancient store. +func (f *Freezer) AncientDatadir() (string, error) { + return f.datadir, nil +} + +// HasAncient returns an indicator whether the specified ancient data exists +// in the freezer. +func (f *Freezer) HasAncient(kind string, number uint64) (bool, error) { + if table := f.tables[kind]; table != nil { + return table.has(number), nil + } + return false, nil +} + +// Ancient retrieves an ancient binary blob from the append-only immutable files. +func (f *Freezer) Ancient(kind string, number uint64) ([]byte, error) { + if table := f.tables[kind]; table != nil { + return table.Retrieve(number) + } + return nil, errUnknownTable +} + +// AncientRange retrieves multiple items in sequence, starting from the index 'start'. +// It will return +// - at most 'count' items, +// - if maxBytes is specified: at least 1 item (even if exceeding the maxByteSize), +// but will otherwise return as many items as fit into maxByteSize. +// - if maxBytes is not specified, 'count' items will be returned if they are present. +func (f *Freezer) AncientRange(kind string, start, count, maxBytes uint64) ([][]byte, error) { + if table := f.tables[kind]; table != nil { + return table.RetrieveItems(start, count, maxBytes) + } + return nil, errUnknownTable +} + +// Ancients returns the length of the frozen items. +func (f *Freezer) Ancients() (uint64, error) { + return f.frozen.Load(), nil +} + +// Tail returns the number of first stored item in the freezer. +func (f *Freezer) Tail() (uint64, error) { + return f.tail.Load(), nil +} + +// AncientSize returns the ancient size of the specified category. +func (f *Freezer) AncientSize(kind string) (uint64, error) { + // This needs the write lock to avoid data races on table fields. + // Speed doesn't matter here, AncientSize is for debugging. + f.writeLock.RLock() + defer f.writeLock.RUnlock() + + if table := f.tables[kind]; table != nil { + return table.size() + } + return 0, errUnknownTable +} + +// ReadAncients runs the given read operation while ensuring that no writes take place +// on the underlying freezer. +func (f *Freezer) ReadAncients(fn func(ethdb.AncientReaderOp) error) (err error) { + f.writeLock.RLock() + defer f.writeLock.RUnlock() + + return fn(f) +} + +// ModifyAncients runs the given write operation. +func (f *Freezer) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (writeSize int64, err error) { + if f.readonly { + return 0, errReadOnly + } + f.writeLock.Lock() + defer f.writeLock.Unlock() + + // Roll back all tables to the starting position in case of error. + prevItem := f.frozen.Load() + defer func() { + if err != nil { + // The write operation has failed. Go back to the previous item position. + for name, table := range f.tables { + err := table.truncateHead(prevItem) + if err != nil { + log.Error("Freezer table roll-back failed", "table", name, "index", prevItem, "err", err) + } + } + } + }() + + f.writeBatch.reset() + if err := fn(f.writeBatch); err != nil { + return 0, err + } + item, writeSize, err := f.writeBatch.commit() + if err != nil { + return 0, err + } + f.frozen.Store(item) + return writeSize, nil +} + +// TruncateHead discards any recent data above the provided threshold number. +func (f *Freezer) TruncateHead(items uint64) error { + if f.readonly { + return errReadOnly + } + f.writeLock.Lock() + defer f.writeLock.Unlock() + + if f.frozen.Load() <= items { + return nil + } + for _, table := range f.tables { + if err := table.truncateHead(items); err != nil { + return err + } + } + f.frozen.Store(items) + return nil +} + +// TruncateTail discards all data below the specified threshold. Note that only +// 'prunable' tables will be truncated. +func (f *Freezer) TruncateTail(tail uint64) error { + if f.readonly { + return errReadOnly + } + f.writeLock.Lock() + defer f.writeLock.Unlock() + + if f.tail.Load() >= tail { + return nil + } + for _, table := range f.tables { + if table.config.prunable { + if err := table.truncateTail(tail); err != nil { + return err + } + } + } + f.tail.Store(tail) + return nil +} + +// Sync flushes all data tables to disk. +func (f *Freezer) Sync() error { + var errs []error + for _, table := range f.tables { + if err := table.Sync(); err != nil { + errs = append(errs, err) + } + } + if errs != nil { + return fmt.Errorf("%v", errs) + } + return nil +} + +// validate checks that every table has the same boundary. +// Used instead of `repair` in readonly mode. +func (f *Freezer) validate() error { + if len(f.tables) == 0 { + return nil + } + var ( + head uint64 + prunedTail *uint64 + ) + // get any head value + for _, table := range f.tables { + head = table.items.Load() + break + } + for kind, table := range f.tables { + // all tables have to have the same head + if head != table.items.Load() { + return fmt.Errorf("freezer table %s has a differing head: %d != %d", kind, table.items.Load(), head) + } + if !table.config.prunable { + // non-prunable tables have to start at 0 + if table.itemHidden.Load() != 0 { + return fmt.Errorf("non-prunable freezer table '%s' has a non-zero tail: %d", kind, table.itemHidden.Load()) + } + } else { + // prunable tables have to have the same length + if prunedTail == nil { + tmp := table.itemHidden.Load() + prunedTail = &tmp + } + if *prunedTail != table.itemHidden.Load() { + return fmt.Errorf("freezer table %s has differing tail: %d != %d", kind, table.itemHidden.Load(), *prunedTail) + } + } + } + + if prunedTail == nil { + tmp := uint64(0) + prunedTail = &tmp + } + + f.frozen.Store(head) + f.tail.Store(*prunedTail) + return nil +} + +// repair truncates all data tables to the same length. +func (f *Freezer) repair() error { + var ( + head = uint64(math.MaxUint64) + prunedTail = uint64(0) + ) + // get the minimal head and the maximum tail + for _, table := range f.tables { + head = min(head, table.items.Load()) + prunedTail = max(prunedTail, table.itemHidden.Load()) + } + // apply the pruning + for kind, table := range f.tables { + // all tables need to have the same head + if err := table.truncateHead(head); err != nil { + return err + } + if !table.config.prunable { + // non-prunable tables have to start at 0 + if table.itemHidden.Load() != 0 { + panic(fmt.Sprintf("non-prunable freezer table %s has non-zero tail: %v", kind, table.itemHidden.Load())) + } + } else { + // prunable tables have to have the same length + if err := table.truncateTail(prunedTail); err != nil { + return err + } + } + } + + f.frozen.Store(head) + f.tail.Store(prunedTail) + return nil +} + +// convertLegacyFn takes a raw freezer entry in an older format and +// returns it in the new format. +type convertLegacyFn = func([]byte) ([]byte, error) + +// MigrateTable processes the entries in a given table in sequence +// converting them to a new format if they're of an old format. +func (f *Freezer) MigrateTable(kind string, convert convertLegacyFn) error { + if f.readonly { + return errReadOnly + } + f.writeLock.Lock() + defer f.writeLock.Unlock() + + table, ok := f.tables[kind] + if !ok { + return errUnknownTable + } + // forEach iterates every entry in the table serially and in order, calling `fn` + // with the item as argument. If `fn` returns an error the iteration stops + // and that error will be returned. + forEach := func(t *freezerTable, offset uint64, fn func(uint64, []byte) error) error { + var ( + items = t.items.Load() + batchSize = uint64(1024) + maxBytes = uint64(1024 * 1024) + ) + for i := offset; i < items; { + if i+batchSize > items { + batchSize = items - i + } + data, err := t.RetrieveItems(i, batchSize, maxBytes) + if err != nil { + return err + } + for j, item := range data { + if err := fn(i+uint64(j), item); err != nil { + return err + } + } + i += uint64(len(data)) + } + return nil + } + // TODO(s1na): This is a sanity-check since as of now no process does tail-deletion. But the migration + // process assumes no deletion at tail and needs to be modified to account for that. + if table.itemOffset.Load() > 0 || table.itemHidden.Load() > 0 { + return errors.New("migration not supported for tail-deleted freezers") + } + ancientsPath := filepath.Dir(table.index.Name()) + // Set up new dir for the migrated table, the content of which + // we'll at the end move over to the ancients dir. + migrationPath := filepath.Join(ancientsPath, "migration") + newTable, err := newFreezerTable(migrationPath, kind, table.config, false) + if err != nil { + return err + } + var ( + batch = newTable.newBatch() + out []byte + start = time.Now() + logged = time.Now() + offset = newTable.items.Load() + ) + if offset > 0 { + log.Info("found previous migration attempt", "migrated", offset) + } + // Iterate through entries and transform them + if err := forEach(table, offset, func(i uint64, blob []byte) error { + if i%10000 == 0 && time.Since(logged) > 16*time.Second { + log.Info("Processing legacy elements", "count", i, "elapsed", common.PrettyDuration(time.Since(start))) + logged = time.Now() + } + out, err = convert(blob) + if err != nil { + return err + } + if err := batch.AppendRaw(i, out); err != nil { + return err + } + return nil + }); err != nil { + return err + } + if err := batch.commit(); err != nil { + return err + } + log.Info("Replacing old table files with migrated ones", "elapsed", common.PrettyDuration(time.Since(start))) + // Release and delete old table files. Note this won't + // delete the index file. + table.releaseFilesAfter(0, true) + + if err := newTable.Close(); err != nil { + return err + } + files, err := os.ReadDir(migrationPath) + if err != nil { + return err + } + // Move migrated files to ancients dir. + for _, f := range files { + // This will replace the old index file as a side-effect. + if err := os.Rename(filepath.Join(migrationPath, f.Name()), filepath.Join(ancientsPath, f.Name())); err != nil { + return err + } + } + // Delete by now empty dir. + if err := os.Remove(migrationPath); err != nil { + return err + } + return nil +} diff --git a/core/rawdb/freezer_batch.go b/core/rawdb/freezer_batch.go new file mode 100644 index 00000000000..6d3812c3dfc --- /dev/null +++ b/core/rawdb/freezer_batch.go @@ -0,0 +1,254 @@ +// Copyright 2021 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rawdb + +import ( + "fmt" + "math" + "time" + + "github.com/XinFinOrg/XDPoSChain/rlp" + "github.com/golang/snappy" +) + +// This is the maximum amount of data that will be buffered in memory +// for a single freezer table batch. +const freezerBatchBufferLimit = 2 * 1024 * 1024 + +// freezerBatch is a write operation of multiple items on a freezer. +type freezerBatch struct { + tables map[string]*freezerTableBatch +} + +func newFreezerBatch(f *Freezer) *freezerBatch { + batch := &freezerBatch{tables: make(map[string]*freezerTableBatch, len(f.tables))} + for kind, table := range f.tables { + batch.tables[kind] = table.newBatch() + } + return batch +} + +// Append adds an RLP-encoded item of the given kind. +func (batch *freezerBatch) Append(kind string, num uint64, item interface{}) error { + return batch.tables[kind].Append(num, item) +} + +// AppendRaw adds an item of the given kind. +func (batch *freezerBatch) AppendRaw(kind string, num uint64, item []byte) error { + return batch.tables[kind].AppendRaw(num, item) +} + +// reset initializes the batch. +func (batch *freezerBatch) reset() { + for _, tb := range batch.tables { + tb.reset() + } +} + +// commit is called at the end of a write operation and +// writes all remaining data to tables. +func (batch *freezerBatch) commit() (item uint64, writeSize int64, err error) { + // Check that count agrees on all batches. + item = uint64(math.MaxUint64) + for name, tb := range batch.tables { + if item < math.MaxUint64 && tb.curItem != item { + return 0, 0, fmt.Errorf("table %s is at item %d, want %d", name, tb.curItem, item) + } + item = tb.curItem + } + + // Commit all table batches. + for _, tb := range batch.tables { + if err := tb.commit(); err != nil { + return 0, 0, err + } + writeSize += tb.totalBytes + } + return item, writeSize, nil +} + +// freezerTableBatch is a batch for a freezer table. +type freezerTableBatch struct { + t *freezerTable + + sb *snappyBuffer + encBuffer writeBuffer + dataBuffer []byte + indexBuffer []byte + curItem uint64 // expected index of next append + totalBytes int64 // counts written bytes since reset +} + +// newBatch creates a new batch for the freezer table. +func (t *freezerTable) newBatch() *freezerTableBatch { + batch := &freezerTableBatch{t: t} + if !t.config.noSnappy { + batch.sb = new(snappyBuffer) + } + batch.reset() + return batch +} + +// reset clears the batch for reuse. +func (batch *freezerTableBatch) reset() { + batch.dataBuffer = batch.dataBuffer[:0] + batch.indexBuffer = batch.indexBuffer[:0] + batch.curItem = batch.t.items.Load() + batch.totalBytes = 0 +} + +// Append rlp-encodes and adds data at the end of the freezer table. The item number is a +// precautionary parameter to ensure data correctness, but the table will reject already +// existing data. +func (batch *freezerTableBatch) Append(item uint64, data interface{}) error { + if item != batch.curItem { + return fmt.Errorf("%w: have %d want %d", errOutOrderInsertion, item, batch.curItem) + } + + // Encode the item. + batch.encBuffer.Reset() + if err := rlp.Encode(&batch.encBuffer, data); err != nil { + return err + } + encItem := batch.encBuffer.data + if batch.sb != nil { + encItem = batch.sb.compress(encItem) + } + return batch.appendItem(encItem) +} + +// AppendRaw injects a binary blob at the end of the freezer table. The item number is a +// precautionary parameter to ensure data correctness, but the table will reject already +// existing data. +func (batch *freezerTableBatch) AppendRaw(item uint64, blob []byte) error { + if item != batch.curItem { + return fmt.Errorf("%w: have %d want %d", errOutOrderInsertion, item, batch.curItem) + } + + encItem := blob + if batch.sb != nil { + encItem = batch.sb.compress(blob) + } + return batch.appendItem(encItem) +} + +func (batch *freezerTableBatch) appendItem(data []byte) error { + // Check if item fits into current data file. + itemSize := int64(len(data)) + itemOffset := batch.t.headBytes + int64(len(batch.dataBuffer)) + if itemOffset+itemSize > int64(batch.t.maxFileSize) { + // It doesn't fit, go to next file first. + if err := batch.commit(); err != nil { + return err + } + if err := batch.t.advanceHead(); err != nil { + return err + } + itemOffset = 0 + } + + // Put data to buffer. + batch.dataBuffer = append(batch.dataBuffer, data...) + batch.totalBytes += itemSize + + // Put index entry to buffer. + entry := indexEntry{filenum: batch.t.headId, offset: uint32(itemOffset + itemSize)} + batch.indexBuffer = entry.append(batch.indexBuffer) + batch.curItem++ + + return batch.maybeCommit() +} + +// maybeCommit writes the buffered data if the buffer is full enough. +func (batch *freezerTableBatch) maybeCommit() error { + if len(batch.dataBuffer) > freezerBatchBufferLimit { + return batch.commit() + } + return nil +} + +// commit writes the batched items to the backing freezerTable. Note index +// file isn't fsync'd after the file write, the recent write can be lost +// after the power failure. +func (batch *freezerTableBatch) commit() error { + _, err := batch.t.head.Write(batch.dataBuffer) + if err != nil { + return err + } + dataSize := int64(len(batch.dataBuffer)) + batch.dataBuffer = batch.dataBuffer[:0] + + _, err = batch.t.index.Write(batch.indexBuffer) + if err != nil { + return err + } + indexSize := int64(len(batch.indexBuffer)) + batch.indexBuffer = batch.indexBuffer[:0] + + // Update headBytes of table. + batch.t.headBytes += dataSize + batch.t.items.Store(batch.curItem) + + // Update metrics. + batch.t.sizeGauge.Inc(dataSize + indexSize) + batch.t.writeMeter.Mark(dataSize + indexSize) + + // Periodically sync the table, todo (rjl493456442) make it configurable? + if time.Since(batch.t.lastSync) > 30*time.Second { + batch.t.lastSync = time.Now() + return batch.t.Sync() + } + return nil +} + +// snappyBuffer writes snappy in block format, and can be reused. It is +// reset when WriteTo is called. +type snappyBuffer struct { + dst []byte +} + +// compress snappy-compresses the data. +func (s *snappyBuffer) compress(data []byte) []byte { + // The snappy library does not care what the capacity of the buffer is, + // but only checks the length. If the length is too small, it will + // allocate a brand new buffer. + // To avoid that, we check the required size here, and grow the size of the + // buffer to utilize the full capacity. + if n := snappy.MaxEncodedLen(len(data)); len(s.dst) < n { + if cap(s.dst) < n { + s.dst = make([]byte, n) + } + s.dst = s.dst[:n] + } + + s.dst = snappy.Encode(s.dst, data) + return s.dst +} + +// writeBuffer implements io.Writer for a byte slice. +type writeBuffer struct { + data []byte +} + +func (wb *writeBuffer) Write(data []byte) (int, error) { + wb.data = append(wb.data, data...) + return len(data), nil +} + +func (wb *writeBuffer) Reset() { + wb.data = wb.data[:0] +} diff --git a/core/rawdb/freezer_meta.go b/core/rawdb/freezer_meta.go new file mode 100644 index 00000000000..48290bb946b --- /dev/null +++ b/core/rawdb/freezer_meta.go @@ -0,0 +1,189 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see + +package rawdb + +import ( + "errors" + "io" + "math" + "os" + + "github.com/XinFinOrg/XDPoSChain/log" + "github.com/XinFinOrg/XDPoSChain/rlp" +) + +const ( + freezerTableV1 = 1 // Initial version of metadata struct + freezerTableV2 = 2 // Add field: 'flushOffset' + freezerVersion = freezerTableV2 // The current used version +) + +// freezerTableMeta is a collection of additional properties that describe the +// freezer table. These properties are designed with error resilience, allowing +// them to be automatically corrected after an error occurs without significantly +// impacting overall correctness. +type freezerTableMeta struct { + file *os.File // file handler of metadata + version uint16 // version descriptor of the freezer table + + // virtualTail represents the number of items marked as deleted. It is + // calculated as the sum of items removed from the table and the items + // hidden within the table, and should never be less than the "actual + // tail". + // + // If lost due to a crash or other reasons, it will be reset to the number + // of items deleted from the table, causing the previously hidden items + // to become visible, which is an acceptable consequence. + virtualTail uint64 + + // flushOffset represents the offset in the index file up to which the index + // items along with the corresponding data items in data files has been flushed + // (fsync’d) to disk. Beyond this offset, data integrity is not guaranteed, + // the extra index items along with the associated data items should be removed + // during the startup. + // + // The principle is that all data items above the flush offset are considered + // volatile and should be recoverable if they are discarded after the unclean + // shutdown. If data integrity is required, manually force a sync of the + // freezer before proceeding with further operations (e.g. do freezer.Sync() + // first and then write data to key value store in some circumstances). + // + // The offset could be moved forward by applying sync operation, or be moved + // backward in cases of head/tail truncation, etc. + flushOffset int64 +} + +// decodeV1 attempts to decode the metadata structure in v1 format. If fails or +// the result is incompatible, nil is returned. +func decodeV1(file *os.File) *freezerTableMeta { + _, err := file.Seek(0, io.SeekStart) + if err != nil { + return nil + } + type obj struct { + Version uint16 + Tail uint64 + } + var o obj + if err := rlp.Decode(file, &o); err != nil { + return nil + } + if o.Version != freezerTableV1 { + return nil + } + return &freezerTableMeta{ + file: file, + version: o.Version, + virtualTail: o.Tail, + } +} + +// decodeV2 attempts to decode the metadata structure in v2 format. If fails or +// the result is incompatible, nil is returned. +func decodeV2(file *os.File) *freezerTableMeta { + _, err := file.Seek(0, io.SeekStart) + if err != nil { + return nil + } + type obj struct { + Version uint16 + Tail uint64 + Offset uint64 + } + var o obj + if err := rlp.Decode(file, &o); err != nil { + return nil + } + if o.Version != freezerTableV2 { + return nil + } + if o.Offset > math.MaxInt64 { + log.Error("Invalid flushOffset %d in freezer metadata", o.Offset, "file", file.Name()) + return nil + } + return &freezerTableMeta{ + file: file, + version: freezerTableV2, + virtualTail: o.Tail, + flushOffset: int64(o.Offset), + } +} + +// newMetadata initializes the metadata object, either by loading it from the file +// or by constructing a new one from scratch. +func newMetadata(file *os.File) (*freezerTableMeta, error) { + stat, err := file.Stat() + if err != nil { + return nil, err + } + if stat.Size() == 0 { + m := &freezerTableMeta{ + file: file, + version: freezerTableV2, + virtualTail: 0, + flushOffset: 0, + } + if err := m.write(true); err != nil { + return nil, err + } + return m, nil + } + if m := decodeV2(file); m != nil { + return m, nil + } + if m := decodeV1(file); m != nil { + return m, nil // legacy metadata + } + return nil, errors.New("failed to decode metadata") +} + +// setVirtualTail sets the virtual tail and flushes the metadata if sync is true. +func (m *freezerTableMeta) setVirtualTail(tail uint64, sync bool) error { + m.virtualTail = tail + return m.write(sync) +} + +// setFlushOffset sets the flush offset and flushes the metadata if sync is true. +func (m *freezerTableMeta) setFlushOffset(offset int64, sync bool) error { + m.flushOffset = offset + return m.write(sync) +} + +// write flushes the content of metadata into file and performs a fsync if required. +func (m *freezerTableMeta) write(sync bool) error { + type obj struct { + Version uint16 + Tail uint64 + Offset uint64 + } + var o obj + o.Version = freezerVersion // forcibly use the current version + o.Tail = m.virtualTail + o.Offset = uint64(m.flushOffset) + + _, err := m.file.Seek(0, io.SeekStart) + if err != nil { + return err + } + if err := rlp.Encode(m.file, &o); err != nil { + return err + } + if !sync { + return nil + } + return m.file.Sync() +} diff --git a/core/rawdb/freezer_meta_test.go b/core/rawdb/freezer_meta_test.go new file mode 100644 index 00000000000..b69d17eeb74 --- /dev/null +++ b/core/rawdb/freezer_meta_test.go @@ -0,0 +1,126 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see + +package rawdb + +import ( + "os" + "testing" + + "github.com/XinFinOrg/XDPoSChain/rlp" +) + +func TestReadWriteFreezerTableMeta(t *testing.T) { + f, err := os.CreateTemp(t.TempDir(), "*") + if err != nil { + t.Fatalf("Failed to create file %v", err) + } + defer f.Close() + + meta, err := newMetadata(f) + if err != nil { + t.Fatalf("Failed to new metadata %v", err) + } + meta.setVirtualTail(100, false) + + meta, err = newMetadata(f) + if err != nil { + t.Fatalf("Failed to reload metadata %v", err) + } + if meta.version != freezerTableV2 { + t.Fatalf("Unexpected version field") + } + if meta.virtualTail != uint64(100) { + t.Fatalf("Unexpected virtual tail field") + } +} + +func TestUpgradeMetadata(t *testing.T) { + f, err := os.CreateTemp(t.TempDir(), "*") + if err != nil { + t.Fatalf("Failed to create file %v", err) + } + defer f.Close() + + // Write legacy metadata into file + type obj struct { + Version uint16 + Tail uint64 + } + var o obj + o.Version = freezerTableV1 + o.Tail = 100 + + if err := rlp.Encode(f, &o); err != nil { + t.Fatalf("Failed to encode %v", err) + } + + // Reload the metadata, a silent upgrade is expected + meta, err := newMetadata(f) + if err != nil { + t.Fatalf("Failed to read metadata %v", err) + } + if meta.version != freezerTableV1 { + t.Fatal("Unexpected version field") + } + if meta.virtualTail != uint64(100) { + t.Fatal("Unexpected virtual tail field") + } + if meta.flushOffset != 0 { + t.Fatal("Unexpected flush offset field") + } + + meta.setFlushOffset(100, true) + + meta, err = newMetadata(f) + if err != nil { + t.Fatalf("Failed to read metadata %v", err) + } + if meta.version != freezerTableV2 { + t.Fatal("Unexpected version field") + } + if meta.virtualTail != uint64(100) { + t.Fatal("Unexpected virtual tail field") + } + if meta.flushOffset != 100 { + t.Fatal("Unexpected flush offset field") + } +} + +func TestInvalidMetadata(t *testing.T) { + f, err := os.CreateTemp(t.TempDir(), "*") + if err != nil { + t.Fatalf("Failed to create file %v", err) + } + defer f.Close() + + // Write invalid legacy metadata into file + type obj struct { + Version uint16 + Tail uint64 + } + var o obj + o.Version = freezerTableV2 // -> invalid version tag + o.Tail = 100 + + if err := rlp.Encode(f, &o); err != nil { + t.Fatalf("Failed to encode %v", err) + } + _, err = newMetadata(f) + if err == nil { + t.Fatal("Unexpected success") + } +} diff --git a/core/rawdb/freezer_resettable.go b/core/rawdb/freezer_resettable.go new file mode 100644 index 00000000000..32e7b1dc4b9 --- /dev/null +++ b/core/rawdb/freezer_resettable.go @@ -0,0 +1,249 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rawdb + +import ( + "os" + "path/filepath" + "sync" + + "github.com/XinFinOrg/XDPoSChain/ethdb" + "github.com/XinFinOrg/XDPoSChain/log" +) + +const tmpSuffix = ".tmp" + +// freezerOpenFunc is the function used to open/create a freezer. +type freezerOpenFunc = func() (*Freezer, error) + +// ResettableFreezer is a wrapper of the freezer which makes the +// freezer resettable. +type ResettableFreezer struct { + readOnly bool + freezer *Freezer + opener freezerOpenFunc + datadir string + lock sync.RWMutex +} + +// NewResettableFreezer creates a resettable freezer, note freezer is +// only resettable if the passed file directory is exclusively occupied +// by the freezer. And also the user-configurable ancient root directory +// is **not** supported for reset since it might be a mount and rename +// will cause a copy of hundreds of gigabyte into local directory. It +// needs some other file based solutions. +// +// 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) { + if err := cleanup(datadir); err != nil { + return nil, err + } + opener := func() (*Freezer, error) { + return NewFreezer(datadir, namespace, readonly, maxTableSize, tables) + } + freezer, err := opener() + if err != nil { + return nil, err + } + return &ResettableFreezer{ + readOnly: readonly, + freezer: freezer, + opener: opener, + datadir: datadir, + }, nil +} + +// Reset deletes the file directory exclusively occupied by the freezer and +// recreate the freezer from scratch. The atomicity of directory deletion +// is guaranteed by the rename operation, the leftover directory will be +// cleaned up in next startup in case crash happens after rename. +func (f *ResettableFreezer) Reset() error { + f.lock.Lock() + defer f.lock.Unlock() + + if f.readOnly { + return errReadOnly + } + if err := f.freezer.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.freezer = freezer + return nil +} + +// Close terminates the chain freezer, unmapping all the data files. +func (f *ResettableFreezer) Close() error { + f.lock.RLock() + defer f.lock.RUnlock() + + return f.freezer.Close() +} + +// HasAncient returns an indicator whether the specified ancient data exists +// in the freezer +func (f *ResettableFreezer) HasAncient(kind string, number uint64) (bool, error) { + f.lock.RLock() + defer f.lock.RUnlock() + + return f.freezer.HasAncient(kind, number) +} + +// Ancient retrieves an ancient binary blob from the append-only immutable files. +func (f *ResettableFreezer) Ancient(kind string, number uint64) ([]byte, error) { + f.lock.RLock() + defer f.lock.RUnlock() + + return f.freezer.Ancient(kind, number) +} + +// AncientRange retrieves multiple items in sequence, starting from the index 'start'. +// It will return +// - at most 'count' items, +// - if maxBytes is specified: at least 1 item (even if exceeding the maxByteSize), +// but will otherwise return as many items as fit into maxByteSize. +// - if maxBytes is not specified, 'count' items will be returned if they are present. +func (f *ResettableFreezer) AncientRange(kind string, start, count, maxBytes uint64) ([][]byte, error) { + f.lock.RLock() + defer f.lock.RUnlock() + + return f.freezer.AncientRange(kind, start, count, maxBytes) +} + +// Ancients returns the length of the frozen items. +func (f *ResettableFreezer) Ancients() (uint64, error) { + f.lock.RLock() + defer f.lock.RUnlock() + + return f.freezer.Ancients() +} + +// Tail returns the number of first stored item in the freezer. +func (f *ResettableFreezer) Tail() (uint64, error) { + f.lock.RLock() + defer f.lock.RUnlock() + + return f.freezer.Tail() +} + +// AncientSize returns the ancient size of the specified category. +func (f *ResettableFreezer) AncientSize(kind string) (uint64, error) { + f.lock.RLock() + defer f.lock.RUnlock() + + return f.freezer.AncientSize(kind) +} + +// ReadAncients runs the given read operation while ensuring that no writes take place +// on the underlying freezer. +func (f *ResettableFreezer) ReadAncients(fn func(ethdb.AncientReaderOp) error) (err error) { + f.lock.RLock() + defer f.lock.RUnlock() + + return f.freezer.ReadAncients(fn) +} + +// ModifyAncients runs the given write operation. +func (f *ResettableFreezer) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (writeSize int64, err error) { + f.lock.RLock() + defer f.lock.RUnlock() + + return f.freezer.ModifyAncients(fn) +} + +// TruncateHead discards any recent data above the provided threshold number. +func (f *ResettableFreezer) TruncateHead(items uint64) error { + f.lock.RLock() + defer f.lock.RUnlock() + + return f.freezer.TruncateHead(items) +} + +// TruncateTail discards any recent data below the provided threshold number. +func (f *ResettableFreezer) TruncateTail(tail uint64) error { + f.lock.RLock() + defer f.lock.RUnlock() + + return f.freezer.TruncateTail(tail) +} + +// Sync flushes all data tables to disk. +func (f *ResettableFreezer) Sync() error { + f.lock.RLock() + defer f.lock.RUnlock() + + return f.freezer.Sync() +} + +// AncientDatadir returns the path of the ancient store. +func (f *ResettableFreezer) AncientDatadir() (string, error) { + f.lock.RLock() + defer f.lock.RUnlock() + + return f.freezer.AncientDatadir() +} + +// MigrateTable processes the entries in a given table in sequence +// converting them to a new format if they're of an old format. +func (f *ResettableFreezer) MigrateTable(kind string, convert convertLegacyFn) error { + f.lock.RLock() + defer f.lock.RUnlock() + + return f.freezer.MigrateTable(kind, convert) +} + +// cleanup removes the directory located in the specified path +// has the name with deletion marker suffix. +func cleanup(path string) error { + parent := filepath.Dir(path) + if _, err := os.Lstat(parent); os.IsNotExist(err) { + return nil + } + dir, err := os.Open(parent) + if err != nil { + return err + } + names, err := dir.Readdirnames(0) + if err != nil { + return err + } + if cerr := dir.Close(); cerr != nil { + return cerr + } + for _, name := range names { + if name == filepath.Base(path)+tmpSuffix { + log.Info("Removed leftover freezer directory", "name", name) + return os.RemoveAll(filepath.Join(parent, name)) + } + } + return nil +} + +func tmpName(path string) string { + return filepath.Join(filepath.Dir(path), filepath.Base(path)+tmpSuffix) +} diff --git a/core/rawdb/freezer_resettable_test.go b/core/rawdb/freezer_resettable_test.go new file mode 100644 index 00000000000..66b830813ae --- /dev/null +++ b/core/rawdb/freezer_resettable_test.go @@ -0,0 +1,107 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rawdb + +import ( + "bytes" + "os" + "testing" + + "github.com/XinFinOrg/XDPoSChain/ethdb" +) + +func TestResetFreezer(t *testing.T) { + items := []struct { + id uint64 + blob []byte + }{ + {0, bytes.Repeat([]byte{0}, 2048)}, + {1, bytes.Repeat([]byte{1}, 2048)}, + {2, bytes.Repeat([]byte{2}, 2048)}, + } + f, _ := NewResettableFreezer(t.TempDir(), "", false, 2048, freezerTestTableDef) + defer f.Close() + + f.ModifyAncients(func(op ethdb.AncientWriteOp) error { + for _, item := range items { + op.AppendRaw("test", item.id, item.blob) + } + return nil + }) + for _, item := range items { + blob, _ := f.Ancient("test", item.id) + if !bytes.Equal(blob, item.blob) { + t.Fatal("Unexpected blob") + } + } + + // Reset freezer + f.Reset() + count, _ := f.Ancients() + if count != 0 { + t.Fatal("Failed to reset freezer") + } + for _, item := range items { + blob, _ := f.Ancient("test", item.id) + if len(blob) != 0 { + t.Fatal("Unexpected blob") + } + } + + // Fill the freezer + f.ModifyAncients(func(op ethdb.AncientWriteOp) error { + for _, item := range items { + op.AppendRaw("test", item.id, item.blob) + } + return nil + }) + for _, item := range items { + blob, _ := f.Ancient("test", item.id) + if !bytes.Equal(blob, item.blob) { + t.Fatal("Unexpected blob") + } + } +} + +func TestFreezerCleanup(t *testing.T) { + items := []struct { + id uint64 + blob []byte + }{ + {0, bytes.Repeat([]byte{0}, 2048)}, + {1, bytes.Repeat([]byte{1}, 2048)}, + {2, bytes.Repeat([]byte{2}, 2048)}, + } + datadir := t.TempDir() + f, _ := NewResettableFreezer(datadir, "", false, 2048, freezerTestTableDef) + f.ModifyAncients(func(op ethdb.AncientWriteOp) error { + for _, item := range items { + op.AppendRaw("test", item.id, item.blob) + } + return nil + }) + f.Close() + os.Rename(datadir, tmpName(datadir)) + + // Open the freezer again, trigger cleanup operation + f, _ = NewResettableFreezer(datadir, "", false, 2048, freezerTestTableDef) + f.Close() + + if _, err := os.Lstat(tmpName(datadir)); !os.IsNotExist(err) { + t.Fatal("Failed to cleanup leftover directory") + } +} diff --git a/core/rawdb/freezer_table.go b/core/rawdb/freezer_table.go index fa174acd651..de76458bb4d 100644 --- a/core/rawdb/freezer_table.go +++ b/core/rawdb/freezer_table.go @@ -17,7 +17,22 @@ package rawdb import ( + "bufio" + "bytes" + "encoding/binary" "errors" + "fmt" + "io" + "os" + "path/filepath" + "sync" + "sync/atomic" + "time" + + "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/log" + "github.com/XinFinOrg/XDPoSChain/metrics" + "github.com/golang/snappy" ) var ( @@ -32,3 +47,1196 @@ var ( // errNotSupported is returned if the database doesn't support the required operation. errNotSupported = errors.New("this operation is not supported") ) + +// indexEntry contains the number/id of the file that the data resides in, as well as the +// offset within the file to the end of the data. +// In serialized form, the filenum is stored as uint16. +type indexEntry struct { + filenum uint32 // stored as uint16 ( 2 bytes ) + offset uint32 // stored as uint32 ( 4 bytes ) +} + +const indexEntrySize = 6 + +// unmarshalBinary deserializes binary b into the rawIndex entry. +func (i *indexEntry) unmarshalBinary(b []byte) { + i.filenum = uint32(binary.BigEndian.Uint16(b[:2])) + i.offset = binary.BigEndian.Uint32(b[2:6]) +} + +// append adds the encoded entry to the end of b. +func (i *indexEntry) append(b []byte) []byte { + offset := len(b) + out := append(b, make([]byte, indexEntrySize)...) + binary.BigEndian.PutUint16(out[offset:], uint16(i.filenum)) + binary.BigEndian.PutUint32(out[offset+2:], i.offset) + return out +} + +// bounds returns the start- and end- offsets, and the file number of where to +// read there data item marked by the two index entries. The two entries are +// assumed to be sequential. +func (i *indexEntry) bounds(end *indexEntry) (startOffset, endOffset, fileId uint32) { + if i.filenum != end.filenum { + // If a piece of data 'crosses' a data-file, + // it's actually in one piece on the second data-file. + // We return a zero-indexEntry for the second file as start + return 0, end.offset, end.filenum + } + return i.offset, end.offset, end.filenum +} + +// freezerTable represents a single chained data table within the freezer (e.g. blocks). +// It consists of a data file (snappy encoded arbitrary data blobs) and an indexEntry +// file (uncompressed 64 bit indices into the data file). +type freezerTable struct { + items atomic.Uint64 // Number of items stored in the table (including items removed from tail) + itemOffset atomic.Uint64 // Number of items removed from the table + + // itemHidden is the number of items marked as deleted. Tail deletion is + // only supported at file level which means the actual deletion will be + // delayed until the entire data file is marked as deleted. Before that + // these items will be hidden to prevent being visited again. The value + // should never be lower than itemOffset. + itemHidden atomic.Uint64 + + config freezerTableConfig // if true, disables snappy compression. Note: does not work retroactively + readonly bool + maxFileSize uint32 // Max file size for data-files + name string + path string + + head *os.File // File descriptor for the data head of the table + index *os.File // File descriptor for the indexEntry file of the table + files map[uint32]*os.File // open files + headId uint32 // number of the currently active head file + tailId uint32 // number of the earliest file + + metadata *freezerTableMeta // metadata of the table + lastSync time.Time // Timestamp when the last sync was performed + + headBytes int64 // Number of bytes written to the head file + readMeter *metrics.Meter // Meter for measuring the effective amount of data read + writeMeter *metrics.Meter // Meter for measuring the effective amount of data written + sizeGauge *metrics.Gauge // Gauge for tracking the combined size of all freezer tables + + logger log.Logger // Logger with database path and table name embedded + lock sync.RWMutex // Mutex protecting the data file descriptors +} + +// newFreezerTable opens the given path as a freezer table. +func newFreezerTable(path, name string, config freezerTableConfig, readonly bool) (*freezerTable, error) { + return newTable(path, name, metrics.NewInactiveMeter(), metrics.NewInactiveMeter(), metrics.NewGauge(), freezerTableSize, config, readonly) +} + +// newTable opens a freezer table, creating the data and index files if they are +// non-existent. Both files are truncated to the shortest common length to ensure +// they don't go out of sync. +func newTable(path string, name string, readMeter, writeMeter *metrics.Meter, sizeGauge *metrics.Gauge, maxFilesize uint32, config freezerTableConfig, readonly bool) (*freezerTable, error) { + // Ensure the containing directory exists and open the indexEntry file + if err := os.MkdirAll(path, 0755); err != nil { + return nil, err + } + var idxName string + if config.noSnappy { + idxName = fmt.Sprintf("%s.ridx", name) // raw index file + } else { + idxName = fmt.Sprintf("%s.cidx", name) // compressed index file + } + var ( + err error + index *os.File + meta *os.File + ) + if readonly { + // Will fail if table index file or meta file is not existent + index, err = openFreezerFileForReadOnly(filepath.Join(path, idxName)) + if err != nil { + return nil, err + } + meta, err = openFreezerFileForReadOnly(filepath.Join(path, fmt.Sprintf("%s.meta", name))) + if err != nil { + return nil, err + } + } else { + index, err = openFreezerFileForAppend(filepath.Join(path, idxName)) + if err != nil { + return nil, err + } + meta, err = openFreezerFileForAppend(filepath.Join(path, fmt.Sprintf("%s.meta", name))) + if err != nil { + return nil, err + } + } + // Load metadata from the file. The tag will be true if legacy metadata + // is detected. + metadata, err := newMetadata(meta) + if err != nil { + return nil, err + } + // Create the table and repair any past inconsistency + tab := &freezerTable{ + index: index, + metadata: metadata, + lastSync: time.Now(), + files: make(map[uint32]*os.File), + readMeter: readMeter, + writeMeter: writeMeter, + sizeGauge: sizeGauge, + name: name, + path: path, + logger: log.New("database", path, "table", name), + config: config, + readonly: readonly, + maxFileSize: maxFilesize, + } + if err := tab.repair(); err != nil { + tab.Close() + return nil, err + } + // Initialize the starting size counter + size, err := tab.sizeNolock() + if err != nil { + tab.Close() + return nil, err + } + tab.sizeGauge.Inc(int64(size)) + + return tab, nil +} + +// repair cross-checks the head and the index file and truncates them to +// be in sync with each other after a potential crash / data loss. +func (t *freezerTable) repair() error { + // Create a temporary offset buffer to init files with and read indexEntry into + buffer := make([]byte, indexEntrySize) + + // If we've just created the files, initialize the index with the 0 indexEntry + stat, err := t.index.Stat() + if err != nil { + return err + } + if stat.Size() == 0 { + if _, err := t.index.Write(buffer); err != nil { + return err + } + } + // Ensure the index is a multiple of indexEntrySize bytes + if overflow := stat.Size() % indexEntrySize; overflow != 0 { + if t.readonly { + return fmt.Errorf("index file(path: %s, name: %s) size is not a multiple of %d", t.path, t.name, indexEntrySize) + } + if err := truncateFreezerFile(t.index, stat.Size()-overflow); err != nil { + return err + } // New file can't trigger this path + } + if err := t.repairIndex(); err != nil { + return err + } + // Retrieve the file sizes and prepare for truncation. Note the file size + // might be changed after index repair. + if stat, err = t.index.Stat(); err != nil { + return err + } + offsetsSize := stat.Size() + + // Open the head file + var ( + firstIndex indexEntry + lastIndex indexEntry + contentSize int64 + contentExp int64 + verbose bool + ) + // Read index zero, determine what file is the earliest + // and what item offset to use + t.index.ReadAt(buffer, 0) + firstIndex.unmarshalBinary(buffer) + + // Assign the tail fields with the first stored index. + // The total removed items is represented with an uint32, + // which is not enough in theory but enough in practice. + // TODO: use uint64 to represent total removed items. + t.tailId = firstIndex.filenum + t.itemOffset.Store(uint64(firstIndex.offset)) + + // Adjust the number of hidden items if it is less than the number of items + // being removed. + if t.itemOffset.Load() > t.metadata.virtualTail { + if err := t.metadata.setVirtualTail(t.itemOffset.Load(), true); err != nil { + return err + } + } + t.itemHidden.Store(t.metadata.virtualTail) + + // Read the last index, use the default value in case the freezer is empty + if offsetsSize == indexEntrySize { + lastIndex = indexEntry{filenum: t.tailId, offset: 0} + } else { + t.index.ReadAt(buffer, offsetsSize-indexEntrySize) + lastIndex.unmarshalBinary(buffer) + } + if t.readonly { + t.head, err = t.openFile(lastIndex.filenum, openFreezerFileForReadOnly) + } else { + t.head, err = t.openFile(lastIndex.filenum, openFreezerFileForAppend) + } + if err != nil { + return err + } + if stat, err = t.head.Stat(); err != nil { + return err + } + contentSize = stat.Size() + + // Keep truncating both files until they come in sync + contentExp = int64(lastIndex.offset) + for contentExp != contentSize { + if t.readonly { + return fmt.Errorf("freezer table(path: %s, name: %s, num: %d) is corrupted", t.path, t.name, lastIndex.filenum) + } + verbose = true + + // Truncate the head file to the last offset pointer + if contentExp < contentSize { + t.logger.Warn("Truncating dangling head", "indexed", contentExp, "stored", contentSize) + if err := truncateFreezerFile(t.head, contentExp); err != nil { + return err + } + contentSize = contentExp + } + // Truncate the index to point within the head file + if contentExp > contentSize { + t.logger.Warn("Truncating dangling indexes", "indexes", offsetsSize/indexEntrySize, "indexed", contentExp, "stored", contentSize) + + newOffset := offsetsSize - indexEntrySize + if err := truncateFreezerFile(t.index, newOffset); err != nil { + return err + } + offsetsSize -= indexEntrySize + + // If the index file is truncated beyond the flush offset, move the flush + // offset back to the new end of the file. A crash may occur before the + // offset is updated, leaving a dangling reference that points to a position + // outside the file. If so, the offset will be reset to the new end of the + // file during the next run. + if t.metadata.flushOffset > newOffset { + if err := t.metadata.setFlushOffset(newOffset, true); err != nil { + return err + } + } + // Read the new head index, use the default value in case + // the freezer is already empty. + var newLastIndex indexEntry + if offsetsSize == indexEntrySize { + newLastIndex = indexEntry{filenum: t.tailId, offset: 0} + } else { + t.index.ReadAt(buffer, offsetsSize-indexEntrySize) + newLastIndex.unmarshalBinary(buffer) + } + // We might have slipped back into an earlier head-file here + if newLastIndex.filenum != lastIndex.filenum { + // Release earlier opened file + t.releaseFile(lastIndex.filenum) + if t.head, err = t.openFile(newLastIndex.filenum, openFreezerFileForAppend); err != nil { + return err + } + if stat, err = t.head.Stat(); err != nil { + // TODO, anything more we can do here? + // A data file has gone missing... + return err + } + contentSize = stat.Size() + } + lastIndex = newLastIndex + contentExp = int64(lastIndex.offset) + } + } + // Sync() fails for read-only files on windows. + if !t.readonly { + // Ensure all reparation changes have been written to disk + if err := t.index.Sync(); err != nil { + return err + } + if err := t.head.Sync(); err != nil { + return err + } + if err := t.metadata.file.Sync(); err != nil { + return err + } + } + // Update the item and byte counters and return + t.items.Store(t.itemOffset.Load() + uint64(offsetsSize/indexEntrySize-1)) // last indexEntry points to the end of the data file + t.headBytes = contentSize + t.headId = lastIndex.filenum + + // Delete the leftover files because of head deletion + t.releaseFilesAfter(t.headId, true) + + // Delete the leftover files because of tail deletion + t.releaseFilesBefore(t.tailId, true) + + // Close opened files and preopen all files + if err := t.preopen(); err != nil { + return err + } + if verbose { + t.logger.Info("Chain freezer table opened", "items", t.items.Load(), "deleted", t.itemOffset.Load(), "hidden", t.itemHidden.Load(), "tailId", t.tailId, "headId", t.headId, "size", t.headBytes) + } else { + t.logger.Debug("Chain freezer table opened", "items", t.items.Load(), "size", common.StorageSize(t.headBytes)) + } + return nil +} + +func (t *freezerTable) repairIndex() error { + stat, err := t.index.Stat() + if err != nil { + return err + } + size := stat.Size() + + // Validate the items in the index file to ensure the data integrity. + // It's possible some garbage data is retained in the index file after + // the power failures and should be truncated first. + size, err = t.checkIndex(size) + if err != nil { + return err + } + // If legacy metadata is detected, attempt to recover the offset from the + // index file to avoid clearing the entire table. + if t.metadata.version == freezerTableV1 { + // Skip truncation if the legacy metadata is opened in read-only mode. + // Since all items in the legacy index file were forcibly synchronized, + // data integrity is guaranteed. Therefore, it's safe to leave any extra + // items untruncated in this special scenario. + if t.readonly { + return nil + } + t.logger.Info("Recovering freezer flushOffset for legacy table", "offset", size) + return t.metadata.setFlushOffset(size, true) + } + + switch { + case size == indexEntrySize && t.metadata.flushOffset == 0: + // It's a new freezer table with no content. + // Move the flush offset to the end of the file. + return t.metadata.setFlushOffset(size, true) + + case size == t.metadata.flushOffset: + // flushOffset is aligned with the index file, all is well. + return nil + + case size > t.metadata.flushOffset: + // Extra index items have been detected beyond the flush offset. Since these + // entries correspond to data that has not been fully flushed to disk in the + // last run (because of unclean shutdown), their integrity cannot be guaranteed. + // To ensure consistency, these index items will be truncated, as there is no + // reliable way to validate or recover their associated data. + extraSize := size - t.metadata.flushOffset + if t.readonly { + return fmt.Errorf("index file(path: %s, name: %s) contains %d garbage data bytes", t.path, t.name, extraSize) + } + t.logger.Warn("Truncating freezer items after flushOffset", "size", extraSize) + return truncateFreezerFile(t.index, t.metadata.flushOffset) + + default: // size < flushOffset + // Flush offset refers to a position larger than index file. The only + // possible scenario for this is: a power failure or system crash has occurred after + // truncating the segment in index file from head or tail, but without updating + // the flush offset. In this case, automatically reset the flush offset with + // the file size which implies the entire index file is complete. + if t.readonly { + return nil // do nothing in read only mode + } + t.logger.Warn("Rewinding freezer flushOffset", "old", t.metadata.flushOffset, "new", size) + return t.metadata.setFlushOffset(size, true) + } +} + +// checkIndex validates the integrity of the index file. According to the design, +// the initial entry in the file denotes the earliest data file along with the +// count of deleted items. Following this, all subsequent entries in the file must +// be in order. This function identifies any corrupted entries and truncates items +// occurring after the corruption point. +// +// corruption can occur because of the power failure. In the Linux kernel, the +// file metadata update and data update are not necessarily performed at the +// same time. Typically, the metadata will be flushed/journalled ahead of the file +// data. Therefore, we make the pessimistic assumption that the file is first +// extended with invalid "garbage" data (normally zero bytes) and that afterwards +// the correct data replaces the garbage. As all the items in index file are +// supposed to be in-order, the leftover garbage must be truncated before the +// index data is utilized. +// +// It's important to note an exception that's unfortunately undetectable: when +// all index entries in the file are zero. Distinguishing whether they represent +// leftover garbage or if all items in the table have zero size is impossible. +// In such instances, the file will remain unchanged to prevent potential data +// loss or misinterpretation. +func (t *freezerTable) checkIndex(size int64) (int64, error) { + // Move the read cursor to the beginning of the file + _, err := t.index.Seek(0, io.SeekStart) + if err != nil { + return 0, err + } + fr := bufio.NewReader(t.index) + + var ( + start = time.Now() + buff = make([]byte, indexEntrySize) + prev indexEntry + head indexEntry + + read = func() (indexEntry, error) { + n, err := io.ReadFull(fr, buff) + if err != nil { + return indexEntry{}, err + } + if n != indexEntrySize { + return indexEntry{}, fmt.Errorf("failed to read from index, n: %d", n) + } + var entry indexEntry + entry.unmarshalBinary(buff) + return entry, nil + } + truncate = func(offset int64) (int64, error) { + if t.readonly { + return 0, fmt.Errorf("index file is corrupted at %d, size: %d", offset, size) + } + if err := truncateFreezerFile(t.index, offset); err != nil { + return 0, err + } + log.Warn("Truncated index file", "offset", offset, "truncated", size-offset) + return offset, nil + } + ) + for offset := int64(0); offset < size; offset += indexEntrySize { + entry, err := read() + if err != nil { + return 0, err + } + if offset == 0 { + head = entry + continue + } + // Ensure that the first non-head index refers to the earliest file, + // or the next file if the earliest file has no space to place the + // first item. + if offset == indexEntrySize { + if entry.filenum != head.filenum && entry.filenum != head.filenum+1 { + log.Error("Corrupted index item detected", "earliest", head.filenum, "filenumber", entry.filenum) + return truncate(offset) + } + prev = entry + continue + } + // ensure two consecutive index items are in order + if err := t.checkIndexItems(prev, entry); err != nil { + log.Error("Corrupted index item detected", "err", err) + return truncate(offset) + } + prev = entry + } + // Move the read cursor to the end of the file. While theoretically, the + // cursor should reach the end by reading all the items in the file, perform + // the seek operation anyway as a precaution. + _, err = t.index.Seek(0, io.SeekEnd) + if err != nil { + return 0, err + } + log.Debug("Verified index file", "items", size/indexEntrySize, "elapsed", common.PrettyDuration(time.Since(start))) + return size, nil +} + +// checkIndexItems validates the correctness of two consecutive index items based +// on the following rules: +// +// - The file number of two consecutive index items must either be the same or +// increase monotonically. If the file number decreases or skips in a +// non-sequential manner, the index item is considered invalid. +// +// - For index items with the same file number, the data offset must be in +// non-decreasing order. Note: Two index items with the same file number +// and the same data offset are permitted if the entry size is zero. +// +// - The first index item in a new data file must not have a zero data offset. +func (t *freezerTable) checkIndexItems(a, b indexEntry) error { + if b.filenum != a.filenum && b.filenum != a.filenum+1 { + return fmt.Errorf("index items with inconsistent file number, prev: %d, next: %d", a.filenum, b.filenum) + } + if b.filenum == a.filenum && b.offset < a.offset { + return fmt.Errorf("index items with unordered offset, prev: %d, next: %d", a.offset, b.offset) + } + if b.filenum == a.filenum+1 && b.offset == 0 { + return fmt.Errorf("index items with zero offset, file number: %d", b.filenum) + } + return nil +} + +// preopen opens all files that the freezer will need. This method should be called from an init-context, +// since it assumes that it doesn't have to bother with locking +// The rationale for doing preopen is to not have to do it from within Retrieve, thus not needing to ever +// obtain a write-lock within Retrieve. +func (t *freezerTable) preopen() (err error) { + // The repair might have already opened (some) files + t.releaseFilesAfter(0, false) + + // Open all except head in RDONLY + for i := t.tailId; i < t.headId; i++ { + if _, err = t.openFile(i, openFreezerFileForReadOnly); err != nil { + return err + } + } + if t.readonly { + t.head, err = t.openFile(t.headId, openFreezerFileForReadOnly) + } else { + // Open head in read/write + t.head, err = t.openFile(t.headId, openFreezerFileForAppend) + } + return err +} + +// truncateHead discards any recent data above the provided threshold number. +func (t *freezerTable) truncateHead(items uint64) error { + t.lock.Lock() + defer t.lock.Unlock() + + // Ensure the given truncate target falls in the correct range + existing := t.items.Load() + if existing <= items { + return nil + } + if items < t.itemHidden.Load() { + return errors.New("truncation below tail") + } + // We need to truncate, save the old size for metrics tracking + oldSize, err := t.sizeNolock() + if err != nil { + return err + } + // Something's out of sync, truncate the table's offset index + log := t.logger.Debug + if existing > items+1 { + log = t.logger.Warn // Only loud warn if we delete multiple items + } + log("Truncating freezer table", "items", existing, "limit", items) + + // Truncate the index file first, the tail position is also considered + // when calculating the new freezer table length. + length := items - t.itemOffset.Load() + newOffset := (length + 1) * indexEntrySize + if err := truncateFreezerFile(t.index, int64(newOffset)); err != nil { + return err + } + if err := t.index.Sync(); err != nil { + return err + } + // If the index file is truncated beyond the flush offset, move the flush + // offset back to the new end of the file. A crash may occur before the + // offset is updated, leaving a dangling reference that points to a position + // outside the file. If so, the offset will be reset to the new end of the + // file during the next run. + if t.metadata.flushOffset > int64(newOffset) { + if err := t.metadata.setFlushOffset(int64(newOffset), true); err != nil { + return err + } + } + // Calculate the new expected size of the data file and truncate it + var expected indexEntry + if length == 0 { + expected = indexEntry{filenum: t.tailId, offset: 0} + } else { + buffer := make([]byte, indexEntrySize) + if _, err := t.index.ReadAt(buffer, int64(length*indexEntrySize)); err != nil { + return err + } + expected.unmarshalBinary(buffer) + } + // We might need to truncate back to older files + if expected.filenum != t.headId { + // If already open for reading, force-reopen for writing + t.releaseFile(expected.filenum) + newHead, err := t.openFile(expected.filenum, openFreezerFileForAppend) + if err != nil { + return err + } + // Release any files _after the current head -- both the previous head + // and any files which may have been opened for reading + t.releaseFilesAfter(expected.filenum, true) + + // Set back the historic head + t.head = newHead + t.headId = expected.filenum + } + if err := truncateFreezerFile(t.head, int64(expected.offset)); err != nil { + return err + } + if err := t.head.Sync(); err != nil { + return err + } + // All data files truncated, set internal counters and return + t.headBytes = int64(expected.offset) + t.items.Store(items) + + // Retrieve the new size and update the total size counter + newSize, err := t.sizeNolock() + if err != nil { + return err + } + t.sizeGauge.Dec(int64(oldSize - newSize)) + return nil +} + +// sizeHidden returns the total data size of hidden items in the freezer table. +// This function assumes the lock is already held. +func (t *freezerTable) sizeHidden() (uint64, error) { + hidden, offset := t.itemHidden.Load(), t.itemOffset.Load() + if hidden <= offset { + return 0, nil + } + indices, err := t.getIndices(hidden-1, 1) + if err != nil { + return 0, err + } + return uint64(indices[1].offset), nil +} + +// truncateTail discards any recent data before the provided threshold number. +func (t *freezerTable) truncateTail(items uint64) error { + t.lock.Lock() + defer t.lock.Unlock() + + // Ensure the given truncate target falls in the correct range + if t.itemHidden.Load() >= items { + return nil + } + if t.items.Load() < items { + return errors.New("truncation above head") + } + // Load the new tail index by the given new tail position + var ( + newTailId uint32 + buffer = make([]byte, indexEntrySize) + ) + if t.items.Load() == items { + newTailId = t.headId + } else { + offset := items - t.itemOffset.Load() + if _, err := t.index.ReadAt(buffer, int64((offset+1)*indexEntrySize)); err != nil { + return err + } + var newTail indexEntry + newTail.unmarshalBinary(buffer) + newTailId = newTail.filenum + } + // Save the old size for metrics tracking. This needs to be done + // before any updates to either itemHidden or itemOffset. + oldSize, err := t.sizeNolock() + if err != nil { + return err + } + // Update the virtual tail marker and hidden these entries in table. + t.itemHidden.Store(items) + + // Update the virtual tail without fsync, otherwise it will significantly + // impact the overall performance. + if err := t.metadata.setVirtualTail(items, false); err != nil { + return err + } + // Hidden items still fall in the current tail file, no data file + // can be dropped. + if t.tailId == newTailId { + return nil + } + // Hidden items fall in the incorrect range, returns the error. + if t.tailId > newTailId { + return fmt.Errorf("invalid index, tail-file %d, item-file %d", t.tailId, newTailId) + } + // Sync the table before performing the index tail truncation. A crash may + // occur after truncating the index file without updating the flush offset, + // leaving a dangling offset that points to a position outside the file. + // The offset will be rewound to the end of file during the next run + // automatically and implicitly assumes all the items within the file are + // complete. + // + // Therefore, forcibly flush everything above the offset to ensure this + // assumption is satisfied! + if err := t.doSync(); err != nil { + return err + } + // Count how many items can be deleted from the file. + var ( + newDeleted = items + deleted = t.itemOffset.Load() + ) + // Hidden items exceed the current tail file, drop the relevant data files. + for current := items - 1; current >= deleted; current -= 1 { + if _, err := t.index.ReadAt(buffer, int64((current-deleted+1)*indexEntrySize)); err != nil { + return err + } + var pre indexEntry + pre.unmarshalBinary(buffer) + if pre.filenum != newTailId { + break + } + newDeleted = current + } + // Close the index file before shorten it. + if err := t.index.Close(); err != nil { + return err + } + // Truncate the deleted index entries from the index file. + err = copyFrom(t.index.Name(), t.index.Name(), indexEntrySize*(newDeleted-deleted+1), func(f *os.File) error { + tailIndex := indexEntry{ + filenum: newTailId, + offset: uint32(newDeleted), + } + _, err := f.Write(tailIndex.append(nil)) + return err + }) + if err != nil { + return err + } + // Reopen the modified index file to load the changes + t.index, err = openFreezerFileForAppend(t.index.Name()) + if err != nil { + return err + } + // Sync the file to ensure changes are flushed to disk + if err := t.index.Sync(); err != nil { + return err + } + // Release any files before the current tail + t.tailId = newTailId + t.itemOffset.Store(newDeleted) + t.releaseFilesBefore(t.tailId, true) + + // Move the index flush offset backward due to the deletion of an index segment. + // A crash may occur before the offset is updated, leaving a dangling reference + // that points to a position outside the file. If so, the offset will be reset + // to the new end of the file during the next run. + // + // Note, both the index and head data file has been persisted before performing + // tail truncation and all the items in these files are regarded as complete. + shorten := indexEntrySize * int64(newDeleted-deleted) + if t.metadata.flushOffset <= shorten { + return fmt.Errorf("invalid index flush offset: %d, shorten: %d", t.metadata.flushOffset, shorten) + } else { + if err := t.metadata.setFlushOffset(t.metadata.flushOffset-shorten, true); err != nil { + return err + } + } + // Retrieve the new size and update the total size counter + newSize, err := t.sizeNolock() + if err != nil { + return err + } + t.sizeGauge.Dec(int64(oldSize - newSize)) + return nil +} + +// Close closes all opened files and finalizes the freezer table for use. +// This operation must be completed before shutdown to prevent the loss of +// recent writes. +func (t *freezerTable) Close() error { + t.lock.Lock() + defer t.lock.Unlock() + + if err := t.doSync(); err != nil { + return err + } + var errs []error + doClose := func(f *os.File) { + if err := f.Close(); err != nil { + errs = append(errs, err) + } + } + doClose(t.index) + doClose(t.metadata.file) + for _, f := range t.files { + doClose(f) + } + t.index = nil + t.head = nil + t.metadata.file = nil + + if errs != nil { + return fmt.Errorf("%v", errs) + } + return nil +} + +// openFile assumes that the write-lock is held by the caller +func (t *freezerTable) openFile(num uint32, opener func(string) (*os.File, error)) (f *os.File, err error) { + var exist bool + if f, exist = t.files[num]; !exist { + var name string + if t.config.noSnappy { + name = fmt.Sprintf("%s.%04d.rdat", t.name, num) + } else { + name = fmt.Sprintf("%s.%04d.cdat", t.name, num) + } + f, err = opener(filepath.Join(t.path, name)) + if err != nil { + return nil, err + } + t.files[num] = f + } + return f, err +} + +// releaseFile closes a file, and removes it from the open file cache. +// Assumes that the caller holds the write lock +func (t *freezerTable) releaseFile(num uint32) { + if f, exist := t.files[num]; exist { + delete(t.files, num) + f.Close() + } +} + +// releaseFilesAfter closes all open files with a higher number, and optionally also deletes the files +func (t *freezerTable) releaseFilesAfter(num uint32, remove bool) { + for fnum, f := range t.files { + if fnum > num { + delete(t.files, fnum) + f.Close() + if remove { + os.Remove(f.Name()) + } + } + } +} + +// releaseFilesBefore closes all open files with a lower number, and optionally also deletes the files +func (t *freezerTable) releaseFilesBefore(num uint32, remove bool) { + for fnum, f := range t.files { + if fnum < num { + delete(t.files, fnum) + f.Close() + if remove { + os.Remove(f.Name()) + } + } + } +} + +// getIndices returns the index entries for the given from-item, covering 'count' items. +// N.B: The actual number of returned indices for N items will always be N+1 (unless an +// error is returned). +// OBS: This method assumes that the caller has already verified (and/or trimmed) the range +// so that the items are within bounds. If this method is used to read out of bounds, +// it will return error. +func (t *freezerTable) getIndices(from, count uint64) ([]*indexEntry, error) { + // Apply the table-offset + from = from - t.itemOffset.Load() + + // For reading N items, we need N+1 indices. + buffer := make([]byte, (count+1)*indexEntrySize) + if _, err := t.index.ReadAt(buffer, int64(from*indexEntrySize)); err != nil { + return nil, err + } + var ( + indices []*indexEntry + offset int + ) + for i := from; i <= from+count; i++ { + index := new(indexEntry) + index.unmarshalBinary(buffer[offset:]) + offset += indexEntrySize + indices = append(indices, index) + } + if from == 0 { + // Special case if we're reading the first item in the freezer. We assume that + // the first item always start from zero(regarding the deletion, we + // only support deletion by files, so that the assumption is held). + // This means we can use the first item metadata to carry information about + // the 'global' offset, for the deletion-case + indices[0].offset = 0 + indices[0].filenum = indices[1].filenum + } + return indices, nil +} + +// Retrieve looks up the data offset of an item with the given number and retrieves +// the raw binary blob from the data file. +func (t *freezerTable) Retrieve(item uint64) ([]byte, error) { + items, err := t.RetrieveItems(item, 1, 0) + if err != nil { + return nil, err + } + return items[0], nil +} + +// RetrieveItems returns multiple items in sequence, starting from the index 'start'. +// It will return at most 'max' items, but will abort earlier to respect the +// 'maxBytes' argument. However, if the 'maxBytes' is smaller than the size of one +// item, it _will_ return one element and possibly overflow the maxBytes. +func (t *freezerTable) RetrieveItems(start, count, maxBytes uint64) ([][]byte, error) { + // First we read the 'raw' data, which might be compressed. + diskData, sizes, err := t.retrieveItems(start, count, maxBytes) + if err != nil { + return nil, err + } + var ( + output = make([][]byte, 0, count) + offset int // offset for reading + outputSize int // size of uncompressed data + ) + // Now slice up the data and decompress. + for i, diskSize := range sizes { + item := diskData[offset : offset+diskSize] + offset += diskSize + decompressedSize := diskSize + if !t.config.noSnappy { + decompressedSize, _ = snappy.DecodedLen(item) + } + if i > 0 && maxBytes != 0 && uint64(outputSize+decompressedSize) > maxBytes { + break + } + if !t.config.noSnappy { + data, err := snappy.Decode(nil, item) + if err != nil { + return nil, err + } + output = append(output, data) + } else { + output = append(output, item) + } + outputSize += decompressedSize + } + return output, nil +} + +// retrieveItems reads up to 'count' items from the table. It reads at least +// one item, but otherwise avoids reading more than maxBytes bytes. Freezer +// will ignore the size limitation and continuously allocate memory to store +// data if maxBytes is 0. It returns the (potentially compressed) data, and +// the sizes. +func (t *freezerTable) retrieveItems(start, count, maxBytes uint64) ([]byte, []int, error) { + t.lock.RLock() + defer t.lock.RUnlock() + + // Ensure the table and the item are accessible + if t.index == nil || t.head == nil || t.metadata.file == nil { + return nil, nil, errClosed + } + var ( + items = t.items.Load() // the total items(head + 1) + hidden = t.itemHidden.Load() // the number of hidden items + ) + // Ensure the start is written, not deleted from the tail, and that the + // caller actually wants something + if items <= start || hidden > start || count == 0 { + return nil, nil, errOutOfBounds + } + if start+count > items { + count = items - start + } + var output []byte // Buffer to read data into + if maxBytes != 0 { + output = make([]byte, 0, maxBytes) + } else { + output = make([]byte, 0, 1024) // initial buffer cap + } + // readData is a helper method to read a single data item from disk. + readData := func(fileId, start uint32, length int) error { + output = grow(output, length) + dataFile, exist := t.files[fileId] + if !exist { + return fmt.Errorf("missing data file %d", fileId) + } + if _, err := dataFile.ReadAt(output[len(output)-length:], int64(start)); err != nil { + return fmt.Errorf("%w, fileid: %d, start: %d, length: %d", err, fileId, start, length) + } + return nil + } + // Read all the indexes in one go + indices, err := t.getIndices(start, count) + if err != nil { + return nil, nil, err + } + var ( + sizes []int // The sizes for each element + totalSize = 0 // The total size of all data read so far + readStart = indices[0].offset // Where, in the file, to start reading + unreadSize = 0 // The size of the as-yet-unread data + ) + + for i, firstIndex := range indices[:len(indices)-1] { + secondIndex := indices[i+1] + // Determine the size of the item. + offset1, offset2, _ := firstIndex.bounds(secondIndex) + size := int(offset2 - offset1) + // Crossing a file boundary? + if secondIndex.filenum != firstIndex.filenum { + // If we have unread data in the first file, we need to do that read now. + if unreadSize > 0 { + if err := readData(firstIndex.filenum, readStart, unreadSize); err != nil { + return nil, nil, err + } + unreadSize = 0 + } + readStart = 0 + } + if i > 0 && uint64(totalSize+size) > maxBytes && maxBytes != 0 { + // About to break out due to byte limit being exceeded. We don't + // read this last item, but we need to do the deferred reads now. + if unreadSize > 0 { + if err := readData(secondIndex.filenum, readStart, unreadSize); err != nil { + return nil, nil, err + } + } + break + } + // Defer the read for later + unreadSize += size + totalSize += size + sizes = append(sizes, size) + if i == len(indices)-2 || (uint64(totalSize) > maxBytes && maxBytes != 0) { + // Last item, need to do the read now + if err := readData(secondIndex.filenum, readStart, unreadSize); err != nil { + return nil, nil, err + } + break + } + } + + // Update metrics. + t.readMeter.Mark(int64(totalSize)) + return output, sizes, nil +} + +// has returns an indicator whether the specified number data is still accessible +// in the freezer table. +func (t *freezerTable) has(number uint64) bool { + return t.items.Load() > number && t.itemHidden.Load() <= number +} + +// size returns the total data size in the freezer table. +func (t *freezerTable) size() (uint64, error) { + t.lock.RLock() + defer t.lock.RUnlock() + + return t.sizeNolock() +} + +// sizeNolock returns the total data size in the freezer table. This function +// assumes the lock is already held. +func (t *freezerTable) sizeNolock() (uint64, error) { + stat, err := t.index.Stat() + if err != nil { + return 0, err + } + hidden, err := t.sizeHidden() + if err != nil { + return 0, err + } + total := uint64(t.maxFileSize)*uint64(t.headId-t.tailId) + uint64(t.headBytes) + uint64(stat.Size()) - hidden + return total, nil +} + +// advanceHead should be called when the current head file would outgrow the file limits, +// and a new file must be opened. The caller of this method must hold the write-lock +// before calling this method. +func (t *freezerTable) advanceHead() error { + t.lock.Lock() + defer t.lock.Unlock() + + if err := t.doSync(); err != nil { + return err + } + // We open the next file in truncated mode -- if this file already + // exists, we need to start over from scratch on it. + nextID := t.headId + 1 + newHead, err := t.openFile(nextID, openFreezerFileTruncated) + if err != nil { + return err + } + // Commit the contents of the old file to stable storage and + // tear it down. It will be re-opened in read-only mode. + if err := t.head.Sync(); err != nil { + return err + } + t.releaseFile(t.headId) + t.openFile(t.headId, openFreezerFileForReadOnly) + + // Swap out the current head. + t.head = newHead + t.headBytes = 0 + t.headId = nextID + return nil +} + +// Sync pushes any pending data from memory out to disk. This is an expensive +// operation, so use it with care. +func (t *freezerTable) Sync() error { + t.lock.Lock() + defer t.lock.Unlock() + + return t.doSync() +} + +// doSync is the internal version of Sync which assumes the lock is already held. +func (t *freezerTable) doSync() error { + // Trying to fsync a file opened in rdonly causes "Access denied" + // error on Windows. + if t.readonly { + return nil + } + if t.index == nil || t.head == nil || t.metadata.file == nil { + return errClosed + } + var err error + trackError := func(e error) { + if e != nil && err == nil { + err = e + } + } + trackError(t.index.Sync()) + trackError(t.head.Sync()) + + // A crash may occur before the offset is updated, leaving the offset + // points to a old position. If so, the extra items above the offset + // will be truncated during the next run. + stat, err := t.index.Stat() + if err != nil { + return err + } + offset := stat.Size() + trackError(t.metadata.setFlushOffset(offset, true)) + return err +} + +func (t *freezerTable) dumpIndexStdout(start, stop int64) { + t.dumpIndex(os.Stdout, start, stop) +} + +func (t *freezerTable) dumpIndexString(start, stop int64) string { + var out bytes.Buffer + out.WriteString("\n") + t.dumpIndex(&out, start, stop) + return out.String() +} + +func (t *freezerTable) dumpIndex(w io.Writer, start, stop int64) { + fmt.Fprintf(w, "Version %d count %d, deleted %d, hidden %d\n", + t.metadata.version, t.items.Load(), t.itemOffset.Load(), t.itemHidden.Load()) + + buf := make([]byte, indexEntrySize) + + fmt.Fprintf(w, "| number | fileno | offset |\n") + fmt.Fprintf(w, "|--------|--------|--------|\n") + + for i := uint64(start); ; i++ { + if _, err := t.index.ReadAt(buf, int64((i+1)*indexEntrySize)); err != nil { + break + } + var entry indexEntry + entry.unmarshalBinary(buf) + fmt.Fprintf(w, "| %03d | %03d | %03d | \n", i, entry.filenum, entry.offset) + if stop > 0 && i >= uint64(stop) { + break + } + } + fmt.Fprintf(w, "|--------------------------|\n") +} diff --git a/core/rawdb/freezer_table_test.go b/core/rawdb/freezer_table_test.go new file mode 100644 index 00000000000..7835479b11a --- /dev/null +++ b/core/rawdb/freezer_table_test.go @@ -0,0 +1,1573 @@ +// Copyright 2018 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rawdb + +import ( + "bytes" + "encoding/binary" + "fmt" + "math/rand" + "os" + "path/filepath" + "reflect" + "testing" + "testing/quick" + + "github.com/XinFinOrg/XDPoSChain/metrics" + "github.com/davecgh/go-spew/spew" + "github.com/stretchr/testify/require" +) + +// TestFreezerBasics test initializing a freezertable from scratch, writing to the table, +// and reading it back. +func TestFreezerBasics(t *testing.T) { + t.Parallel() + // set cutoff at 50 bytes + f, err := newTable(os.TempDir(), + fmt.Sprintf("unittest-%d", rand.Uint64()), + metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + // 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) + // + //db[0] = 000000000000000000000000000000 + //db[1] = 010101010101010101010101010101 + //db[2] = 020202020202020202020202020202 + + for y := 0; y < 255; y++ { + exp := getChunk(15, y) + got, err := f.Retrieve(uint64(y)) + if err != nil { + t.Fatalf("reading item %d: %v", y, err) + } + if !bytes.Equal(got, exp) { + t.Fatalf("test %d, got \n%x != \n%x", y, got, exp) + } + } + // Check that we cannot read too far + _, err = f.Retrieve(uint64(255)) + if err != errOutOfBounds { + t.Fatal(err) + } +} + +// TestFreezerBasicsClosing tests same as TestFreezerBasics, but also closes and reopens the freezer between +// every operation +func TestFreezerBasicsClosing(t *testing.T) { + t.Parallel() + // set cutoff at 50 bytes + var ( + fname = fmt.Sprintf("basics-close-%d", rand.Uint64()) + rm, wm, sg = metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() + f *freezerTable + err error + ) + f, err = newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + + // Write 15 bytes 255 times, results in 85 files. + // In-between writes, the table is closed and re-opened. + for x := 0; x < 255; x++ { + data := getChunk(15, x) + batch := f.newBatch() + require.NoError(t, batch.AppendRaw(uint64(x), data)) + require.NoError(t, batch.commit()) + f.Close() + + f, err = newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + } + defer f.Close() + + for y := 0; y < 255; y++ { + exp := getChunk(15, y) + got, err := f.Retrieve(uint64(y)) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, exp) { + t.Fatalf("test %d, got \n%x != \n%x", y, got, exp) + } + f.Close() + f, err = newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + } +} + +// TestFreezerRepairDanglingHead tests that we can recover if index entries are removed +func TestFreezerRepairDanglingHead(t *testing.T) { + t.Parallel() + rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() + fname := fmt.Sprintf("dangling_headtest-%d", rand.Uint64()) + + // Fill table + { + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + // Write 15 bytes 255 times + writeChunks(t, f, 255, 15) + + // The last item should be there + if _, err = f.Retrieve(0xfe); err != nil { + t.Fatal(err) + } + f.Close() + } + + // open the index + idxFile, err := os.OpenFile(filepath.Join(os.TempDir(), fmt.Sprintf("%s.ridx", fname)), os.O_RDWR, 0644) + if err != nil { + t.Fatalf("Failed to open index file: %v", err) + } + // Remove 4 bytes + stat, err := idxFile.Stat() + if err != nil { + t.Fatalf("Failed to stat index file: %v", err) + } + idxFile.Truncate(stat.Size() - 4) + idxFile.Close() + + // Now open it again + { + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + // The last item should be missing + if _, err = f.Retrieve(0xff); err == nil { + t.Errorf("Expected error for missing index entry") + } + // The one before should still be there + if _, err = f.Retrieve(0xfd); err != nil { + t.Fatalf("Expected no error, got %v", err) + } + } +} + +// TestFreezerRepairDanglingHeadLarge tests that we can recover if very many index entries are removed +func TestFreezerRepairDanglingHeadLarge(t *testing.T) { + t.Parallel() + rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() + fname := fmt.Sprintf("dangling_headtest-%d", rand.Uint64()) + + // Fill a table and close it + { + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + // Write 15 bytes 255 times + writeChunks(t, f, 255, 15) + + // The last item should be there + if _, err = f.Retrieve(f.items.Load() - 1); err != nil { + t.Fatal(err) + } + f.Close() + } + + // open the index + idxFile, err := os.OpenFile(filepath.Join(os.TempDir(), fmt.Sprintf("%s.ridx", fname)), os.O_RDWR, 0644) + if err != nil { + t.Fatalf("Failed to open index file: %v", err) + } + // Remove everything but the first item, and leave data unaligned + // 0-indexEntry, 1-indexEntry, corrupt-indexEntry + idxFile.Truncate(2*indexEntrySize + indexEntrySize/2) + idxFile.Close() + + // Now open it again + { + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + // The first item should be there + if _, err = f.Retrieve(0); err != nil { + t.Fatal(err) + } + // The second item should be missing + if _, err = f.Retrieve(1); err == nil { + t.Errorf("Expected error for missing index entry") + } + // We should now be able to store items again, from item = 1 + batch := f.newBatch() + for x := 1; x < 0xff; x++ { + require.NoError(t, batch.AppendRaw(uint64(x), getChunk(15, ^x))) + } + require.NoError(t, batch.commit()) + f.Close() + } + + // And if we open it, we should now be able to read all of them (new values) + { + f, _ := newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + for y := 1; y < 255; y++ { + exp := getChunk(15, ^y) + got, err := f.Retrieve(uint64(y)) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, exp) { + t.Fatalf("test %d, got \n%x != \n%x", y, got, exp) + } + } + } +} + +// TestSnappyDetection tests that we fail to open a snappy database and vice versa +func TestSnappyDetection(t *testing.T) { + t.Parallel() + rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() + fname := fmt.Sprintf("snappytest-%d", rand.Uint64()) + + // Open with snappy + { + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + // Write 15 bytes 255 times + writeChunks(t, f, 255, 15) + f.Close() + } + + // Open with snappy + { + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + // There should be 255 items + if _, err = f.Retrieve(0xfe); err != nil { + f.Close() + t.Fatalf("expected no error, got %v", err) + } + } + + // Open without snappy + { + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: false}, false) + if err != nil { + t.Fatal(err) + } + if _, err = f.Retrieve(0); err == nil { + f.Close() + t.Fatalf("expected empty table") + } + } +} + +func assertFileSize(f string, size int64) error { + stat, err := os.Stat(f) + if err != nil { + return err + } + if stat.Size() != size { + return fmt.Errorf("error, expected size %d, got %d", size, stat.Size()) + } + return nil +} + +// TestFreezerRepairDanglingIndex checks that if the index has more entries than there are data, +// the index is repaired +func TestFreezerRepairDanglingIndex(t *testing.T) { + t.Parallel() + rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() + fname := fmt.Sprintf("dangling_indextest-%d", rand.Uint64()) + + // Fill a table and close it + { + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + // Write 15 bytes 9 times : 150 bytes + writeChunks(t, f, 9, 15) + + // The last item should be there + if _, err = f.Retrieve(f.items.Load() - 1); err != nil { + f.Close() + t.Fatal(err) + } + f.Close() + // File sizes should be 45, 45, 45 : items[3, 3, 3) + } + + // Crop third file + fileToCrop := filepath.Join(os.TempDir(), fmt.Sprintf("%s.0002.rdat", fname)) + // Truncate third file: 45 ,45, 20 + { + if err := assertFileSize(fileToCrop, 45); err != nil { + t.Fatal(err) + } + file, err := os.OpenFile(fileToCrop, os.O_RDWR, 0644) + if err != nil { + t.Fatal(err) + } + file.Truncate(20) + file.Close() + } + + // Open db it again + // It should restore the file(s) to + // 45, 45, 15 + // with 3+3+1 items + { + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + defer f.Close() + if f.items.Load() != 7 { + t.Fatalf("expected %d items, got %d", 7, f.items.Load()) + } + if err := assertFileSize(fileToCrop, 15); err != nil { + t.Fatal(err) + } + } +} + +func TestFreezerTruncate(t *testing.T) { + t.Parallel() + rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() + fname := fmt.Sprintf("truncation-%d", rand.Uint64()) + + // Fill table + { + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + // Write 15 bytes 30 times + writeChunks(t, f, 30, 15) + + // The last item should be there + if _, err = f.Retrieve(f.items.Load() - 1); err != nil { + t.Fatal(err) + } + f.Close() + } + + // Reopen, truncate + { + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + defer f.Close() + f.truncateHead(10) // 150 bytes + if f.items.Load() != 10 { + t.Fatalf("expected %d items, got %d", 10, f.items.Load()) + } + // 45, 45, 45, 15 -- bytes should be 15 + if f.headBytes != 15 { + t.Fatalf("expected %d bytes, got %d", 15, f.headBytes) + } + } +} + +// TestFreezerRepairFirstFile tests a head file with the very first item only half-written. +// That will rewind the index, and _should_ truncate the head file +func TestFreezerRepairFirstFile(t *testing.T) { + t.Parallel() + rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() + fname := fmt.Sprintf("truncationfirst-%d", rand.Uint64()) + + // Fill table + { + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + // Write 80 bytes, splitting out into two files + batch := f.newBatch() + require.NoError(t, batch.AppendRaw(0, getChunk(40, 0xFF))) + require.NoError(t, batch.AppendRaw(1, getChunk(40, 0xEE))) + require.NoError(t, batch.commit()) + + // The last item should be there + if _, err = f.Retrieve(1); err != nil { + t.Fatal(err) + } + f.Close() + } + + // Truncate the file in half + fileToCrop := filepath.Join(os.TempDir(), fmt.Sprintf("%s.0001.rdat", fname)) + { + if err := assertFileSize(fileToCrop, 40); err != nil { + t.Fatal(err) + } + file, err := os.OpenFile(fileToCrop, os.O_RDWR, 0644) + if err != nil { + t.Fatal(err) + } + file.Truncate(20) + file.Close() + } + + // Reopen + { + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + if f.items.Load() != 1 { + f.Close() + t.Fatalf("expected %d items, got %d", 0, f.items.Load()) + } + + // Write 40 bytes + batch := f.newBatch() + require.NoError(t, batch.AppendRaw(1, getChunk(40, 0xDD))) + require.NoError(t, batch.commit()) + + f.Close() + + // Should have been truncated down to zero and then 40 written + if err := assertFileSize(fileToCrop, 40); err != nil { + t.Fatal(err) + } + } +} + +// TestFreezerReadAndTruncate tests: +// - we have a table open +// - do some reads, so files are open in readonly +// - truncate so those files are 'removed' +// - check that we did not keep the rdonly file descriptors +func TestFreezerReadAndTruncate(t *testing.T) { + t.Parallel() + rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() + fname := fmt.Sprintf("read_truncate-%d", rand.Uint64()) + + // Fill table + { + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + // Write 15 bytes 30 times + writeChunks(t, f, 30, 15) + + // The last item should be there + if _, err = f.Retrieve(f.items.Load() - 1); err != nil { + t.Fatal(err) + } + f.Close() + } + + // Reopen and read all files + { + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + if f.items.Load() != 30 { + f.Close() + t.Fatalf("expected %d items, got %d", 0, f.items.Load()) + } + for y := byte(0); y < 30; y++ { + f.Retrieve(uint64(y)) + } + + // Now, truncate back to zero + f.truncateHead(0) + + // Write the data again + batch := f.newBatch() + for x := 0; x < 30; x++ { + require.NoError(t, batch.AppendRaw(uint64(x), getChunk(15, ^x))) + } + require.NoError(t, batch.commit()) + f.Close() + } +} + +func TestFreezerOffset(t *testing.T) { + t.Parallel() + rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() + fname := fmt.Sprintf("offset-%d", rand.Uint64()) + + // Fill table + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 40, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + + // Write 6 x 20 bytes, splitting out into three files + batch := f.newBatch() + require.NoError(t, batch.AppendRaw(0, getChunk(20, 0xFF))) + require.NoError(t, batch.AppendRaw(1, getChunk(20, 0xEE))) + + require.NoError(t, batch.AppendRaw(2, getChunk(20, 0xdd))) + require.NoError(t, batch.AppendRaw(3, getChunk(20, 0xcc))) + + require.NoError(t, batch.AppendRaw(4, getChunk(20, 0xbb))) + require.NoError(t, batch.AppendRaw(5, getChunk(20, 0xaa))) + require.NoError(t, batch.commit()) + + t.Log(f.dumpIndexString(0, 100)) + + // Now crop it. + f.truncateTail(4) + f.Close() + + // Now open again + f, err = newTable(os.TempDir(), fname, rm, wm, sg, 40, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + t.Log(f.dumpIndexString(0, 100)) + + // It should allow writing item 6. + batch = f.newBatch() + require.NoError(t, batch.AppendRaw(6, getChunk(20, 0x99))) + require.NoError(t, batch.commit()) + + checkRetrieveError(t, f, map[uint64]error{ + 0: errOutOfBounds, + 1: errOutOfBounds, + 2: errOutOfBounds, + 3: errOutOfBounds, + }) + checkRetrieve(t, f, map[uint64][]byte{ + 4: getChunk(20, 0xbb), + 5: getChunk(20, 0xaa), + 6: getChunk(20, 0x99), + }) + f.Close() + + // Edit the index again, with a much larger initial offset of 1M. + { + // Read the index file + p := filepath.Join(os.TempDir(), fmt.Sprintf("%v.ridx", fname)) + indexFile, err := os.OpenFile(p, os.O_RDWR, 0644) + if err != nil { + t.Fatal(err) + } + indexBuf := make([]byte, 3*indexEntrySize) + indexFile.Read(indexBuf) + + // Update the index file, so that we store + // [ file = 2, offset = 1M ] at index zero + + zeroIndex := indexEntry{ + offset: uint32(1000000), // We have removed 1M items + filenum: uint32(2), // First file is 2 + } + buf := zeroIndex.append(nil) + + // Overwrite index zero + copy(indexBuf, buf) + indexFile.WriteAt(indexBuf, 0) + indexFile.Close() + } + + // Check that existing items have been moved to index 1M. + { + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 40, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + defer f.Close() + t.Log(f.dumpIndexString(0, 100)) + + checkRetrieveError(t, f, map[uint64]error{ + 0: errOutOfBounds, + 1: errOutOfBounds, + 2: errOutOfBounds, + 3: errOutOfBounds, + 999999: errOutOfBounds, + }) + checkRetrieve(t, f, map[uint64][]byte{ + 1000000: getChunk(20, 0xbb), + 1000001: getChunk(20, 0xaa), + }) + } +} + +func assertTableSize(t *testing.T, f *freezerTable, size int) { + t.Helper() + if got, err := f.size(); got != uint64(size) { + t.Fatalf("expected size of %d bytes, got %d, err: %v", size, got, err) + } +} + +func TestTruncateTail(t *testing.T) { + t.Parallel() + rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() + fname := fmt.Sprintf("truncate-tail-%d", rand.Uint64()) + + // Fill table + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 40, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + + // Write 7 x 20 bytes, splitting out into four files + batch := f.newBatch() + require.NoError(t, batch.AppendRaw(0, getChunk(20, 0xFF))) + require.NoError(t, batch.AppendRaw(1, getChunk(20, 0xEE))) + require.NoError(t, batch.AppendRaw(2, getChunk(20, 0xdd))) + require.NoError(t, batch.AppendRaw(3, getChunk(20, 0xcc))) + require.NoError(t, batch.AppendRaw(4, getChunk(20, 0xbb))) + require.NoError(t, batch.AppendRaw(5, getChunk(20, 0xaa))) + require.NoError(t, batch.AppendRaw(6, getChunk(20, 0x11))) + require.NoError(t, batch.commit()) + + // nothing to do, all the items should still be there. + f.truncateTail(0) + fmt.Println(f.dumpIndexString(0, 1000)) + checkRetrieve(t, f, map[uint64][]byte{ + 0: getChunk(20, 0xFF), + 1: getChunk(20, 0xEE), + 2: getChunk(20, 0xdd), + 3: getChunk(20, 0xcc), + 4: getChunk(20, 0xbb), + 5: getChunk(20, 0xaa), + 6: getChunk(20, 0x11), + }) + // maxFileSize*fileCount + headBytes + indexFileSize - hiddenBytes + expected := 20*7 + 48 - 0 + assertTableSize(t, f, expected) + + // truncate single element( item 0 ), deletion is only supported at file level + f.truncateTail(1) + fmt.Println(f.dumpIndexString(0, 1000)) + checkRetrieveError(t, f, map[uint64]error{ + 0: errOutOfBounds, + }) + checkRetrieve(t, f, map[uint64][]byte{ + 1: getChunk(20, 0xEE), + 2: getChunk(20, 0xdd), + 3: getChunk(20, 0xcc), + 4: getChunk(20, 0xbb), + 5: getChunk(20, 0xaa), + 6: getChunk(20, 0x11), + }) + expected = 20*7 + 48 - 20 + assertTableSize(t, f, expected) + + // Reopen the table, the deletion information should be persisted as well + f.Close() + f, err = newTable(os.TempDir(), fname, rm, wm, sg, 40, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + checkRetrieveError(t, f, map[uint64]error{ + 0: errOutOfBounds, + }) + checkRetrieve(t, f, map[uint64][]byte{ + 1: getChunk(20, 0xEE), + 2: getChunk(20, 0xdd), + 3: getChunk(20, 0xcc), + 4: getChunk(20, 0xbb), + 5: getChunk(20, 0xaa), + 6: getChunk(20, 0x11), + }) + + // truncate two elements( item 0, item 1 ), the file 0 should be deleted + f.truncateTail(2) + checkRetrieveError(t, f, map[uint64]error{ + 0: errOutOfBounds, + 1: errOutOfBounds, + }) + checkRetrieve(t, f, map[uint64][]byte{ + 2: getChunk(20, 0xdd), + 3: getChunk(20, 0xcc), + 4: getChunk(20, 0xbb), + 5: getChunk(20, 0xaa), + 6: getChunk(20, 0x11), + }) + expected = 20*5 + 36 - 0 + assertTableSize(t, f, expected) + + // Reopen the table, the above testing should still pass + f.Close() + f, err = newTable(os.TempDir(), fname, rm, wm, sg, 40, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + checkRetrieveError(t, f, map[uint64]error{ + 0: errOutOfBounds, + 1: errOutOfBounds, + }) + checkRetrieve(t, f, map[uint64][]byte{ + 2: getChunk(20, 0xdd), + 3: getChunk(20, 0xcc), + 4: getChunk(20, 0xbb), + 5: getChunk(20, 0xaa), + 6: getChunk(20, 0x11), + }) + + // truncate 3 more elements( item 2, 3, 4), the file 1 should be deleted + // file 2 should only contain item 5 + f.truncateTail(5) + checkRetrieveError(t, f, map[uint64]error{ + 0: errOutOfBounds, + 1: errOutOfBounds, + 2: errOutOfBounds, + 3: errOutOfBounds, + 4: errOutOfBounds, + }) + checkRetrieve(t, f, map[uint64][]byte{ + 5: getChunk(20, 0xaa), + 6: getChunk(20, 0x11), + }) + expected = 20*3 + 24 - 20 + assertTableSize(t, f, expected) + + // truncate all, the entire freezer should be deleted + f.truncateTail(7) + checkRetrieveError(t, f, map[uint64]error{ + 0: errOutOfBounds, + 1: errOutOfBounds, + 2: errOutOfBounds, + 3: errOutOfBounds, + 4: errOutOfBounds, + 5: errOutOfBounds, + 6: errOutOfBounds, + }) + expected = 12 + assertTableSize(t, f, expected) +} + +func TestTruncateHead(t *testing.T) { + t.Parallel() + rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() + fname := fmt.Sprintf("truncate-head-blow-tail-%d", rand.Uint64()) + + // Fill table + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 40, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + + // Write 7 x 20 bytes, splitting out into four files + batch := f.newBatch() + require.NoError(t, batch.AppendRaw(0, getChunk(20, 0xFF))) + require.NoError(t, batch.AppendRaw(1, getChunk(20, 0xEE))) + require.NoError(t, batch.AppendRaw(2, getChunk(20, 0xdd))) + require.NoError(t, batch.AppendRaw(3, getChunk(20, 0xcc))) + require.NoError(t, batch.AppendRaw(4, getChunk(20, 0xbb))) + require.NoError(t, batch.AppendRaw(5, getChunk(20, 0xaa))) + require.NoError(t, batch.AppendRaw(6, getChunk(20, 0x11))) + require.NoError(t, batch.commit()) + + f.truncateTail(4) // Tail = 4 + + // NewHead is required to be 3, the entire table should be truncated + f.truncateHead(4) + checkRetrieveError(t, f, map[uint64]error{ + 0: errOutOfBounds, // Deleted by tail + 1: errOutOfBounds, // Deleted by tail + 2: errOutOfBounds, // Deleted by tail + 3: errOutOfBounds, // Deleted by tail + 4: errOutOfBounds, // Deleted by Head + 5: errOutOfBounds, // Deleted by Head + 6: errOutOfBounds, // Deleted by Head + }) + + // Append new items + batch = f.newBatch() + require.NoError(t, batch.AppendRaw(4, getChunk(20, 0xbb))) + require.NoError(t, batch.AppendRaw(5, getChunk(20, 0xaa))) + require.NoError(t, batch.AppendRaw(6, getChunk(20, 0x11))) + require.NoError(t, batch.commit()) + + checkRetrieve(t, f, map[uint64][]byte{ + 4: getChunk(20, 0xbb), + 5: getChunk(20, 0xaa), + 6: getChunk(20, 0x11), + }) +} + +func checkRetrieve(t *testing.T, f *freezerTable, items map[uint64][]byte) { + t.Helper() + + for item, wantBytes := range items { + value, err := f.Retrieve(item) + if err != nil { + t.Fatalf("can't get expected item %d: %v", item, err) + } + if !bytes.Equal(value, wantBytes) { + t.Fatalf("item %d has wrong value %x (want %x)", item, value, wantBytes) + } + } +} + +func checkRetrieveError(t *testing.T, f *freezerTable, items map[uint64]error) { + t.Helper() + + for item, wantError := range items { + value, err := f.Retrieve(item) + if err == nil { + t.Fatalf("unexpected value %x for item %d, want error %v", item, value, wantError) + } + if err != wantError { + t.Fatalf("wrong error for item %d: %v", item, err) + } + } +} + +// Gets a chunk of data, filled with 'b' +func getChunk(size int, b int) []byte { + data := make([]byte, size) + for i := range data { + data[i] = byte(b) + } + return data +} + +// TODO (?) +// - test that if we remove several head-files, aswell as data last data-file, +// the index is truncated accordingly +// Right now, the freezer would fail on these conditions: +// 1. have data files d0, d1, d2, d3 +// 2. remove d2,d3 +// +// However, all 'normal' failure modes arising due to failing to sync() or save a file +// should be handled already, and the case described above can only (?) happen if an +// external process/user deletes files from the filesystem. + +func writeChunks(t *testing.T, ft *freezerTable, n int, length int) { + t.Helper() + + batch := ft.newBatch() + for i := 0; i < n; i++ { + if err := batch.AppendRaw(uint64(i), getChunk(length, i)); err != nil { + t.Fatalf("AppendRaw(%d, ...) returned error: %v", i, err) + } + } + if err := batch.commit(); err != nil { + t.Fatalf("Commit returned error: %v", err) + } +} + +// TestSequentialRead does some basic tests on the RetrieveItems. +func TestSequentialRead(t *testing.T) { + rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() + fname := fmt.Sprintf("batchread-%d", rand.Uint64()) + { // Fill table + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + // Write 15 bytes 30 times + writeChunks(t, f, 30, 15) + f.dumpIndexStdout(0, 30) + f.Close() + } + { // Open it, iterate, verify iteration + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + items, err := f.RetrieveItems(0, 10000, 100000) + if err != nil { + t.Fatal(err) + } + if have, want := len(items), 30; have != want { + t.Fatalf("want %d items, have %d ", want, have) + } + for i, have := range items { + want := getChunk(15, i) + if !bytes.Equal(want, have) { + t.Fatalf("data corruption: have\n%x\n, want \n%x\n", have, want) + } + } + f.Close() + } + { // Open it, iterate, verify byte limit. The byte limit is less than item + // size, so each lookup should only return one item + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 40, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + items, err := f.RetrieveItems(0, 10000, 10) + if err != nil { + t.Fatal(err) + } + if have, want := len(items), 1; have != want { + t.Fatalf("want %d items, have %d ", want, have) + } + for i, have := range items { + want := getChunk(15, i) + if !bytes.Equal(want, have) { + t.Fatalf("data corruption: have\n%x\n, want \n%x\n", have, want) + } + } + f.Close() + } +} + +// TestSequentialReadByteLimit does some more advanced tests on batch reads. +// These tests check that when the byte limit hits, we correctly abort in time, +// but also properly do all the deferred reads for the previous data, regardless +// of whether the data crosses a file boundary or not. +func TestSequentialReadByteLimit(t *testing.T) { + rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() + fname := fmt.Sprintf("batchread-2-%d", rand.Uint64()) + { // Fill table + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 100, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + // Write 10 bytes 30 times, + // Splitting it at every 100 bytes (10 items) + writeChunks(t, f, 30, 10) + f.Close() + } + for i, tc := range []struct { + items uint64 + limit uint64 + want int + }{ + {9, 89, 8}, + {10, 99, 9}, + {11, 109, 10}, + {100, 89, 8}, + {100, 99, 9}, + {100, 109, 10}, + } { + { + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 100, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + items, err := f.RetrieveItems(0, tc.items, tc.limit) + if err != nil { + t.Fatal(err) + } + if have, want := len(items), tc.want; have != want { + t.Fatalf("test %d: want %d items, have %d ", i, want, have) + } + for ii, have := range items { + want := getChunk(10, ii) + if !bytes.Equal(want, have) { + t.Fatalf("test %d: data corruption item %d: have\n%x\n, want \n%x\n", i, ii, have, want) + } + } + f.Close() + } + } +} + +// TestSequentialReadNoByteLimit tests the batch-read if maxBytes is not specified. +// Freezer should return the requested items regardless the size limitation. +func TestSequentialReadNoByteLimit(t *testing.T) { + rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge() + fname := fmt.Sprintf("batchread-3-%d", rand.Uint64()) + { // Fill table + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 100, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + // Write 10 bytes 30 times, + // Splitting it at every 100 bytes (10 items) + writeChunks(t, f, 30, 10) + f.Close() + } + for i, tc := range []struct { + items uint64 + want int + }{ + {1, 1}, + {30, 30}, + {31, 30}, + } { + { + f, err := newTable(os.TempDir(), fname, rm, wm, sg, 100, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + items, err := f.RetrieveItems(0, tc.items, 0) + if err != nil { + t.Fatal(err) + } + if have, want := len(items), tc.want; have != want { + t.Fatalf("test %d: want %d items, have %d ", i, want, have) + } + for ii, have := range items { + want := getChunk(10, ii) + if !bytes.Equal(want, have) { + t.Fatalf("test %d: data corruption item %d: have\n%x\n, want \n%x\n", i, ii, have, want) + } + } + f.Close() + } + } +} + +func TestFreezerReadonly(t *testing.T) { + tmpdir := os.TempDir() + // Case 1: Check it fails on non-existent file. + _, err := newTable(tmpdir, + fmt.Sprintf("readonlytest-%d", rand.Uint64()), + metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), 50, freezerTableConfig{noSnappy: true}, true) + if err == nil { + t.Fatal("readonly table instantiation should fail for non-existent table") + } + + // Case 2: Check that it fails on invalid index length. + fname := fmt.Sprintf("readonlytest-%d", rand.Uint64()) + idxFile, err := openFreezerFileForAppend(filepath.Join(tmpdir, fmt.Sprintf("%s.ridx", fname))) + if err != nil { + t.Errorf("Failed to open index file: %v\n", err) + } + // size should not be a multiple of indexEntrySize. + idxFile.Write(make([]byte, 17)) + idxFile.Close() + _, err = newTable(tmpdir, fname, + metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), 50, freezerTableConfig{noSnappy: true}, true) + if err == nil { + t.Errorf("readonly table instantiation should fail for invalid index size") + } + + // Case 3: Open table non-readonly table to write some data. + // Then corrupt the head file and make sure opening the table + // again in readonly triggers an error. + fname = fmt.Sprintf("readonlytest-%d", rand.Uint64()) + f, err := newTable(tmpdir, fname, + metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatalf("failed to instantiate table: %v", err) + } + writeChunks(t, f, 8, 32) + // Corrupt table file + if _, err := f.head.Write([]byte{1, 1}); err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } + _, err = newTable(tmpdir, fname, + metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), 50, freezerTableConfig{noSnappy: true}, true) + if err == nil { + t.Errorf("readonly table instantiation should fail for corrupt table file") + } + + // Case 4: Write some data to a table and later re-open it as readonly. + // Should be successful. + fname = fmt.Sprintf("readonlytest-%d", rand.Uint64()) + f, err = newTable(tmpdir, fname, + metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatalf("failed to instantiate table: %v\n", err) + } + writeChunks(t, f, 32, 128) + if err := f.Close(); err != nil { + t.Fatal(err) + } + f, err = newTable(tmpdir, fname, + metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), 50, freezerTableConfig{noSnappy: true}, true) + if err != nil { + t.Fatal(err) + } + v, err := f.Retrieve(10) + if err != nil { + t.Fatal(err) + } + exp := getChunk(128, 10) + if !bytes.Equal(v, exp) { + t.Errorf("retrieved value is incorrect") + } + + // Case 5: Now write some data via a batch. + // This should fail either during AppendRaw or Commit + batch := f.newBatch() + writeErr := batch.AppendRaw(32, make([]byte, 1)) + if writeErr == nil { + writeErr = batch.commit() + } + if writeErr == nil { + t.Fatalf("Writing to readonly table should fail") + } +} + +// randTest performs random freezer table operations. +// Instances of this test are created by Generate. +type randTest []randTestStep + +type randTestStep struct { + op int + items []uint64 // for append and retrieve + blobs [][]byte // for append + target uint64 // for truncate(head/tail) + err error // for debugging +} + +const ( + opReload = iota + opAppend + opRetrieve + opTruncateHead + opTruncateHeadAll + opTruncateTail + opTruncateTailAll + opCheckAll + opMax // boundary value, not an actual op +) + +func getVals(first uint64, n int) [][]byte { + var ret [][]byte + for i := 0; i < n; i++ { + val := make([]byte, 8) + binary.BigEndian.PutUint64(val, first+uint64(i)) + ret = append(ret, val) + } + return ret +} + +func (randTest) Generate(r *rand.Rand, size int) reflect.Value { + var ( + deleted uint64 // The number of deleted items from tail + items []uint64 // The index of entries in table + + // getItems retrieves the indexes for items in table. + getItems = func(n int) []uint64 { + length := len(items) + if length == 0 { + return nil + } + var ret []uint64 + index := rand.Intn(length) + for i := index; len(ret) < n && i < length; i++ { + ret = append(ret, items[i]) + } + return ret + } + + // addItems appends the given length items into the table. + addItems = func(n int) []uint64 { + var first = deleted + if len(items) != 0 { + first = items[len(items)-1] + 1 + } + var ret []uint64 + for i := 0; i < n; i++ { + ret = append(ret, first+uint64(i)) + } + items = append(items, ret...) + return ret + } + ) + + var steps randTest + for i := 0; i < size; i++ { + step := randTestStep{op: r.Intn(opMax)} + switch step.op { + case opReload, opCheckAll: + case opAppend: + num := r.Intn(3) + step.items = addItems(num) + if len(step.items) == 0 { + step.blobs = nil + } else { + step.blobs = getVals(step.items[0], num) + } + case opRetrieve: + step.items = getItems(r.Intn(3)) + case opTruncateHead: + if len(items) == 0 { + step.target = deleted + } else { + index := r.Intn(len(items)) + items = items[:index] + step.target = deleted + uint64(index) + } + case opTruncateHeadAll: + step.target = deleted + items = items[:0] + case opTruncateTail: + if len(items) == 0 { + step.target = deleted + } else { + index := r.Intn(len(items)) + items = items[index:] + deleted += uint64(index) + step.target = deleted + } + case opTruncateTailAll: + step.target = deleted + uint64(len(items)) + items = items[:0] + deleted = step.target + } + steps = append(steps, step) + } + return reflect.ValueOf(steps) +} + +func runRandTest(rt randTest) bool { + fname := fmt.Sprintf("randtest-%d", rand.Uint64()) + f, err := newTable(os.TempDir(), fname, metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + panic("failed to initialize table") + } + var values [][]byte + for i, step := range rt { + switch step.op { + case opReload: + f.Close() + f, err = newTable(os.TempDir(), fname, metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), 50, freezerTableConfig{noSnappy: true}, false) + if err != nil { + rt[i].err = fmt.Errorf("failed to reload table %v", err) + } + case opCheckAll: + tail := f.itemHidden.Load() + head := f.items.Load() + + if tail == head { + continue + } + got, err := f.RetrieveItems(f.itemHidden.Load(), head-tail, 100000) + if err != nil { + rt[i].err = err + } else { + if !reflect.DeepEqual(got, values) { + rt[i].err = fmt.Errorf("mismatch on retrieved values %v %v", got, values) + } + } + + case opAppend: + batch := f.newBatch() + for i := 0; i < len(step.items); i++ { + batch.AppendRaw(step.items[i], step.blobs[i]) + } + batch.commit() + values = append(values, step.blobs...) + + case opRetrieve: + var blobs [][]byte + if len(step.items) == 0 { + continue + } + tail := f.itemHidden.Load() + for i := 0; i < len(step.items); i++ { + blobs = append(blobs, values[step.items[i]-tail]) + } + got, err := f.RetrieveItems(step.items[0], uint64(len(step.items)), 100000) + if err != nil { + rt[i].err = err + } else { + if !reflect.DeepEqual(got, blobs) { + rt[i].err = fmt.Errorf("mismatch on retrieved values %v %v %v", got, blobs, step.items) + } + } + + case opTruncateHead: + f.truncateHead(step.target) + + length := f.items.Load() - f.itemHidden.Load() + values = values[:length] + + case opTruncateHeadAll: + f.truncateHead(step.target) + values = nil + + case opTruncateTail: + prev := f.itemHidden.Load() + f.truncateTail(step.target) + + truncated := f.itemHidden.Load() - prev + values = values[truncated:] + + case opTruncateTailAll: + f.truncateTail(step.target) + values = nil + } + // Abort the test on error. + if rt[i].err != nil { + return false + } + } + f.Close() + return true +} + +func TestRandom(t *testing.T) { + if err := quick.Check(runRandTest, nil); err != nil { + if cerr, ok := err.(*quick.CheckError); ok { + t.Fatalf("random test iteration %d failed: %s", cerr.Count, spew.Sdump(cerr.In)) + } + t.Fatal(err) + } +} + +func TestIndexValidation(t *testing.T) { + const dataSize = 10 + + garbage := indexEntry{ + filenum: 100, + offset: 200, + } + var cases = []struct { + write int + offset int64 + data []byte + expItems int + hasCorruption bool + }{ + // extend index file with zero bytes at the end + { + write: 5, + offset: (5 + 1) * indexEntrySize, + data: make([]byte, indexEntrySize), + expItems: 5, + }, + // extend index file with unaligned zero bytes at the end + { + write: 5, + offset: (5 + 1) * indexEntrySize, + data: make([]byte, indexEntrySize*1.5), + expItems: 5, + }, + // write garbage in the first non-head item + { + write: 5, + offset: indexEntrySize, + data: garbage.append(nil), + expItems: 0, + }, + // write garbage in the middle + { + write: 5, + offset: 3 * indexEntrySize, + data: garbage.append(nil), + expItems: 2, + }, + // fulfill the first data file (but not yet advanced), the zero bytes + // at tail should be truncated. + { + write: 10, + offset: 11 * indexEntrySize, + data: garbage.append(nil), + expItems: 10, + }, + } + for _, c := range cases { + fn := fmt.Sprintf("t-%d", rand.Uint64()) + f, err := newTable(os.TempDir(), fn, metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), 10*dataSize, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + writeChunks(t, f, c.write, dataSize) + + // write corrupted data + f.index.WriteAt(c.data, c.offset) + f.Close() + + // reopen the table, corruption should be truncated + f, err = newTable(os.TempDir(), fn, metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), 100, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + for i := 0; i < c.expItems; i++ { + exp := getChunk(10, i) + got, err := f.Retrieve(uint64(i)) + if err != nil && !c.hasCorruption { + t.Fatalf("Failed to read from table, %v", err) + } + if !bytes.Equal(exp, got) && !c.hasCorruption { + t.Fatalf("Unexpected item data, want: %v, got: %v", exp, got) + } + } + if f.items.Load() != uint64(c.expItems) { + t.Fatalf("Unexpected item number, want: %d, got: %d", c.expItems, f.items.Load()) + } + } +} + +// TestFlushOffsetTracking tests the flush offset tracking. The offset moving +// in the test is mostly triggered by the advanceHead (new data file) and +// heda/tail truncation. +func TestFlushOffsetTracking(t *testing.T) { + const ( + items = 35 + dataSize = 10 + fileSize = 100 + ) + fn := fmt.Sprintf("t-%d", rand.Uint64()) + f, err := newTable(os.TempDir(), fn, metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), fileSize, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + // Data files: + // F1(10 items) -> F2(10 items) -> F3(10 items) -> F4(5 items, non-full) + writeChunks(t, f, items, dataSize) + + var cases = []struct { + op func(*freezerTable) + offset int64 + }{ + { + // Data files: + // F1(10 items) -> F2(10 items) -> F3(10 items) -> F4(5 items, non-full) + func(f *freezerTable) {}, // no-op + 31 * indexEntrySize, + }, + { + // Write more items to fulfill the newest data file, but the file advance + // is not triggered. + + // Data files: + // F1(10 items) -> F2(10 items) -> F3(10 items) -> F4(10 items, full) + func(f *freezerTable) { + batch := f.newBatch() + for i := 0; i < 5; i++ { + batch.AppendRaw(items+uint64(i), make([]byte, dataSize)) + } + batch.commit() + }, + 31 * indexEntrySize, + }, + { + // Write more items to trigger the data file advance + + // Data files: + // F1(10 items) -> F2(10 items) -> F3(10 items) -> F4(10 items) -> F5(1 item) + func(f *freezerTable) { + batch := f.newBatch() + batch.AppendRaw(items+5, make([]byte, dataSize)) + batch.commit() + }, + 41 * indexEntrySize, + }, + { + // Head truncate + + // Data files: + // F1(10 items) -> F2(10 items) -> F3(10 items) -> F4(10 items) -> F5(0 item) + func(f *freezerTable) { + f.truncateHead(items + 5) + }, + 41 * indexEntrySize, + }, + { + // Tail truncate + + // Data files: + // F1(1 hidden, 9 visible) -> F2(10 items) -> F3(10 items) -> F4(10 items) -> F5(0 item) + func(f *freezerTable) { + f.truncateTail(1) + }, + 41 * indexEntrySize, + }, + { + // Tail truncate + + // Data files: + // F2(10 items) -> F3(10 items) -> F4(10 items) -> F5(0 item) + func(f *freezerTable) { + f.truncateTail(10) + }, + 31 * indexEntrySize, + }, + { + // Tail truncate + + // Data files: + // F4(10 items) -> F5(0 item) + func(f *freezerTable) { + f.truncateTail(30) + }, + 11 * indexEntrySize, + }, + { + // Head truncate + + // Data files: + // F4(9 items) + func(f *freezerTable) { + f.truncateHead(items + 4) + }, + 10 * indexEntrySize, + }, + } + for _, c := range cases { + c.op(f) + if f.metadata.flushOffset != c.offset { + t.Fatalf("Unexpected index flush offset, want: %d, got: %d", c.offset, f.metadata.flushOffset) + } + } +} + +func TestTailTruncationCrash(t *testing.T) { + const ( + items = 35 + dataSize = 10 + fileSize = 100 + ) + fn := fmt.Sprintf("t-%d", rand.Uint64()) + f, err := newTable(os.TempDir(), fn, metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), fileSize, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + // Data files: + // F1(10 items) -> F2(10 items) -> F3(10 items) -> F4(5 items, non-full) + writeChunks(t, f, items, dataSize) + + // The latest 5 items are not persisted yet + if f.metadata.flushOffset != 31*indexEntrySize { + t.Fatalf("Unexpected index flush offset, want: %d, got: %d", 31*indexEntrySize, f.metadata.flushOffset) + } + + f.truncateTail(5) + if f.metadata.flushOffset != 31*indexEntrySize { + t.Fatalf("Unexpected index flush offset, want: %d, got: %d", 31*indexEntrySize, f.metadata.flushOffset) + } + + // Truncate the first 10 items which results in the first data file + // being removed. The offset should be moved to 26*indexEntrySize. + f.truncateTail(10) + if f.metadata.flushOffset != 26*indexEntrySize { + t.Fatalf("Unexpected index flush offset, want: %d, got: %d", 26*indexEntrySize, f.metadata.flushOffset) + } + + // Write the offset back to 31*indexEntrySize to simulate a crash + // which occurs after truncating the index file without updating + // the offset + f.metadata.setFlushOffset(31*indexEntrySize, true) + + f, err = newTable(os.TempDir(), fn, metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), fileSize, freezerTableConfig{noSnappy: true}, false) + if err != nil { + t.Fatal(err) + } + if f.metadata.flushOffset != 26*indexEntrySize { + t.Fatalf("Unexpected index flush offset, want: %d, got: %d", 26*indexEntrySize, f.metadata.flushOffset) + } +} diff --git a/core/rawdb/freezer_test.go b/core/rawdb/freezer_test.go new file mode 100644 index 00000000000..f7a3ee07ca5 --- /dev/null +++ b/core/rawdb/freezer_test.go @@ -0,0 +1,482 @@ +// Copyright 2021 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rawdb + +import ( + "bytes" + "errors" + "fmt" + "math/big" + "math/rand" + "os" + "path" + "sync" + "testing" + + "github.com/XinFinOrg/XDPoSChain/ethdb" + "github.com/XinFinOrg/XDPoSChain/rlp" + "github.com/stretchr/testify/require" +) + +var freezerTestTableDef = map[string]freezerTableConfig{"test": {noSnappy: true}} + +func TestFreezerModify(t *testing.T) { + t.Parallel() + + // Create test data. + var valuesRaw [][]byte + var valuesRLP []*big.Int + for x := 0; x < 100; x++ { + v := getChunk(256, x) + valuesRaw = append(valuesRaw, v) + iv := big.NewInt(int64(x)) + iv = iv.Exp(iv, iv, nil) + valuesRLP = append(valuesRLP, iv) + } + + tables := map[string]freezerTableConfig{"raw": {noSnappy: true}, "rlp": {noSnappy: false}} + f, _ := newFreezerForTesting(t, tables) + defer f.Close() + + // Commit test data. + _, err := f.ModifyAncients(func(op ethdb.AncientWriteOp) error { + for i := range valuesRaw { + if err := op.AppendRaw("raw", uint64(i), valuesRaw[i]); err != nil { + return err + } + if err := op.Append("rlp", uint64(i), valuesRLP[i]); err != nil { + return err + } + } + return nil + }) + if err != nil { + t.Fatal("ModifyAncients failed:", err) + } + + // Dump indexes. + for _, table := range f.tables { + t.Log(table.name, "index:", table.dumpIndexString(0, int64(len(valuesRaw)))) + } + + // Read back test data. + checkAncientCount(t, f, "raw", uint64(len(valuesRaw))) + checkAncientCount(t, f, "rlp", uint64(len(valuesRLP))) + for i := range valuesRaw { + v, _ := f.Ancient("raw", uint64(i)) + if !bytes.Equal(v, valuesRaw[i]) { + t.Fatalf("wrong raw value at %d: %x", i, v) + } + ivEnc, _ := f.Ancient("rlp", uint64(i)) + want, _ := rlp.EncodeToBytes(valuesRLP[i]) + if !bytes.Equal(ivEnc, want) { + t.Fatalf("wrong RLP value at %d: %x", i, ivEnc) + } + } +} + +// This checks that ModifyAncients rolls back freezer updates +// when the function passed to it returns an error. +func TestFreezerModifyRollback(t *testing.T) { + t.Parallel() + + f, dir := newFreezerForTesting(t, freezerTestTableDef) + + theError := errors.New("oops") + _, err := f.ModifyAncients(func(op ethdb.AncientWriteOp) error { + // Append three items. This creates two files immediately, + // because the table size limit of the test freezer is 2048. + require.NoError(t, op.AppendRaw("test", 0, make([]byte, 2048))) + require.NoError(t, op.AppendRaw("test", 1, make([]byte, 2048))) + require.NoError(t, op.AppendRaw("test", 2, make([]byte, 2048))) + return theError + }) + if err != theError { + t.Errorf("ModifyAncients returned wrong error %q", err) + } + checkAncientCount(t, f, "test", 0) + f.Close() + + // 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) + if err != nil { + t.Fatalf("can't reopen freezer after failed ModifyAncients: %v", err) + } + defer f2.Close() + checkAncientCount(t, f2, "test", 0) +} + +// This test runs ModifyAncients and Ancient concurrently with each other. +func TestFreezerConcurrentModifyRetrieve(t *testing.T) { + t.Parallel() + + f, _ := newFreezerForTesting(t, freezerTestTableDef) + defer f.Close() + + var ( + numReaders = 5 + writeBatchSize = uint64(50) + written = make(chan uint64, numReaders*6) + wg sync.WaitGroup + ) + wg.Add(numReaders + 1) + + // Launch the writer. It appends 10000 items in batches. + go func() { + defer wg.Done() + defer close(written) + for item := uint64(0); item < 10000; item += writeBatchSize { + _, err := f.ModifyAncients(func(op ethdb.AncientWriteOp) error { + for i := uint64(0); i < writeBatchSize; i++ { + item := item + i + value := getChunk(32, int(item)) + if err := op.AppendRaw("test", item, value); err != nil { + return err + } + } + return nil + }) + if err != nil { + panic(err) + } + for i := 0; i < numReaders; i++ { + written <- item + writeBatchSize + } + } + }() + + // Launch the readers. They read random items from the freezer up to the + // current frozen item count. + for i := 0; i < numReaders; i++ { + go func() { + defer wg.Done() + for frozen := range written { + for rc := 0; rc < 80; rc++ { + num := uint64(rand.Intn(int(frozen))) + value, err := f.Ancient("test", num) + if err != nil { + panic(fmt.Errorf("error reading %d (frozen %d): %v", num, frozen, err)) + } + if !bytes.Equal(value, getChunk(32, int(num))) { + panic(fmt.Errorf("wrong value at %d", num)) + } + } + } + }() + } + + wg.Wait() +} + +// This test runs ModifyAncients and TruncateHead concurrently with each other. +func TestFreezerConcurrentModifyTruncate(t *testing.T) { + f, _ := newFreezerForTesting(t, freezerTestTableDef) + defer f.Close() + + var item = make([]byte, 256) + + for i := 0; i < 10; i++ { + // First reset and write 100 items. + if err := f.TruncateHead(0); err != nil { + t.Fatal("truncate failed:", err) + } + _, err := f.ModifyAncients(func(op ethdb.AncientWriteOp) error { + for i := uint64(0); i < 100; i++ { + if err := op.AppendRaw("test", i, item); err != nil { + return err + } + } + return nil + }) + if err != nil { + t.Fatal("modify failed:", err) + } + checkAncientCount(t, f, "test", 100) + + // Now append 100 more items and truncate concurrently. + var ( + wg sync.WaitGroup + truncateErr error + modifyErr error + ) + wg.Add(3) + go func() { + _, modifyErr = f.ModifyAncients(func(op ethdb.AncientWriteOp) error { + for i := uint64(100); i < 200; i++ { + if err := op.AppendRaw("test", i, item); err != nil { + return err + } + } + return nil + }) + wg.Done() + }() + go func() { + truncateErr = f.TruncateHead(10) + wg.Done() + }() + go func() { + f.AncientSize("test") + wg.Done() + }() + wg.Wait() + + // Now check the outcome. If the truncate operation went through first, the append + // fails, otherwise it succeeds. In either case, the freezer should be positioned + // at 10 after both operations are done. + if truncateErr != nil { + t.Fatal("concurrent truncate failed:", err) + } + if !(errors.Is(modifyErr, nil) || errors.Is(modifyErr, errOutOrderInsertion)) { + t.Fatal("wrong error from concurrent modify:", modifyErr) + } + checkAncientCount(t, f, "test", 10) + } +} + +func TestFreezerReadonlyValidate(t *testing.T) { + tables := map[string]freezerTableConfig{"a": {noSnappy: true}, "b": {noSnappy: true}} + dir := t.TempDir() + // Open non-readonly freezer and fill individual tables + // with different amount of data. + f, err := NewFreezer(dir, "", false, 2049, tables) + if err != nil { + t.Fatal("can't open freezer", err) + } + var item = make([]byte, 1024) + aBatch := f.tables["a"].newBatch() + require.NoError(t, aBatch.AppendRaw(0, item)) + require.NoError(t, aBatch.AppendRaw(1, item)) + require.NoError(t, aBatch.AppendRaw(2, item)) + require.NoError(t, aBatch.commit()) + bBatch := f.tables["b"].newBatch() + require.NoError(t, bBatch.AppendRaw(0, item)) + require.NoError(t, bBatch.commit()) + if f.tables["a"].items.Load() != 3 { + t.Fatalf("unexpected number of items in table") + } + if f.tables["b"].items.Load() != 1 { + t.Fatalf("unexpected number of items in table") + } + require.NoError(t, f.Close()) + + // Re-openening as readonly should fail when validating + // table lengths. + _, err = NewFreezer(dir, "", true, 2049, tables) + if err == nil { + t.Fatal("readonly freezer should fail with differing table lengths") + } +} + +func TestFreezerConcurrentReadonly(t *testing.T) { + t.Parallel() + + tables := map[string]freezerTableConfig{"a": {noSnappy: true}} + dir := t.TempDir() + + f, err := NewFreezer(dir, "", false, 2049, tables) + if err != nil { + t.Fatal("can't open freezer", err) + } + var item = make([]byte, 1024) + batch := f.tables["a"].newBatch() + items := uint64(10) + for i := uint64(0); i < items; i++ { + require.NoError(t, batch.AppendRaw(i, item)) + } + require.NoError(t, batch.commit()) + if loaded := f.tables["a"].items.Load(); loaded != items { + t.Fatalf("unexpected number of items in table, want: %d, have: %d", items, loaded) + } + require.NoError(t, f.Close()) + + var ( + wg sync.WaitGroup + fs = make([]*Freezer, 5) + errs = make([]error, 5) + ) + for i := 0; i < 5; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + + f, err := NewFreezer(dir, "", true, 2049, tables) + if err == nil { + fs[i] = f + } else { + errs[i] = err + } + }(i) + } + + wg.Wait() + + for i := range fs { + if err := errs[i]; err != nil { + t.Fatal("failed to open freezer", err) + } + require.NoError(t, fs[i].Close()) + } +} + +func newFreezerForTesting(t *testing.T, tables map[string]freezerTableConfig) (*Freezer, string) { + t.Helper() + + 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) + if err != nil { + t.Fatal("can't open freezer", err) + } + return f, dir +} + +// checkAncientCount verifies that the freezer contains n items. +func checkAncientCount(t *testing.T, f *Freezer, kind string, n uint64) { + t.Helper() + + if frozen, _ := f.Ancients(); frozen != n { + t.Fatalf("Ancients() returned %d, want %d", frozen, n) + } + + // Check at index n-1. + if n > 0 { + index := n - 1 + if ok, _ := f.HasAncient(kind, index); !ok { + t.Errorf("HasAncient(%q, %d) returned false unexpectedly", kind, index) + } + if _, err := f.Ancient(kind, index); err != nil { + t.Errorf("Ancient(%q, %d) returned unexpected error %q", kind, index, err) + } + } + + // Check at index n. + index := n + if ok, _ := f.HasAncient(kind, index); ok { + t.Errorf("HasAncient(%q, %d) returned true unexpectedly", kind, index) + } + if _, err := f.Ancient(kind, index); err == nil { + t.Errorf("Ancient(%q, %d) didn't return expected error", kind, index) + } else if err != errOutOfBounds { + t.Errorf("Ancient(%q, %d) returned unexpected error %q", kind, index, err) + } +} + +func TestRenameWindows(t *testing.T) { + var ( + fname = "file.bin" + fname2 = "file2.bin" + data = []byte{1, 2, 3, 4} + data2 = []byte{2, 3, 4, 5} + data3 = []byte{3, 5, 6, 7} + dataLen = 4 + ) + + // Create 2 temp dirs + dir1 := t.TempDir() + dir2 := t.TempDir() + + // Create file in dir1 and fill with data + f, err := os.Create(path.Join(dir1, fname)) + if err != nil { + t.Fatal(err) + } + f2, err := os.Create(path.Join(dir1, fname2)) + if err != nil { + t.Fatal(err) + } + f3, err := os.Create(path.Join(dir2, fname2)) + if err != nil { + t.Fatal(err) + } + if _, err := f.Write(data); err != nil { + t.Fatal(err) + } + if _, err := f2.Write(data2); err != nil { + t.Fatal(err) + } + if _, err := f3.Write(data3); err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } + if err := f2.Close(); err != nil { + t.Fatal(err) + } + if err := f3.Close(); err != nil { + t.Fatal(err) + } + if err := os.Rename(f.Name(), path.Join(dir2, fname)); err != nil { + t.Fatal(err) + } + if err := os.Rename(f2.Name(), path.Join(dir2, fname2)); err != nil { + t.Fatal(err) + } + + // Check file contents + f, err = os.Open(path.Join(dir2, fname)) + if err != nil { + t.Fatal(err) + } + defer f.Close() + defer os.Remove(f.Name()) + buf := make([]byte, dataLen) + if _, err := f.Read(buf); err != nil { + t.Fatal(err) + } + if !bytes.Equal(buf, data) { + t.Errorf("unexpected file contents. Got %v\n", buf) + } + + f, err = os.Open(path.Join(dir2, fname2)) + if err != nil { + t.Fatal(err) + } + defer f.Close() + defer os.Remove(f.Name()) + if _, err := f.Read(buf); err != nil { + t.Fatal(err) + } + if !bytes.Equal(buf, data2) { + t.Errorf("unexpected file contents. Got %v\n", buf) + } +} + +func TestFreezerCloseSync(t *testing.T) { + t.Parallel() + f, _ := newFreezerForTesting(t, map[string]freezerTableConfig{"a": {noSnappy: true}, "b": {noSnappy: true}}) + defer f.Close() + + // Now, close and sync. This mimics the behaviour if the node is shut down, + // just as the chain freezer is writing. + // 1: thread-1: chain treezer writes, via freezeRange (holds lock) + // 2: thread-2: Close called, waits for write to finish + // 3: thread-1: finishes writing, releases lock + // 4: thread-2: obtains lock, completes Close() + // 5: thread-1: calls f.Sync() + if err := f.Close(); err != nil { + t.Fatal(err) + } + if err := f.Sync(); err == nil { + t.Fatalf("want error, have nil") + } else if have, want := err.Error(), "[closed closed]"; have != want { + t.Fatalf("want %v, have %v", have, want) + } +} diff --git a/core/rawdb/freezer_utils.go b/core/rawdb/freezer_utils.go new file mode 100644 index 00000000000..752e95ba6ae --- /dev/null +++ b/core/rawdb/freezer_utils.go @@ -0,0 +1,131 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rawdb + +import ( + "io" + "os" + "path/filepath" +) + +// copyFrom copies data from 'srcPath' at offset 'offset' into 'destPath'. +// The 'destPath' is created if it doesn't exist, otherwise it is overwritten. +// Before the copy is executed, there is a callback can be registered to +// manipulate the dest file. +// It is perfectly valid to have destPath == srcPath. +func copyFrom(srcPath, destPath string, offset uint64, before func(f *os.File) error) error { + // Create a temp file in the same dir where we want it to wind up + f, err := os.CreateTemp(filepath.Dir(destPath), "*") + if err != nil { + return err + } + fname := f.Name() + + // Clean up the leftover file + defer func() { + if f != nil { + f.Close() + } + os.Remove(fname) + }() + // Apply the given function if it's not nil before we copy + // the content from the src. + if before != nil { + if err := before(f); err != nil { + return err + } + } + // Open the source file + src, err := os.Open(srcPath) + if err != nil { + return err + } + if _, err = src.Seek(int64(offset), 0); err != nil { + src.Close() + return err + } + // io.Copy uses 32K buffer internally. + _, err = io.Copy(f, src) + if err != nil { + src.Close() + return err + } + // Rename the temporary file to the specified dest name. + // src may be same as dest, so needs to be closed before + // we do the final move. + src.Close() + + if err := f.Close(); err != nil { + return err + } + f = nil + return os.Rename(fname, destPath) +} + +// openFreezerFileForAppend opens a freezer table file and seeks to the end +func openFreezerFileForAppend(filename string) (*os.File, error) { + // Open the file without the O_APPEND flag + // because it has differing behaviour during Truncate operations + // on different OS's + file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return nil, err + } + // Seek to end for append + if _, err = file.Seek(0, io.SeekEnd); err != nil { + return nil, err + } + return file, nil +} + +// openFreezerFileForReadOnly opens a freezer table file for read only access +func openFreezerFileForReadOnly(filename string) (*os.File, error) { + return os.OpenFile(filename, os.O_RDONLY, 0644) +} + +// openFreezerFileTruncated opens a freezer table making sure it is truncated +func openFreezerFileTruncated(filename string) (*os.File, error) { + return os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) +} + +// truncateFreezerFile resizes a freezer table file and seeks to the end +func truncateFreezerFile(file *os.File, size int64) error { + if err := file.Truncate(size); err != nil { + return err + } + // Seek to end for append + if _, err := file.Seek(0, io.SeekEnd); err != nil { + return err + } + return nil +} + +// grow prepares the slice space for new item, and doubles the slice capacity +// if space is not enough. +func grow(buf []byte, n int) []byte { + if cap(buf)-len(buf) < n { + newcap := 2 * cap(buf) + if newcap-len(buf) < n { + newcap = len(buf) + n + } + nbuf := make([]byte, len(buf), newcap) + copy(nbuf, buf) + buf = nbuf + } + buf = buf[:len(buf)+n] + return buf +} diff --git a/core/rawdb/freezer_utils_test.go b/core/rawdb/freezer_utils_test.go new file mode 100644 index 00000000000..de8087f9b93 --- /dev/null +++ b/core/rawdb/freezer_utils_test.go @@ -0,0 +1,76 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rawdb + +import ( + "bytes" + "io/ioutil" + "os" + "testing" +) + +func TestCopyFrom(t *testing.T) { + var ( + content = []byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8} + prefix = []byte{0x9, 0xa, 0xb, 0xc, 0xd, 0xf} + ) + var cases = []struct { + src, dest string + offset uint64 + writePrefix bool + }{ + {"foo", "bar", 0, false}, + {"foo", "bar", 1, false}, + {"foo", "bar", 8, false}, + {"foo", "foo", 0, false}, + {"foo", "foo", 1, false}, + {"foo", "foo", 8, false}, + {"foo", "bar", 0, true}, + {"foo", "bar", 1, true}, + {"foo", "bar", 8, true}, + } + for _, c := range cases { + ioutil.WriteFile(c.src, content, 0644) + + if err := copyFrom(c.src, c.dest, c.offset, func(f *os.File) error { + if !c.writePrefix { + return nil + } + f.Write(prefix) + return nil + }); err != nil { + os.Remove(c.src) + t.Fatalf("Failed to copy %v", err) + } + + blob, err := ioutil.ReadFile(c.dest) + if err != nil { + os.Remove(c.src) + os.Remove(c.dest) + t.Fatalf("Failed to read %v", err) + } + want := content[c.offset:] + if c.writePrefix { + want = append(prefix, want...) + } + if !bytes.Equal(blob, want) { + t.Fatal("Unexpected value") + } + os.Remove(c.src) + os.Remove(c.dest) + } +} diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 3171acfe844..490337b1616 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -39,9 +39,24 @@ var ( // headFastBlockKey tracks the latest known incomplete block's hash during fast sync. headFastBlockKey = []byte("LastFast") + // lastPivotKey tracks the last pivot block used by fast sync (to reenable on sethead). + lastPivotKey = []byte("LastPivot") + // fastTrieProgressKey tracks the number of trie entries imported during fast sync. fastTrieProgressKey = []byte("TrieSync") + // txIndexTailKey tracks the oldest block whose transactions have been indexed. + txIndexTailKey = []byte("TransactionIndexTail") + + // fastTxLookupLimitKey tracks the transaction lookup limit during fast sync. + fastTxLookupLimitKey = []byte("FastTransactionLookupLimit") + + // badBlockKey tracks the list of bad blocks seen by local + badBlockKey = []byte("InvalidBlock") + + // uncleanShutdownKey tracks the list of local crashes + uncleanShutdownKey = []byte("unclean-shutdown") // config prefix for the db + // Data item prefixes (use single byte to avoid mixing data types, avoid `i`, used for indexes). headerPrefix = []byte("h") // headerPrefix + num (uint64 big endian) + hash -> header headerTDSuffix = []byte("t") // headerPrefix + num (uint64 big endian) + hash + headerTDSuffix -> td @@ -53,7 +68,7 @@ var ( txLookupPrefix = []byte("l") // txLookupPrefix + hash -> transaction/receipt lookup metadata bloomBitsPrefix = []byte("B") // bloomBitsPrefix + bit (uint16 big endian) + section (uint64 big endian) + hash -> bloom bits - codePrefix = []byte("c") // codePrefix + code hash -> account code + CodePrefix = []byte("c") // codePrefix + code hash -> account code // used by old db, now only used for conversion oldReceiptsPrefix = []byte("receipts-") @@ -71,23 +86,6 @@ var ( preimageHitCounter = metrics.NewRegisteredCounter("db/preimage/hits", nil) ) -const ( - // freezerHeaderTable indicates the name of the freezer header table. - freezerHeaderTable = "headers" - - // freezerHashTable indicates the name of the freezer canonical hash table. - freezerHashTable = "hashes" - - // freezerBodiesTable indicates the name of the freezer block body table. - freezerBodiesTable = "bodies" - - // freezerReceiptTable indicates the name of the freezer receipts table. - freezerReceiptTable = "receipts" - - // freezerDifficultyTable indicates the name of the freezer total difficulty table. - freezerDifficultyTable = "diffs" -) - // LegacyTxLookupEntry is the legacy TxLookupEntry definition with some unnecessary // fields. type LegacyTxLookupEntry struct { @@ -160,14 +158,14 @@ func preimageKey(hash common.Hash) []byte { // codeKey = codePrefix + hash func codeKey(hash common.Hash) []byte { - return append(codePrefix, hash.Bytes()...) + return append(CodePrefix, hash.Bytes()...) } // IsCodeKey reports whether the given byte slice is the key of contract code, // if so return the raw code hash as well. func IsCodeKey(key []byte) (bool, []byte) { - if bytes.HasPrefix(key, codePrefix) && len(key) == common.HashLength+len(codePrefix) { - return true, key[len(codePrefix):] + if bytes.HasPrefix(key, CodePrefix) && len(key) == common.HashLength+len(CodePrefix) { + return true, key[len(CodePrefix):] } return false, nil } diff --git a/core/rawdb/table.go b/core/rawdb/table.go index dfb58c07b62..c596234ea64 100644 --- a/core/rawdb/table.go +++ b/core/rawdb/table.go @@ -62,28 +62,49 @@ func (t *table) Ancient(kind string, number uint64) ([]byte, error) { return t.db.Ancient(kind, number) } +// AncientRange is a noop passthrough that just forwards the request to the underlying +// database. +func (t *table) AncientRange(kind string, start, count, maxBytes uint64) ([][]byte, error) { + return t.db.AncientRange(kind, start, count, maxBytes) +} + // Ancients is a noop passthrough that just forwards the request to the underlying // database. func (t *table) Ancients() (uint64, error) { return t.db.Ancients() } +// Tail is a noop passthrough that just forwards the request to the underlying +// database. +func (t *table) Tail() (uint64, error) { + return t.db.Tail() +} + // AncientSize is a noop passthrough that just forwards the request to the underlying // database. func (t *table) AncientSize(kind string) (uint64, error) { return t.db.AncientSize(kind) } -// AppendAncient is a noop passthrough that just forwards the request to the underlying +// ModifyAncients runs an ancient write operation on the underlying database. +func (t *table) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (int64, error) { + return t.db.ModifyAncients(fn) +} + +func (t *table) ReadAncients(fn func(reader ethdb.AncientReaderOp) error) (err error) { + return t.db.ReadAncients(fn) +} + +// TruncateHead is a noop passthrough that just forwards the request to the underlying // database. -func (t *table) AppendAncient(number uint64, hash, header, body, receipts, td []byte) error { - return t.db.AppendAncient(number, hash, header, body, receipts, td) +func (t *table) TruncateHead(items uint64) error { + return t.db.TruncateHead(items) } -// TruncateAncients is a noop passthrough that just forwards the request to the underlying +// TruncateTail is a noop passthrough that just forwards the request to the underlying // database. -func (t *table) TruncateAncients(items uint64) error { - return t.db.TruncateAncients(items) +func (t *table) TruncateTail(items uint64) error { + return t.db.TruncateTail(items) } // Sync is a noop passthrough that just forwards the request to the underlying @@ -92,6 +113,17 @@ func (t *table) Sync() error { return t.db.Sync() } +// MigrateTable processes the entries in a given table in sequence +// converting them to a new format if they're of an old format. +func (t *table) MigrateTable(kind string, convert convertLegacyFn) error { + return t.db.MigrateTable(kind, convert) +} + +// AncientDatadir returns the ancient datadir of the underlying database. +func (t *table) AncientDatadir() (string, error) { + return t.db.AncientDatadir() +} + // Put inserts the given value into the database at a prefixed version of the // provided key. func (t *table) Put(key []byte, value []byte) error { diff --git a/core/rlp_test.go b/core/rlp_test.go new file mode 100644 index 00000000000..eab0853129a --- /dev/null +++ b/core/rlp_test.go @@ -0,0 +1,204 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "fmt" + "math/big" + "testing" + + "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/consensus/ethash" + "github.com/XinFinOrg/XDPoSChain/core/rawdb" + "github.com/XinFinOrg/XDPoSChain/core/types" + "github.com/XinFinOrg/XDPoSChain/crypto" + "github.com/XinFinOrg/XDPoSChain/params" + "github.com/XinFinOrg/XDPoSChain/rlp" + "golang.org/x/crypto/sha3" +) + +func getBlock(transactions int, uncles int, dataSize int) *types.Block { + config := *params.TestChainConfig + + var ( + aa = common.HexToAddress("0x000000000000000000000000000000000000aaaa") + // Generate a canonical chain to act as the main dataset + engine = ethash.NewFaker() + db = rawdb.NewMemoryDatabase() + // A sender who makes transactions, has some funds + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + address = crypto.PubkeyToAddress(key.PublicKey) + funds = big.NewInt(1_000_000_000_000_000_000) + + gspec = &Genesis{ + Config: &config, + Alloc: GenesisAlloc{address: {Balance: funds}}, + } + genesis = gspec.MustCommit(db) + ) + + // We need to generate as many blocks +1 as uncles + blocks, _ := GenerateChain(&config, genesis, engine, db, uncles+1, + func(n int, b *BlockGen) { + if n == uncles { + // Add transactions and stuff on the last block + for i := 0; i < transactions; i++ { + tx, _ := types.SignTx(types.NewTransaction(uint64(i), aa, + big.NewInt(0), 50000, b.header.BaseFee, make([]byte, dataSize)), types.HomesteadSigner{}, key) + b.AddTx(tx) + } + for i := 0; i < uncles; i++ { + b.AddUncle(&types.Header{ParentHash: b.PrevBlock(n - 1 - i).Hash(), Number: big.NewInt(int64(n - i))}) + } + } + }) + block := blocks[len(blocks)-1] + return block +} + +// TestRlpIterator tests that individual transactions can be picked out +// from blocks without full unmarshalling/marshalling +func TestRlpIterator(t *testing.T) { + for _, tt := range []struct { + txs int + uncles int + datasize int + }{ + {0, 0, 0}, + {0, 2, 0}, + {10, 0, 0}, + {10, 2, 0}, + {10, 2, 50}, + } { + testRlpIterator(t, tt.txs, tt.uncles, tt.datasize) + } +} + +func testRlpIterator(t *testing.T, txs, uncles, datasize int) { + desc := fmt.Sprintf("%d txs [%d datasize] and %d uncles", txs, datasize, uncles) + bodyRlp, _ := rlp.EncodeToBytes(getBlock(txs, uncles, datasize).Body()) + it, err := rlp.NewListIterator(bodyRlp) + if err != nil { + t.Fatal(err) + } + // Check that txs exist + if !it.Next() { + t.Fatal("expected two elems, got zero") + } + txdata := it.Value() + // Check that uncles exist + if !it.Next() { + t.Fatal("expected two elems, got one") + } + // No more after that + if it.Next() { + t.Fatal("expected only two elems, got more") + } + txIt, err := rlp.NewListIterator(txdata) + if err != nil { + t.Fatal(err) + } + var gotHashes []common.Hash + var expHashes []common.Hash + for txIt.Next() { + gotHashes = append(gotHashes, crypto.Keccak256Hash(txIt.Value())) + } + + var expBody types.Body + err = rlp.DecodeBytes(bodyRlp, &expBody) + if err != nil { + t.Fatal(err) + } + for _, tx := range expBody.Transactions { + expHashes = append(expHashes, tx.Hash()) + } + if gotLen, expLen := len(gotHashes), len(expHashes); gotLen != expLen { + t.Fatalf("testcase %v: length wrong, got %d exp %d", desc, gotLen, expLen) + } + // also sanity check against input + if gotLen := len(gotHashes); gotLen != txs { + t.Fatalf("testcase %v: length wrong, got %d exp %d", desc, gotLen, txs) + } + for i, got := range gotHashes { + if exp := expHashes[i]; got != exp { + t.Errorf("testcase %v: hash wrong, got %x, exp %x", desc, got, exp) + } + } +} + +// BenchmarkHashing compares the speeds of hashing a rlp raw data directly +// without the unmarshalling/marshalling step +func BenchmarkHashing(b *testing.B) { + // Make a pretty fat block + var ( + bodyRlp []byte + blockRlp []byte + ) + { + block := getBlock(200, 2, 50) + bodyRlp, _ = rlp.EncodeToBytes(block.Body()) + blockRlp, _ = rlp.EncodeToBytes(block) + } + var got common.Hash + var hasher = sha3.NewLegacyKeccak256() + b.Run("iteratorhashing", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + var hash common.Hash + it, err := rlp.NewListIterator(bodyRlp) + if err != nil { + b.Fatal(err) + } + it.Next() + txs := it.Value() + txIt, err := rlp.NewListIterator(txs) + if err != nil { + b.Fatal(err) + } + for txIt.Next() { + hasher.Reset() + hasher.Write(txIt.Value()) + hasher.Sum(hash[:0]) + got = hash + } + } + }) + var exp common.Hash + b.Run("fullbodyhashing", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + var body types.Body + rlp.DecodeBytes(bodyRlp, &body) + for _, tx := range body.Transactions { + exp = tx.Hash() + } + } + }) + b.Run("fullblockhashing", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + var block types.Block + rlp.DecodeBytes(blockRlp, &block) + for _, tx := range block.Transactions() { + tx.Hash() + } + } + }) + if got != exp { + b.Fatalf("hash wrong, got %x exp %x", got, exp) + } +} diff --git a/core/state/state_object.go b/core/state/state_object.go index f24702e4c22..cc851d80651 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -59,7 +59,7 @@ func (s Storage) Copy() Storage { // The usage pattern is as follows: // First you need to obtain a state object. // Account values can be accessed and modified through the object. -// Finally, call CommitTrie to write the modified storage trie into a database. +// Finally, call commitTrie to write the modified storage trie into a database. type stateObject struct { db *StateDB address common.Address // address of ethereum account @@ -335,9 +335,9 @@ func (s *stateObject) updateRoot(db Database) { s.data.Root = s.trie.Hash() } -// CommitTrie the storage trie of the object to dwb. -// This updates the trie root. -func (s *stateObject) CommitTrie(db Database) error { +// commitTrie submits the storage changes into the storage trie and re-computes +// the root. Besides, all trie changes will be collected in a nodeset and returned. +func (s *stateObject) commitTrie(db Database) error { // If nothing changed, don't bother with hashing anything if s.updateTrie(db) == nil { return nil diff --git a/core/state/statedb.go b/core/state/statedb.go index 8661982cb3d..bb19a7fab85 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -824,10 +824,16 @@ func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, error) { obj.dirtyCode = false } // Write any storage changes in the state object to its storage trie. - if err := obj.CommitTrie(s.db); err != nil { + if err := obj.commitTrie(s.db); err != nil { return common.Hash{}, err } } + // If the contract is destructed, the storage is still left in the + // database as dangling data. Theoretically it's should be wiped from + // database as well, but in hash-based-scheme it's extremely hard to + // determine that if the trie nodes are also referenced by other storage, + // and in path-based-scheme some technical challenges are still unsolved. + // Although it won't affect the correctness but please fix it TODO(rjl493456442). } if len(s.stateObjectsDirty) > 0 { s.stateObjectsDirty = make(map[common.Address]struct{}) diff --git a/core/state_processor_test.go b/core/state_processor_test.go index 830e2badc65..e29cde097e2 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -88,7 +88,7 @@ func TestStateProcessorErrors(t *testing.T) { }, } genesis = gspec.MustCommit(db) - blockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}) + blockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) ) defer blockchain.Stop() bigNumber := new(big.Int).SetBytes(common.MaxHash.Bytes()) @@ -222,7 +222,7 @@ func TestStateProcessorErrors(t *testing.T) { }, } genesis = gspec.MustCommit(db) - blockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}) + blockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil) ) defer blockchain.Stop() for i, tt := range []struct { diff --git a/core/types/block.go b/core/types/block.go index 24bb9ff69de..11820cd005b 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -486,3 +486,21 @@ func (bs blockSorter) Swap(i, j int) { func (bs blockSorter) Less(i, j int) bool { return bs.by(bs.blocks[i], bs.blocks[j]) } func Number(b1, b2 *Block) bool { return b1.header.Number.Cmp(b2.header.Number) < 0 } + +// HeaderParentHashFromRLP returns the parentHash of an RLP-encoded +// header. If 'header' is invalid, the zero hash is returned. +func HeaderParentHashFromRLP(header []byte) common.Hash { + // parentHash is the first list element. + listContent, _, err := rlp.SplitList(header) + if err != nil { + return common.Hash{} + } + parentHash, _, err := rlp.SplitString(listContent) + if err != nil { + return common.Hash{} + } + if len(parentHash) != 32 { + return common.Hash{} + } + return common.BytesToHash(parentHash) +} diff --git a/core/types/block_test.go b/core/types/block_test.go index 4c496b29017..edd07773937 100644 --- a/core/types/block_test.go +++ b/core/types/block_test.go @@ -19,6 +19,7 @@ package types import ( "bytes" "hash" + go_math "math" "math/big" "reflect" "testing" @@ -213,3 +214,64 @@ func makeBenchBlock() *Block { } return NewBlock(header, txs, uncles, receipts, newHasher()) } + +func TestRlpDecodeParentHash(t *testing.T) { + // A minimum one + want := common.HexToHash("0x112233445566778899001122334455667788990011223344556677889900aabb") + if rlpData, err := rlp.EncodeToBytes(Header{ParentHash: want}); err != nil { + t.Fatal(err) + } else { + if have := HeaderParentHashFromRLP(rlpData); have != want { + t.Fatalf("have %x, want %x", have, want) + } + } + // And a maximum one + // | Difficulty | dynamic| *big.Int | 0x5ad3c2c71bbff854908 (current mainnet TD: 76 bits) | + // | Number | dynamic| *big.Int | 64 bits | + // | Extra | dynamic| []byte | 65+32 byte (clique) | + // | BaseFee | dynamic| *big.Int | 64 bits | + mainnetTd := new(big.Int) + mainnetTd.SetString("5ad3c2c71bbff854908", 16) + if rlpData, err := rlp.EncodeToBytes(Header{ + ParentHash: want, + Difficulty: mainnetTd, + Number: new(big.Int).SetUint64(go_math.MaxUint64), + Extra: make([]byte, 65+32), + BaseFee: new(big.Int).SetUint64(go_math.MaxUint64), + }); err != nil { + t.Fatal(err) + } else { + if have := HeaderParentHashFromRLP(rlpData); have != want { + t.Fatalf("have %x, want %x", have, want) + } + } + // Also test a very very large header. + { + // The rlp-encoding of the heder belowCauses _total_ length of 65540, + // which is the first to blow the fast-path. + h := Header{ + ParentHash: want, + Extra: make([]byte, 65041), + } + if rlpData, err := rlp.EncodeToBytes(h); err != nil { + t.Fatal(err) + } else { + if have := HeaderParentHashFromRLP(rlpData); have != want { + t.Fatalf("have %x, want %x", have, want) + } + } + } + { + // Test some invalid erroneous stuff + for i, rlpData := range [][]byte{ + nil, + common.FromHex("0x"), + common.FromHex("0x01"), + common.FromHex("0x3031323334"), + } { + if have, want := HeaderParentHashFromRLP(rlpData), (common.Hash{}); have != want { + t.Fatalf("invalid %d: have %x, want %x", i, have, want) + } + } + } +} diff --git a/core/types/legacy.go b/core/types/legacy.go new file mode 100644 index 00000000000..1c4921c73d1 --- /dev/null +++ b/core/types/legacy.go @@ -0,0 +1,53 @@ +// Copyright 2021 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package types + +import ( + "errors" + + "github.com/XinFinOrg/XDPoSChain/rlp" +) + +// IsLegacyStoredReceipts tries to parse the RLP-encoded blob +// first as an array of v3 stored receipt, then v4 stored receipt and +// returns true if successful. +func IsLegacyStoredReceipts(raw []byte) (bool, error) { + var v3 []v3StoredReceiptRLP + if err := rlp.DecodeBytes(raw, &v3); err == nil { + return true, nil + } + var v4 []v4StoredReceiptRLP + if err := rlp.DecodeBytes(raw, &v4); err == nil { + return true, nil + } + var v5 []storedReceiptRLP + // Check to see valid fresh stored receipt + if err := rlp.DecodeBytes(raw, &v5); err == nil { + return false, nil + } + return false, errors.New("value is not a valid receipt encoding") +} + +// ConvertLegacyStoredReceipts takes the RLP encoding of an array of legacy +// stored receipts and returns a fresh RLP-encoded stored receipt. +func ConvertLegacyStoredReceipts(raw []byte) ([]byte, error) { + var receipts []ReceiptForStorage + if err := rlp.DecodeBytes(raw, &receipts); err != nil { + return nil, err + } + return rlp.EncodeToBytes(&receipts) +} diff --git a/eth/api_backend.go b/eth/api_backend.go index f549ddafd63..ee6bc3dde0e 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -248,8 +248,8 @@ func (b *EthAPIBackend) GetLogs(ctx context.Context, hash common.Hash, number ui return rawdb.ReadLogs(b.eth.chainDb, hash, number), nil } -func (b *EthAPIBackend) GetTd(ctx context.Context, hash common.Hash) *big.Int { - return b.eth.blockchain.GetTdByHash(hash) +func (b *EthAPIBackend) GetTd(ctx context.Context, blockHash common.Hash) *big.Int { + return b.eth.blockchain.GetTdByHash(blockHash) } func (b *EthAPIBackend) GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, XDCxState *tradingstate.TradingStateDB, header *types.Header, vmConfig *vm.Config) (*vm.EVM, func() error, error) { @@ -314,6 +314,11 @@ func (b *EthAPIBackend) GetPoolTransaction(hash common.Hash) *types.Transaction return b.eth.txPool.Get(hash) } +func (b *EthAPIBackend) GetTransaction(ctx context.Context, txHash common.Hash) (*types.Transaction, common.Hash, uint64, uint64, error) { + tx, blockHash, blockNumber, index := rawdb.ReadTransaction(b.eth.ChainDb(), txHash) + return tx, blockHash, blockNumber, index, nil +} + func (b *EthAPIBackend) GetPoolNonce(ctx context.Context, addr common.Address) (uint64, error) { return b.eth.txPool.Nonce(addr), nil } diff --git a/eth/backend.go b/eth/backend.go index da8ec18b4b9..af92955d9d4 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -111,7 +111,7 @@ func New(stack *node.Node, config *ethconfig.Config, XDCXServ *XDCx.XDCX, lendin } // Assemble the Ethereum object - chainDb, err := stack.OpenDatabase("chaindata", config.DatabaseCache, config.DatabaseHandles, "eth/db/chaindata/", false) + chainDb, err := stack.OpenDatabaseWithFreezer("chaindata", config.DatabaseCache, config.DatabaseHandles, config.DatabaseFreezer, "eth/db/chaindata/", false) if err != nil { return nil, err } @@ -186,7 +186,7 @@ func New(stack *node.Node, config *ethconfig.Config, XDCXServ *XDCx.XDCX, lendin return eth.Lending } } - eth.blockchain, err = core.NewBlockChainEx(chainDb, XDCXServ.GetLevelDB(), cacheConfig, eth.chainConfig, eth.engine, vmConfig) + eth.blockchain, err = core.NewBlockChainEx(chainDb, XDCXServ.GetLevelDB(), cacheConfig, eth.chainConfig, eth.engine, vmConfig, &config.TxLookupLimit) if err != nil { return nil, err } diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go index 6e362b397ca..bb199caf3d6 100644 --- a/eth/downloader/downloader.go +++ b/eth/downloader/downloader.go @@ -44,24 +44,24 @@ var ( MaxBlockFetch = 128 // Amount of blocks to be fetched per retrieval request MaxHeaderFetch = 192 // Amount of block headers to be fetched per retrieval request MaxSkeletonSize = 128 // Number of header fetches to need for a skeleton assembly - MaxBodyFetch = 128 // Amount of block bodies to be fetched per retrieval request MaxReceiptFetch = 256 // Amount of transaction receipts to allow fetching per request MaxStateFetch = 384 // Amount of node state values to allow fetching per request - MaxForkAncestry = 3 * params.EpochDuration // Maximum chain reorganisation - rttMinEstimate = 2 * time.Second // Minimum round-trip time to target for download requests - rttMaxEstimate = 5 * time.Second // Maximum rount-trip time to target for download requests - rttMinConfidence = 0.1 // Worse confidence factor in our estimated RTT value - ttlScaling = 2 // Constant scaling factor for RTT -> TTL conversion - ttlLimit = 5 * time.Second // Maximum TTL allowance to prevent reaching crazy timeouts + rttMinEstimate = 2 * time.Second // Minimum round-trip time to target for download requests + rttMaxEstimate = 5 * time.Second // Maximum rount-trip time to target for download requests + rttMinConfidence = 0.1 // Worse confidence factor in our estimated RTT value + ttlScaling = 2 // Constant scaling factor for RTT -> TTL conversion + ttlLimit = 5 * time.Second // Maximum TTL allowance to prevent reaching crazy timeouts qosTuningPeers = 5 // Number of peers to tune based on (best peers) qosConfidenceCap = 10 // Number of peers above which not to modify RTT confidence qosTuningImpact = 0.25 // Impact that a new tuning target has on the previous value - maxQueuedHeaders = 32 * 1024 // [eth/62] Maximum number of headers to queue for import (DOS protection) - maxHeadersProcess = 2048 // Number of header download results to import at once into the chain - maxResultsProcess = 2048 // Number of content download results to import at once into the chain + maxQueuedHeaders = 32 * 1024 // [eth/62] Maximum number of headers to queue for import (DOS protection) + maxHeadersProcess = 2048 // Number of header download results to import at once into the chain + maxResultsProcess = 2048 // Number of content download results to import at once into the chain + fullMaxForkAncestry uint64 = params.FullImmutabilityThreshold // Maximum chain reorganisation (locally redeclared so tests can reduce it) + lightMaxForkAncestry uint64 = params.LightImmutabilityThreshold // Maximum chain reorganisation (locally redeclared so tests can reduce it) reorgProtThreshold = 48 // Threshold number of recent blocks to disable mini reorg protection reorgProtHeaderDelay = 2 // Number of headers to delay delivering to cover mini reorgs @@ -95,6 +95,13 @@ var ( ) type Downloader struct { + // WARNING: The `rttEstimate` and `rttConfidence` fields are accessed atomically. + // On 32 bit platforms, only 64-bit aligned fields can be atomic. The struct is + // guaranteed to be so aligned, so take advantage of that. For more information, + // see https://golang.org/pkg/sync/atomic/#pkg-note-BUG. + rttEstimate uint64 // Round trip time to target for download requests + rttConfidence uint64 // Confidence in the estimated RTT (unit: millionths to allow atomic ops) + mode uint32 // Synchronisation mode defining the strategy used (per sync cycle) mux *event.TypeMux // Event multiplexer to announce sync operation events @@ -102,9 +109,6 @@ type Downloader struct { peers *peerSet // Set of active peers from which download can proceed stateDB ethdb.Database - rttEstimate uint64 // Round trip time to target for download requests - rttConfidence uint64 // Confidence in the estimated RTT (unit: millionths to allow atomic ops) - // Statistics syncStatsChainOrigin uint64 // Origin block number where syncing started at syncStatsChainHeight uint64 // Highest block number known when syncing started @@ -123,6 +127,7 @@ type Downloader struct { synchronising int32 notified int32 committed int32 + ancientLimit uint64 // The maximum block number which can be regarded as ancient data. // Channels headerCh chan dataPack // [eth/62] Channel receiving inbound block headers @@ -132,7 +137,10 @@ type Downloader struct { receiptWakeCh chan bool // [eth/63] Channel to signal the receipt fetcher of new tasks headerProcCh chan []*types.Header // [eth/62] Channel to feed the header processor new tasks - // for stateFetcher + // State sync + pivotHeader *types.Header // Pivot block header to dynamically push the syncing state root + pivotLock sync.RWMutex // Lock protecting pivot header reads from updates + stateSyncStart chan *stateSync trackStateReq chan *stateReq stateCh chan dataPack // [eth/63] Channel receiving inbound node state data @@ -170,8 +178,8 @@ type LightChain interface { // InsertHeaderChain inserts a batch of headers into the local chain. InsertHeaderChain([]*types.Header, int) (int, error) - // Rollback removes a few recently added elements from the local chain. - Rollback([]common.Hash) + // SetHead rewinds the local chain to a new head. + SetHead(uint64) error } // BlockChain encapsulates functions required to sync a (full or fast) blockchain. @@ -201,7 +209,7 @@ type BlockChain interface { InsertChain(types.Blocks) (int, error) // InsertReceiptChain inserts a batch of receipts into the local chain. - InsertReceiptChain(types.Blocks, []types.Receipts) (int, error) + InsertReceiptChain(types.Blocks, []types.Receipts, uint64) (int, error) } // New creates a new downloader to fetch hashes and blocks from remote peers. @@ -426,14 +434,21 @@ func (d *Downloader) syncWithPeer(p *peerConnection, hash common.Hash, td *big.I log.Debug("Synchronising with the network", "peer", p.id, "eth", p.version, "head", hash, "td", td, "mode", mode) defer func(start time.Time) { - log.Debug("Synchronisation terminated", "elapsed", time.Since(start)) + log.Debug("Synchronisation terminated", "elapsed", common.PrettyDuration(time.Since(start))) }(time.Now()) // Look up the sync boundaries: the common ancestor and the target block - latest, err := d.fetchHeight(p, hash) + latest, pivot, err := d.fetchHead(p) if err != nil { return err } + if mode == FastSync && pivot == nil { + // If no pivot block was returned, the head is below the min full block + // threshold (i.e. new chain). In that case we won't really fast sync + // anyway, but still need a valid pivot block to avoid some code hitting + // nil panics on an access. + pivot = d.blockchain.CurrentBlock().Header() + } height := latest.Number.Uint64() origin, err := d.findAncestor(p, latest) @@ -448,35 +463,77 @@ func (d *Downloader) syncWithPeer(p *peerConnection, hash common.Hash, td *big.I d.syncStatsLock.Unlock() // Ensure our origin point is below any fast sync pivot point - pivot := uint64(0) if mode == FastSync { if height <= uint64(fsMinFullBlocks) { origin = 0 } else { - pivot = height - uint64(fsMinFullBlocks) - if pivot <= origin { - origin = pivot - 1 + pivotNumber := pivot.Number.Uint64() + if pivotNumber <= origin { + origin = pivotNumber - 1 } + // Write out the pivot into the database so a rollback beyond it will + // reenable fast sync + rawdb.WriteLastPivotNumber(d.stateDB, pivotNumber) } } d.committed = 1 - if mode == FastSync && pivot != 0 { + if mode == FastSync && pivot.Number.Uint64() != 0 { d.committed = 0 } + if mode == FastSync { + // Set the ancient data limitation. + // If we are running fast sync, all block data older than ancientLimit will be + // written to the ancient store. More recent data will be written to the active + // database and will wait for the freezer to migrate. + // + // If there is a checkpoint available, then calculate the ancientLimit through + // that. Otherwise calculate the ancient limit through the advertised height + // of the remote peer. + // + // The reason for picking checkpoint first is that a malicious peer can give us + // a fake (very high) height, forcing the ancient limit to also be very high. + // The peer would start to feed us valid blocks until head, resulting in all of + // the blocks might be written into the ancient store. A following mini-reorg + // could cause issues. + if height > fullMaxForkAncestry+1 { + d.ancientLimit = height - fullMaxForkAncestry - 1 + } else { + d.ancientLimit = 0 + } + frozen, _ := d.stateDB.Ancients() // Ignore the error here since light client can also hit here. + + // If a part of blockchain data has already been written into active store, + // disable the ancient style insertion explicitly. + if origin >= frozen && frozen != 0 { + d.ancientLimit = 0 + log.Info("Disabling direct-ancient mode", "origin", origin, "ancient", frozen-1) + } else if d.ancientLimit > 0 { + log.Debug("Enabling direct-ancient mode", "ancient", d.ancientLimit) + } + // Rewind the ancient store and blockchain if reorg happens. + if origin+1 < frozen { + if err := d.lightchain.SetHead(origin); err != nil { + return err + } + } + } // Initiate the sync using a concurrent header and content retrieval algorithm d.queue.Prepare(origin+1, mode) if d.syncInitHook != nil { d.syncInitHook(origin, height) } - fetchers := []func() error{ - func() error { return d.fetchHeaders(p, origin+1, pivot) }, // Headers are always retrieved - func() error { return d.fetchBodies(origin + 1) }, // Bodies are retrieved during normal and fast sync - func() error { return d.fetchReceipts(origin + 1) }, // Receipts are retrieved during fast sync - func() error { return d.processHeaders(origin+1, pivot, td) }, + func() error { return d.fetchHeaders(p, origin+1) }, // Headers are always retrieved + func() error { return d.fetchBodies(origin + 1) }, // Bodies are retrieved during normal and fast sync + func() error { return d.fetchReceipts(origin + 1) }, // Receipts are retrieved during fast sync + func() error { return d.processHeaders(origin+1, td) }, } if mode == FastSync { - fetchers = append(fetchers, func() error { return d.processFastSyncContent(latest) }) + d.pivotLock.Lock() + d.pivotHeader = pivot + d.pivotLock.Unlock() + + fetchers = append(fetchers, func() error { return d.processFastSyncContent() }) } else if mode == FullSync { fetchers = append(fetchers, func() error { return d.processFullSyncContent(height) }) } @@ -553,19 +610,26 @@ func (d *Downloader) Terminate() { d.Cancel() } -// fetchHeight retrieves the head header of the remote peer to aid in estimating -// the total time a pending synchronisation would take. -func (d *Downloader) fetchHeight(p *peerConnection, hash common.Hash) (*types.Header, error) { +// fetchHead retrieves the head header and prior pivot block (if available) from +// a remote peer. +func (d *Downloader) fetchHead(p *peerConnection) (head *types.Header, pivot *types.Header, err error) { + p.log.Debug("Retrieving remote chain head") + mode := d.getMode() // Request the advertised remote head block and wait for the response - go p.peer.RequestHeadersByHash(hash, 1, 0, false) + latest, _ := p.peer.Head() + fetch := 1 + if mode == FastSync { + fetch = 2 // head + pivot headers + } + go p.peer.RequestHeadersByHash(latest, fetch, fsMinFullBlocks-1, true) ttl := d.requestTTL() timeout := time.After(ttl) for { select { case <-d.cancelCh: - return nil, errCanceled + return nil, nil, errCanceled case packet := <-d.headerCh: // Discard anything not from the origin peer @@ -573,19 +637,33 @@ func (d *Downloader) fetchHeight(p *peerConnection, hash common.Hash) (*types.He log.Debug("Received headers from incorrect peer", "peer", packet.PeerId()) break } - // Make sure the peer actually gave something valid + // Make sure the peer gave us at least one and at most the requested headers headers := packet.(*headerPack).headers - if len(headers) != 1 { - p.log.Debug("Multiple headers for single request", "headers", len(headers)) - return nil, fmt.Errorf("%w: multiple headers (%d) for single request", errBadPeer, len(headers)) + if len(headers) == 0 || len(headers) > fetch { + return nil, nil, fmt.Errorf("%w: returned headers %d != requested %d", errBadPeer, len(headers), fetch) } + // The first header needs to be the head, validate against the checkpoint + // and request. If only 1 header was returned, make sure there's no pivot + // or there was not one requested. head := headers[0] - p.log.Debug("Remote head header identified", "number", head.Number, "hash", head.Hash()) - return head, nil + if len(headers) == 1 { + if mode == FastSync && head.Number.Uint64() > uint64(fsMinFullBlocks) { + return nil, nil, fmt.Errorf("%w: no pivot included along head header", errBadPeer) + } + p.log.Debug("Remote head identified, no pivot", "number", head.Number, "hash", head.Hash()) + return head, nil, nil + } + // At this point we have 2 headers in total and the first is the + // validated head of the chain. Check the pivot number and return, + pivot := headers[1] + if pivot.Number.Uint64() != head.Number.Uint64()-uint64(fsMinFullBlocks) { + return nil, nil, fmt.Errorf("%w: remote pivot %d != requested %d", errInvalidChain, pivot.Number, head.Number.Uint64()-uint64(fsMinFullBlocks)) + } + return head, pivot, nil case <-timeout: p.log.Debug("Waiting for head header timed out", "elapsed", ttl) - return nil, errTimeout + return nil, nil, errTimeout case <-d.bodyCh: case <-d.receiptCh: @@ -671,9 +749,15 @@ func (d *Downloader) findAncestor(p *peerConnection, remoteHeader *types.Header) } p.log.Debug("Looking for common ancestor", "local", localHeight, "remote", remoteHeight) - if localHeight >= MaxForkAncestry { - floor = int64(localHeight - MaxForkAncestry) + // Recap floor value for binary search + maxForkAncestry := fullMaxForkAncestry + if mode == LightSync { + maxForkAncestry = lightMaxForkAncestry } + if localHeight >= maxForkAncestry { + floor = int64(localHeight - maxForkAncestry) + } + from, count, skip, max := calculateRequestSpan(remoteHeight, localHeight) p.log.Trace("Span searching for common ancestor", "count", count, "from", from, "skip", skip) @@ -776,14 +860,14 @@ func (d *Downloader) findAncestor(p *peerConnection, remoteHeader *types.Header) case <-d.cancelCh: return 0, errCanceled - case packer := <-d.headerCh: + case packet := <-d.headerCh: // Discard anything not from the origin peer - if packer.PeerId() != p.id { - log.Debug("Received headers from incorrect peer", "peer", packer.PeerId()) + if packet.PeerId() != p.id { + log.Debug("Received headers from incorrect peer", "peer", packet.PeerId()) break } // Make sure the peer actually gave something valid - headers := packer.(*headerPack).headers + headers := packet.(*headerPack).headers if len(headers) != 1 { p.log.Debug("Multiple headers for single request", "headers", len(headers)) return 0, fmt.Errorf("%w: multiple headers (%d) for single request", errBadPeer, len(headers)) @@ -842,18 +926,20 @@ func (d *Downloader) findAncestor(p *peerConnection, remoteHeader *types.Header) // other peers are only accepted if they map cleanly to the skeleton. If no one // can fill in the skeleton - not even the origin peer - it's assumed invalid and // the origin is dropped. -func (d *Downloader) fetchHeaders(p *peerConnection, from uint64, pivot uint64) error { +func (d *Downloader) fetchHeaders(p *peerConnection, from uint64) error { p.log.Debug("Directing header downloads", "origin", from) defer p.log.Debug("Header download terminated") // Create a timeout timer, and the associated header fetcher skeleton := true // Skeleton assembly phase or finishing up + pivoting := false // Whether the next request is pivot verification request := time.Now() // time of the last skeleton fetch request timeout := time.NewTimer(0) // timer to dump a non-responsive active peer <-timeout.C // timeout channel should be initially empty defer timeout.Stop() var ttl time.Duration + getHeaders := func(from uint64) { request = time.Now() @@ -868,7 +954,24 @@ func (d *Downloader) fetchHeaders(p *peerConnection, from uint64, pivot uint64) go p.peer.RequestHeadersByNumber(from, MaxHeaderFetch, 0, false) } } + + getNextPivot := func() { + pivoting = true + request = time.Now() + + ttl = d.requestTTL() + timeout.Reset(ttl) + + d.pivotLock.RLock() + pivot := d.pivotHeader.Number.Uint64() + d.pivotLock.RUnlock() + + p.log.Trace("Fetching next pivot header", "number", pivot+uint64(fsMinFullBlocks)) + go p.peer.RequestHeadersByNumber(pivot+uint64(fsMinFullBlocks), 2, fsMinFullBlocks-9, false) // move +64 when it's 2x64-8 deep + } + // Start pulling the header chain skeleton until all is done + ancestor := from getHeaders(from) mode := d.getMode() @@ -886,8 +989,46 @@ func (d *Downloader) fetchHeaders(p *peerConnection, from uint64, pivot uint64) headerReqTimer.UpdateSince(request) timeout.Stop() + // If the pivot is being checked, move if it became stale and run the real retrieval + var pivot uint64 + + d.pivotLock.RLock() + if d.pivotHeader != nil { + pivot = d.pivotHeader.Number.Uint64() + } + d.pivotLock.RUnlock() + + if pivoting { + if packet.Items() == 2 { + // Retrieve the headers and do some sanity checks, just in case + headers := packet.(*headerPack).headers + + if have, want := headers[0].Number.Uint64(), pivot+uint64(fsMinFullBlocks); have != want { + log.Warn("Peer sent invalid next pivot", "have", have, "want", want) + return fmt.Errorf("%w: next pivot number %d != requested %d", errInvalidChain, have, want) + } + if have, want := headers[1].Number.Uint64(), pivot+2*uint64(fsMinFullBlocks)-8; have != want { + log.Warn("Peer sent invalid pivot confirmer", "have", have, "want", want) + return fmt.Errorf("%w: next pivot confirmer number %d != requested %d", errInvalidChain, have, want) + } + log.Warn("Pivot seemingly stale, moving", "old", pivot, "new", headers[0].Number) + pivot = headers[0].Number.Uint64() + + d.pivotLock.Lock() + d.pivotHeader = headers[0] + d.pivotLock.Unlock() + + // Write out the pivot into the database so a rollback beyond + // it will reenable fast sync and update the state root that + // the state syncer will be downloading. + rawdb.WriteLastPivotNumber(d.stateDB, pivot) + } + pivoting = false + getHeaders(from) + continue + } // If the skeleton's finished, pull any remaining head headers directly from the origin - if packet.Items() == 0 && skeleton { + if skeleton && packet.Items() == 0 { skeleton = false getHeaders(from) continue @@ -931,7 +1072,7 @@ func (d *Downloader) fetchHeaders(p *peerConnection, from uint64, pivot uint64) // chain errors. if n := len(headers); n > 0 { // Retrieve the current head we're at - head := uint64(0) + var head uint64 if mode == LightSync { head = d.lightchain.CurrentHeader().Number.Uint64() } else { @@ -940,6 +1081,12 @@ func (d *Downloader) fetchHeaders(p *peerConnection, from uint64, pivot uint64) head = full } } + // If the head is below the common ancestor, we're actually deduplicating + // already existing chain segments, so use the ancestor as the fake head. + // Otherwise we might end up delaying header deliveries pointlessly. + if head < ancestor { + head = ancestor + } // If the head is way older than this batch, delay the last few headers if head+uint64(reorgProtThreshold) < headers[n-1].Number.Uint64() { delay := reorgProtHeaderDelay @@ -959,7 +1106,14 @@ func (d *Downloader) fetchHeaders(p *peerConnection, from uint64, pivot uint64) return errCanceled } from += uint64(len(headers)) - getHeaders(from) + + // If we're still skeleton filling fast sync, check pivot staleness + // before continuing to the next skeleton filling + if skeleton && pivot > 0 { + getNextPivot() + } else { + getHeaders(from) + } } else { // No headers delivered, or all of them being delayed, sleep a bit and retry p.log.Trace("All headers delayed, waiting") @@ -1284,41 +1438,42 @@ func (d *Downloader) fetchParts(deliveryCh chan dataPack, deliver func(dataPack) // processHeaders takes batches of retrieved headers from an input channel and // keeps processing and scheduling them into the header chain and downloader's // queue until the stream ends or a failure occurs. -func (d *Downloader) processHeaders(origin uint64, pivot uint64, td *big.Int) error { +func (d *Downloader) processHeaders(origin uint64, td *big.Int) error { // Keep a count of uncertain headers to roll back - rollback := []*types.Header{} - mode := d.getMode() + var ( + rollback uint64 // Zero means no rollback (fine as you can't unroll the genesis) + rollbackErr error + mode = d.getMode() + ) defer func() { - if len(rollback) > 0 { - // Flatten the headers and roll them back - hashes := make([]common.Hash, len(rollback)) - for i, header := range rollback { - hashes[i] = header.Hash() - } + if rollback > 0 { lastHeader, lastFastBlock, lastBlock := d.lightchain.CurrentHeader().Number, common.Big0, common.Big0 if mode != LightSync { lastFastBlock = d.blockchain.CurrentFastBlock().Number() lastBlock = d.blockchain.CurrentBlock().Number() } - d.lightchain.Rollback(hashes) + if err := d.lightchain.SetHead(rollback - 1); err != nil { // -1 to target the parent of the first uncertain block + // We're already unwinding the stack, only print the error to make it more visible + log.Error("Failed to roll back chain segment", "head", rollback-1, "err", err) + } curFastBlock, curBlock := common.Big0, common.Big0 if mode != LightSync { curFastBlock = d.blockchain.CurrentFastBlock().Number() curBlock = d.blockchain.CurrentBlock().Number() } - log.Warn("Rolled back headers", "count", len(hashes), + log.Warn("Rolled back chain segment", "header", fmt.Sprintf("%d->%d", lastHeader, d.lightchain.CurrentHeader().Number), "fast", fmt.Sprintf("%d->%d", lastFastBlock, curFastBlock), - "block", fmt.Sprintf("%d->%d", lastBlock, curBlock)) + "block", fmt.Sprintf("%d->%d", lastBlock, curBlock), "reason", rollbackErr) } }() - // Wait for batches of headers to process gotHeaders := false for { select { case <-d.cancelCh: + rollbackErr = errCanceled return errCanceled case headers := <-d.headerProcCh: @@ -1363,7 +1518,7 @@ func (d *Downloader) processHeaders(origin uint64, pivot uint64, td *big.Int) er } } // Disable any rollback and return - rollback = nil + rollback = 0 return nil } // Otherwise split the chunk of headers into batches and process them @@ -1372,6 +1527,7 @@ func (d *Downloader) processHeaders(origin uint64, pivot uint64, td *big.Int) er // Terminate if something failed in between processing chunks select { case <-d.cancelCh: + rollbackErr = errCanceled return errCanceled default: } @@ -1383,30 +1539,37 @@ func (d *Downloader) processHeaders(origin uint64, pivot uint64, td *big.Int) er chunk := headers[:limit] // In case of header only syncing, validate the chunk immediately if mode == FastSync || mode == LightSync { - // Collect the yet unknown headers to mark them as uncertain - unknown := make([]*types.Header, 0, len(headers)) - for _, header := range chunk { - if !d.lightchain.HasHeader(header.Hash(), header.Number.Uint64()) { - unknown = append(unknown, header) - } - } // If we're importing pure headers, verify based on their recentness + var pivot uint64 + + d.pivotLock.RLock() + if d.pivotHeader != nil { + pivot = d.pivotHeader.Number.Uint64() + } + d.pivotLock.RUnlock() + frequency := fsHeaderCheckFrequency if chunk[len(chunk)-1].Number.Uint64()+uint64(fsHeaderForceVerify) > pivot { frequency = 1 } if n, err := d.lightchain.InsertHeaderChain(chunk, frequency); err != nil { - // If some headers were inserted, add them too to the rollback list - if n > 0 { - rollback = append(rollback, chunk[:n]...) + rollbackErr = err + + // If some headers were inserted, track them as uncertain + if (mode == FastSync || frequency > 1) && n > 0 && rollback == 0 { + rollback = chunk[0].Number.Uint64() } - log.Debug("Invalid header encountered", "number", chunk[n].Number, "hash", chunk[n].Hash(), "err", err) + log.Warn("Invalid header encountered", "number", chunk[n].Number, "hash", chunk[n].Hash(), "parent", chunk[n].ParentHash, "err", err) return fmt.Errorf("%w: %v", errInvalidChain, err) } - // All verifications passed, store newly found uncertain headers - rollback = append(rollback, unknown...) - if len(rollback) > fsHeaderSafetyNet { - rollback = append(rollback[:0], rollback[len(rollback)-fsHeaderSafetyNet:]...) + // All verifications passed, track all headers within the alloted limits + if mode == FastSync { + head := chunk[len(chunk)-1].Number.Uint64() + if head-rollback > uint64(fsHeaderSafetyNet) { + rollback = head - uint64(fsHeaderSafetyNet) + } else { + rollback = 1 + } } } // Unless we're doing light chains, schedule the headers for associated content retrieval @@ -1415,6 +1578,7 @@ func (d *Downloader) processHeaders(origin uint64, pivot uint64, td *big.Int) er for d.queue.PendingBlocks() >= maxQueuedHeaders || d.queue.PendingReceipts() >= maxQueuedHeaders { select { case <-d.cancelCh: + rollbackErr = errCanceled return errCanceled case <-time.After(time.Second): } @@ -1422,7 +1586,7 @@ func (d *Downloader) processHeaders(origin uint64, pivot uint64, td *big.Int) er // Otherwise insert the headers for content retrieval inserts := d.queue.Schedule(chunk, origin) if len(inserts) != len(chunk) { - log.Debug("Stale headers") + rollbackErr = fmt.Errorf("stale headers: len inserts %v len(chunk) %v", len(inserts), len(chunk)) return fmt.Errorf("%w: stale headers", errBadPeer) } } @@ -1542,23 +1706,27 @@ func (d *Downloader) importBlockResults(results []*fetchResult) error { // processFastSyncContent takes fetch results from the queue and writes them to the // database. It also controls the synchronisation of state nodes of the pivot block. -func (d *Downloader) processFastSyncContent(latest *types.Header) error { +func (d *Downloader) processFastSyncContent() error { // Start syncing state of the reported head block. This should get us most of // the state of the pivot block. - sync := d.syncState(latest.Root) - defer sync.Cancel() + d.pivotLock.RLock() + sync := d.syncState(d.pivotHeader.Root) + d.pivotLock.RUnlock() + + defer func() { + // The `sync` object is replaced every time the pivot moves. We need to + // defer close the very last active one, hence the lazy evaluation vs. + // calling defer sync.Cancel() !!! + sync.Cancel() + }() + closeOnErr := func(s *stateSync) { if err := s.Wait(); err != nil && err != errCancelStateFetch && err != errCanceled { d.queue.Close() // wake up Results } } go closeOnErr(sync) - // Figure out the ideal pivot block. Note, that this goalpost may move if the - // sync takes long enough for the chain head to move significantly. - pivot := uint64(0) - if height := latest.Number.Uint64(); height > uint64(fsMinFullBlocks) { - pivot = height - uint64(fsMinFullBlocks) - } + // To cater for moving pivot points, track the pivot block and subsequently // accumulated download results separatey. var ( @@ -1585,18 +1753,46 @@ func (d *Downloader) processFastSyncContent(latest *types.Header) error { if d.chainInsertHook != nil { d.chainInsertHook(results) } - if oldPivot != nil { + // If we haven't downloaded the pivot block yet, check pivot staleness + // notifications from the header downloader + d.pivotLock.RLock() + pivot := d.pivotHeader + d.pivotLock.RUnlock() + + if oldPivot == nil { + if pivot.Root != sync.root { + sync.Cancel() + sync = d.syncState(pivot.Root) + + go closeOnErr(sync) + } + } else { results = append(append([]*fetchResult{oldPivot}, oldTail...), results...) } // Split around the pivot block and process the two sides via fast/full sync if atomic.LoadInt32(&d.committed) == 0 { - latest = results[len(results)-1].Header - if height := latest.Number.Uint64(); height > pivot+2*uint64(fsMinFullBlocks) { - log.Warn("Pivot became stale, moving", "old", pivot, "new", height-uint64(fsMinFullBlocks)) - pivot = height - uint64(fsMinFullBlocks) + latest := results[len(results)-1].Header + // If the height is above the pivot block by 2 sets, it means the pivot + // become stale in the network and it was garbage collected, move to a + // new pivot. + // + // Note, we have `reorgProtHeaderDelay` number of blocks withheld, Those + // need to be taken into account, otherwise we're detecting the pivot move + // late and will drop peers due to unavailable state!!! + if height := latest.Number.Uint64(); height >= pivot.Number.Uint64()+2*uint64(fsMinFullBlocks)-uint64(reorgProtHeaderDelay) { + log.Warn("Pivot became stale, moving", "old", pivot.Number.Uint64(), "new", height-uint64(fsMinFullBlocks)+uint64(reorgProtHeaderDelay)) + pivot = results[len(results)-1-fsMinFullBlocks+reorgProtHeaderDelay].Header // must exist as lower old pivot is uncommitted + + d.pivotLock.Lock() + d.pivotHeader = pivot + d.pivotLock.Unlock() + + // Write out the pivot into the database so a rollback beyond it will + // reenable fast sync + rawdb.WriteLastPivotNumber(d.stateDB, pivot.Number.Uint64()) } } - P, beforeP, afterP := splitAroundPivot(pivot, results) + P, beforeP, afterP := splitAroundPivot(pivot.Number.Uint64(), results) if err := d.commitFastSyncData(beforeP, sync); err != nil { return err } @@ -1604,9 +1800,8 @@ func (d *Downloader) processFastSyncContent(latest *types.Header) error { // If new pivot block found, cancel old state retrieval and restart if oldPivot != P { sync.Cancel() - sync = d.syncState(P.Header.Root) - defer sync.Cancel() + go closeOnErr(sync) oldPivot = P } @@ -1634,6 +1829,13 @@ func (d *Downloader) processFastSyncContent(latest *types.Header) error { } func splitAroundPivot(pivot uint64, results []*fetchResult) (p *fetchResult, before, after []*fetchResult) { + if len(results) == 0 { + return nil, nil, nil + } + if lastNum := results[len(results)-1].Header.Number.Uint64(); lastNum < pivot { + // the pivot is somewhere in the future + return nil, results, nil + } for _, result := range results { num := result.Header.Number.Uint64() switch { @@ -1674,7 +1876,7 @@ func (d *Downloader) commitFastSyncData(results []*fetchResult, stateSync *state blocks[i] = types.NewBlockWithHeader(result.Header).WithBody(result.Transactions, result.Uncles) receipts[i] = result.Receipts } - if index, err := d.blockchain.InsertReceiptChain(blocks, receipts); err != nil { + if index, err := d.blockchain.InsertReceiptChain(blocks, receipts, d.ancientLimit); err != nil { log.Debug("Downloaded item processing failed", "number", results[index].Header.Number, "hash", results[index].Header.Hash(), "err", err) return fmt.Errorf("%w: %v", errInvalidChain, err) } @@ -1684,7 +1886,7 @@ func (d *Downloader) commitFastSyncData(results []*fetchResult, stateSync *state func (d *Downloader) commitPivotBlock(result *fetchResult) error { block := types.NewBlockWithHeader(result.Header).WithBody(result.Transactions, result.Uncles) log.Debug("Committing fast sync pivot as new head", "number", block.Number(), "hash", block.Hash()) - if _, err := d.blockchain.InsertReceiptChain([]*types.Block{block}, []types.Receipts{result.Receipts}); err != nil { + if _, err := d.blockchain.InsertReceiptChain([]*types.Block{block}, []types.Receipts{result.Receipts}, d.ancientLimit); err != nil { return err } if err := d.blockchain.FastSyncCommitHead(block.Hash()); err != nil { diff --git a/eth/downloader/downloader_test.go b/eth/downloader/downloader_test.go index d3d45b8f09f..a8e0a86ff88 100644 --- a/eth/downloader/downloader_test.go +++ b/eth/downloader/downloader_test.go @@ -38,7 +38,8 @@ import ( // Reduce some of the parameters to make the tester faster. func init() { - MaxForkAncestry = uint64(10000) + fullMaxForkAncestry = 10000 + lightMaxForkAncestry = 10000 blockCacheItems = 1024 fsHeaderContCheck = 500 * time.Millisecond } @@ -58,6 +59,11 @@ type downloadTester struct { ownReceipts map[common.Hash]types.Receipts // Receipts belonging to the tester ownChainTd map[common.Hash]*big.Int // Total difficulties of the blocks in the local chain + ancientHeaders map[common.Hash]*types.Header // Ancient headers belonging to the tester + ancientBlocks map[common.Hash]*types.Block // Ancient blocks belonging to the tester + ancientReceipts map[common.Hash]types.Receipts // Ancient receipts belonging to the tester + ancientChainTd map[common.Hash]*big.Int // Ancient total difficulties of the blocks in the local chain + lock sync.RWMutex } @@ -72,9 +78,16 @@ func newTester() *downloadTester { ownBlocks: map[common.Hash]*types.Block{testGenesis.Hash(): testGenesis}, ownReceipts: map[common.Hash]types.Receipts{testGenesis.Hash(): nil}, ownChainTd: map[common.Hash]*big.Int{testGenesis.Hash(): testGenesis.Difficulty()}, + + // Initialize ancient store with test genesis block + ancientHeaders: map[common.Hash]*types.Header{testGenesis.Hash(): testGenesis.Header()}, + ancientBlocks: map[common.Hash]*types.Block{testGenesis.Hash(): testGenesis}, + ancientReceipts: map[common.Hash]types.Receipts{testGenesis.Hash(): nil}, + ancientChainTd: map[common.Hash]*big.Int{testGenesis.Hash(): testGenesis.Difficulty()}, } tester.stateDb = rawdb.NewMemoryDatabase() tester.stateDb.Put(testGenesis.Root().Bytes(), []byte{0x00}) + tester.downloader = New(tester.stateDb, new(event.TypeMux), tester, nil, tester.dropPeer, tester.handleProposedBlock) return tester } @@ -122,6 +135,9 @@ func (dl *downloadTester) HasFastBlock(hash common.Hash, number uint64) bool { dl.lock.RLock() defer dl.lock.RUnlock() + if _, ok := dl.ancientReceipts[hash]; ok { + return true + } _, ok := dl.ownReceipts[hash] return ok } @@ -131,6 +147,10 @@ func (dl *downloadTester) GetHeaderByHash(hash common.Hash) *types.Header { dl.lock.RLock() defer dl.lock.RUnlock() + header := dl.ancientHeaders[hash] + if header != nil { + return header + } return dl.ownHeaders[hash] } @@ -139,6 +159,10 @@ func (dl *downloadTester) GetBlockByHash(hash common.Hash) *types.Block { dl.lock.RLock() defer dl.lock.RUnlock() + block := dl.ancientBlocks[hash] + if block != nil { + return block + } return dl.ownBlocks[hash] } @@ -148,6 +172,9 @@ func (dl *downloadTester) CurrentHeader() *types.Header { defer dl.lock.RUnlock() for i := len(dl.ownHashes) - 1; i >= 0; i-- { + if header := dl.ancientHeaders[dl.ownHashes[i]]; header != nil { + return header + } if header := dl.ownHeaders[dl.ownHashes[i]]; header != nil { return header } @@ -161,6 +188,12 @@ func (dl *downloadTester) CurrentBlock() *types.Block { defer dl.lock.RUnlock() for i := len(dl.ownHashes) - 1; i >= 0; i-- { + if block := dl.ancientBlocks[dl.ownHashes[i]]; block != nil { + if _, err := dl.stateDb.Get(block.Root().Bytes()); err == nil { + return block + } + return block + } if block := dl.ownBlocks[dl.ownHashes[i]]; block != nil { if _, err := dl.stateDb.Get(block.Root().Bytes()); err == nil { return block @@ -176,6 +209,9 @@ func (dl *downloadTester) CurrentFastBlock() *types.Block { defer dl.lock.RUnlock() for i := len(dl.ownHashes) - 1; i >= 0; i-- { + if block := dl.ancientBlocks[dl.ownHashes[i]]; block != nil { + return block + } if block := dl.ownBlocks[dl.ownHashes[i]]; block != nil { return block } @@ -198,6 +234,9 @@ func (dl *downloadTester) GetTd(hash common.Hash, number uint64) *big.Int { dl.lock.RLock() defer dl.lock.RUnlock() + if td := dl.ancientChainTd[hash]; td != nil { + return td + } return dl.ownChainTd[hash] } @@ -246,6 +285,7 @@ func (dl *downloadTester) InsertChain(blocks types.Blocks) (i int, err error) { dl.ownHeaders[block.Hash()] = block.Header() } dl.ownBlocks[block.Hash()] = block + dl.ownReceipts[block.Hash()] = make(types.Receipts, 0) dl.stateDb.Put(block.Root().Bytes(), []byte{0x00}) dl.ownChainTd[block.Hash()] = new(big.Int).Add(dl.ownChainTd[block.ParentHash()], block.Difficulty()) } @@ -253,7 +293,7 @@ func (dl *downloadTester) InsertChain(blocks types.Blocks) (i int, err error) { } // InsertReceiptChain injects a new batch of receipts into the simulated chain. -func (dl *downloadTester) InsertReceiptChain(blocks types.Blocks, receipts []types.Receipts) (i int, err error) { +func (dl *downloadTester) InsertReceiptChain(blocks types.Blocks, receipts []types.Receipts, ancientLimit uint64) (i int, err error) { dl.lock.Lock() defer dl.lock.Unlock() @@ -261,29 +301,71 @@ func (dl *downloadTester) InsertReceiptChain(blocks types.Blocks, receipts []typ if _, ok := dl.ownHeaders[blocks[i].Hash()]; !ok { return i, errors.New("unknown owner") } - if _, ok := dl.ownBlocks[blocks[i].ParentHash()]; !ok { - return i, errors.New("unknown parent") + if _, ok := dl.ancientBlocks[blocks[i].ParentHash()]; !ok { + if _, ok := dl.ownBlocks[blocks[i].ParentHash()]; !ok { + return i, errors.New("unknown parent") + } + } + if blocks[i].NumberU64() <= ancientLimit { + dl.ancientBlocks[blocks[i].Hash()] = blocks[i] + dl.ancientReceipts[blocks[i].Hash()] = receipts[i] + + // Migrate from active db to ancient db + dl.ancientHeaders[blocks[i].Hash()] = blocks[i].Header() + dl.ancientChainTd[blocks[i].Hash()] = new(big.Int).Add(dl.ancientChainTd[blocks[i].ParentHash()], blocks[i].Difficulty()) + + delete(dl.ownHeaders, blocks[i].Hash()) + delete(dl.ownChainTd, blocks[i].Hash()) + } else { + dl.ownBlocks[blocks[i].Hash()] = blocks[i] + dl.ownReceipts[blocks[i].Hash()] = receipts[i] } - dl.ownBlocks[blocks[i].Hash()] = blocks[i] - dl.ownReceipts[blocks[i].Hash()] = receipts[i] } return len(blocks), nil } -// Rollback removes some recently added elements from the chain. -func (dl *downloadTester) Rollback(hashes []common.Hash) { +// SetHead rewinds the local chain to a new head. +func (dl *downloadTester) SetHead(head uint64) error { dl.lock.Lock() defer dl.lock.Unlock() - for i := len(hashes) - 1; i >= 0; i-- { - if dl.ownHashes[len(dl.ownHashes)-1] == hashes[i] { - dl.ownHashes = dl.ownHashes[:len(dl.ownHashes)-1] + // Find the hash of the head to reset to + var hash common.Hash + for h, header := range dl.ownHeaders { + if header.Number.Uint64() == head { + hash = h + } + } + for h, header := range dl.ancientHeaders { + if header.Number.Uint64() == head { + hash = h + } + } + if hash == (common.Hash{}) { + return fmt.Errorf("unknown head to set: %d", head) + } + // Find the offset in the header chain + var offset int + for o, h := range dl.ownHashes { + if h == hash { + offset = o + break } - delete(dl.ownChainTd, hashes[i]) - delete(dl.ownHeaders, hashes[i]) - delete(dl.ownReceipts, hashes[i]) - delete(dl.ownBlocks, hashes[i]) } + // Remove all the hashes and associated data afterwards + for i := offset + 1; i < len(dl.ownHashes); i++ { + delete(dl.ownChainTd, dl.ownHashes[i]) + delete(dl.ownHeaders, dl.ownHashes[i]) + delete(dl.ownReceipts, dl.ownHashes[i]) + delete(dl.ownBlocks, dl.ownHashes[i]) + + delete(dl.ancientChainTd, dl.ownHashes[i]) + delete(dl.ancientHeaders, dl.ownHashes[i]) + delete(dl.ancientReceipts, dl.ownHashes[i]) + delete(dl.ancientBlocks, dl.ownHashes[i]) + } + dl.ownHashes = dl.ownHashes[:offset+1] + return nil } // newPeer registers a new block download source into the downloader. @@ -335,11 +417,7 @@ func (dlp *downloadTesterPeer) Head() (common.Hash, *big.Int) { // origin; associated with a particular peer in the download tester. The returned // function can be used to retrieve batches of headers from the particular peer. func (dlp *downloadTesterPeer) RequestHeadersByHash(origin common.Hash, amount int, skip int, reverse bool) error { - if reverse { - panic("reverse header requests not supported") - } - - result := dlp.chain.headersByHash(origin, amount, skip) + result := dlp.chain.headersByHash(origin, amount, skip, reverse) go dlp.dl.downloader.DeliverHeaders(dlp.id, result) return nil } @@ -348,11 +426,7 @@ func (dlp *downloadTesterPeer) RequestHeadersByHash(origin common.Hash, amount i // origin; associated with a particular peer in the download tester. The returned // function can be used to retrieve batches of headers from the particular peer. func (dlp *downloadTesterPeer) RequestHeadersByNumber(origin uint64, amount int, skip int, reverse bool) error { - if reverse { - panic("reverse header requests not supported") - } - - result := dlp.chain.headersByNumber(origin, amount, skip) + result := dlp.chain.headersByNumber(origin, amount, skip, reverse) go dlp.dl.downloader.DeliverHeaders(dlp.id, result) return nil } @@ -397,37 +471,37 @@ func (dlp *downloadTesterPeer) RequestNodeData(hashes []common.Hash) error { // assertOwnChain checks if the local chain contains the correct number of items // of the various chain components. func assertOwnChain(t *testing.T, tester *downloadTester, length int) { + // Mark this method as a helper to report errors at callsite, not in here + t.Helper() + assertOwnForkedChain(t, tester, 1, []int{length}) } // assertOwnForkedChain checks if the local forked chain contains the correct // number of items of the various chain components. func assertOwnForkedChain(t *testing.T, tester *downloadTester, common int, lengths []int) { + // Mark this method as a helper to report errors at callsite, not in here + t.Helper() + // Initialize the counters for the first fork - headers, blocks, receipts := lengths[0], lengths[0], lengths[0]-fsMinFullBlocks + headers, blocks, receipts := lengths[0], lengths[0], lengths[0] - if receipts < 0 { - receipts = 1 - } // Update the counters for each subsequent fork for _, length := range lengths[1:] { headers += length - common blocks += length - common - receipts += length - common - fsMinFullBlocks + receipts += length - common } - switch SyncMode(tester.downloader.mode) { - case FullSync: - receipts = 1 - case LightSync: + if tester.downloader.mode == uint32(LightSync) { blocks, receipts = 1, 1 } - if hs := len(tester.ownHeaders); hs != headers { + if hs := len(tester.ownHeaders) + len(tester.ancientHeaders) - 1; hs != headers { t.Fatalf("synchronised headers mismatch: have %v, want %v", hs, headers) } - if bs := len(tester.ownBlocks); bs != blocks { + if bs := len(tester.ownBlocks) + len(tester.ancientBlocks) - 1; bs != blocks { t.Fatalf("synchronised blocks mismatch: have %v, want %v", bs, blocks) } - if rs := len(tester.ownReceipts); rs != receipts { + if rs := len(tester.ownReceipts) + len(tester.ancientReceipts) - 1; rs != receipts { t.Fatalf("synchronised receipts mismatch: have %v, want %v", rs, receipts) } } @@ -933,9 +1007,8 @@ func testShiftedHeaderAttack(t *testing.T, protocol int, mode SyncMode) { // Tests that upon detecting an invalid header, the recent ones are rolled back // for various failure scenarios. Afterwards a full sync is attempted to make // sure no state was corrupted. -func TestInvalidHeaderRollback63Fast(t *testing.T) { testInvalidHeaderRollback(t, 63, FastSync) } -func TestInvalidHeaderRollback64Fast(t *testing.T) { testInvalidHeaderRollback(t, 64, FastSync) } -func TestInvalidHeaderRollback64Light(t *testing.T) { testInvalidHeaderRollback(t, 64, LightSync) } +func TestInvalidHeaderRollback63Fast(t *testing.T) { testInvalidHeaderRollback(t, 63, FastSync) } +func TestInvalidHeaderRollback64Fast(t *testing.T) { testInvalidHeaderRollback(t, 64, FastSync) } func testInvalidHeaderRollback(t *testing.T, protocol int, mode SyncMode) { t.Parallel() @@ -1151,13 +1224,8 @@ func testSyncProgress(t *testing.T, protocol int, mode SyncMode) { } }() <-starting - // TODO(daniel): set StartingBlock to `uint64(chain.len()/2 - 1)` for mode FastSync, ref: #17916 - var startingBlock = uint64(0) - if mode != FastSync { - startingBlock = uint64(chain.len()/2 - 1) - } checkProgress(t, tester.downloader, "completing", ethereum.SyncProgress{ - StartingBlock: startingBlock, + StartingBlock: uint64(chain.len()/2 - 1), CurrentBlock: uint64(chain.len()/2 - 1), HighestBlock: uint64(chain.len() - 1), }) @@ -1166,7 +1234,7 @@ func testSyncProgress(t *testing.T, protocol int, mode SyncMode) { progress <- struct{}{} pending.Wait() checkProgress(t, tester.downloader, "final", ethereum.SyncProgress{ - StartingBlock: startingBlock, + StartingBlock: uint64(chain.len()/2 - 1), CurrentBlock: uint64(chain.len() - 1), HighestBlock: uint64(chain.len() - 1), }) diff --git a/eth/downloader/testchain_test.go b/eth/downloader/testchain_test.go index 9f864042ba5..799b60b98a0 100644 --- a/eth/downloader/testchain_test.go +++ b/eth/downloader/testchain_test.go @@ -45,7 +45,7 @@ var testChainBase = newTestChain(blockCacheItems+200, testGenesis) var testChainForkLightA, testChainForkLightB, testChainForkHeavy *testChain func init() { - var forkLen = int(MaxForkAncestry + 50) + var forkLen = int(fullMaxForkAncestry + 50) var wg sync.WaitGroup wg.Add(3) go func() { testChainForkLightA = testChainBase.makeFork(forkLen, false, 1); wg.Done() }() @@ -170,18 +170,27 @@ func (tc *testChain) td(hash common.Hash) *big.Int { return tc.tdm[hash] } -// headersByHash returns headers in ascending order from the given hash. -func (tc *testChain) headersByHash(origin common.Hash, amount int, skip int) []*types.Header { +// headersByHash returns headers in order from the given hash. +func (tc *testChain) headersByHash(origin common.Hash, amount int, skip int, reverse bool) []*types.Header { num, _ := tc.hashToNumber(origin) - return tc.headersByNumber(num, amount, skip) + return tc.headersByNumber(num, amount, skip, reverse) } // headersByNumber returns headers in ascending order from the given number. -func (tc *testChain) headersByNumber(origin uint64, amount int, skip int) []*types.Header { +func (tc *testChain) headersByNumber(origin uint64, amount int, skip int, reverse bool) []*types.Header { result := make([]*types.Header, 0, amount) - for num := origin; num < uint64(len(tc.chain)) && len(result) < amount; num += uint64(skip) + 1 { - if header, ok := tc.headerm[tc.chain[int(num)]]; ok { - result = append(result, header) + + if !reverse { + for num := origin; num < uint64(len(tc.chain)) && len(result) < amount; num += uint64(skip) + 1 { + if header, ok := tc.headerm[tc.chain[int(num)]]; ok { + result = append(result, header) + } + } + } else { + for num := int64(origin); num >= 0 && len(result) < amount; num -= int64(skip) + 1 { + if header, ok := tc.headerm[tc.chain[int(num)]]; ok { + result = append(result, header) + } } } return result diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 9cb5c583b19..235a4bbfb0a 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -102,7 +102,10 @@ type Config struct { // zero, the chain ID is used as network ID. NetworkId uint64 SyncMode downloader.SyncMode - NoPruning bool + + NoPruning bool // Whether to disable pruning and flush everything to disk + + TxLookupLimit uint64 `toml:",omitempty"` // The maximum number of blocks from head whose tx indices are reserved. // Light client options LightServ int `toml:",omitempty"` // Maximum percentage of time allowed for serving LES requests @@ -112,6 +115,7 @@ type Config struct { SkipBcVersionCheck bool `toml:"-"` DatabaseHandles int `toml:"-"` DatabaseCache int + DatabaseFreezer string TrieCache int TrieTimeout time.Duration diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go index da2e1850823..4fd8d7a2d45 100644 --- a/eth/ethconfig/gen_config.go +++ b/eth/ethconfig/gen_config.go @@ -24,11 +24,13 @@ func (c Config) MarshalTOML() (interface{}, error) { NetworkId uint64 SyncMode downloader.SyncMode NoPruning bool - LightServ int `toml:",omitempty"` - LightPeers int `toml:",omitempty"` - SkipBcVersionCheck bool `toml:"-"` - DatabaseHandles int `toml:"-"` + TxLookupLimit uint64 `toml:",omitempty"` + LightServ int `toml:",omitempty"` + LightPeers int `toml:",omitempty"` + SkipBcVersionCheck bool `toml:"-"` + DatabaseHandles int `toml:"-"` DatabaseCache int + DatabaseFreezer string TrieCache int TrieTimeout time.Duration FilterLogCacheSize int @@ -48,11 +50,13 @@ func (c Config) MarshalTOML() (interface{}, error) { enc.NetworkId = c.NetworkId enc.SyncMode = c.SyncMode enc.NoPruning = c.NoPruning + enc.TxLookupLimit = c.TxLookupLimit enc.LightServ = c.LightServ enc.LightPeers = c.LightPeers enc.SkipBcVersionCheck = c.SkipBcVersionCheck enc.DatabaseHandles = c.DatabaseHandles enc.DatabaseCache = c.DatabaseCache + enc.DatabaseFreezer = c.DatabaseFreezer enc.TrieCache = c.TrieCache enc.TrieTimeout = c.TrieTimeout enc.FilterLogCacheSize = c.FilterLogCacheSize @@ -76,11 +80,13 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { NetworkId *uint64 SyncMode *downloader.SyncMode NoPruning *bool - LightServ *int `toml:",omitempty"` - LightPeers *int `toml:",omitempty"` - SkipBcVersionCheck *bool `toml:"-"` - DatabaseHandles *int `toml:"-"` + TxLookupLimit *uint64 `toml:",omitempty"` + LightServ *int `toml:",omitempty"` + LightPeers *int `toml:",omitempty"` + SkipBcVersionCheck *bool `toml:"-"` + DatabaseHandles *int `toml:"-"` DatabaseCache *int + DatabaseFreezer *string TrieCache *int TrieTimeout *time.Duration FilterLogCacheSize *int @@ -111,6 +117,9 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { if dec.NoPruning != nil { c.NoPruning = *dec.NoPruning } + if dec.TxLookupLimit != nil { + c.TxLookupLimit = *dec.TxLookupLimit + } if dec.LightServ != nil { c.LightServ = *dec.LightServ } @@ -126,6 +135,9 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { if dec.DatabaseCache != nil { c.DatabaseCache = *dec.DatabaseCache } + if dec.DatabaseFreezer != nil { + c.DatabaseFreezer = *dec.DatabaseFreezer + } if dec.TrieCache != nil { c.TrieCache = *dec.TrieCache } diff --git a/eth/gasprice/gasprice_test.go b/eth/gasprice/gasprice_test.go index e6ac77a0768..bc5b3d84deb 100644 --- a/eth/gasprice/gasprice_test.go +++ b/eth/gasprice/gasprice_test.go @@ -154,7 +154,7 @@ func newTestBackend(t *testing.T, eip1559Block *big.Int, pending bool) *testBack // Construct testing chain diskdb := rawdb.NewMemoryDatabase() gspec.Commit(diskdb) - chain, err := core.NewBlockChain(diskdb, nil, gspec.Config, engine, vm.Config{}) + chain, err := core.NewBlockChain(diskdb, nil, gspec.Config, engine, vm.Config{}, nil) if err != nil { t.Fatalf("Failed to create local chain, %v", err) } diff --git a/eth/handler.go b/eth/handler.go index 1d91474ae93..f6bddfc992e 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -75,6 +75,7 @@ type ProtocolManager struct { orderpool orderPool lendingpool lendingPool blockchain *core.BlockChain + chaindb ethdb.Database chainconfig *params.ChainConfig maxPeers int @@ -133,6 +134,7 @@ func NewProtocolManager(config *params.ChainConfig, mode downloader.SyncMode, ne eventMux: mux, txpool: txpool, blockchain: blockchain, + chaindb: chaindb, chainconfig: config, peers: newPeerSet(), newPeerCh: make(chan *peer), diff --git a/eth/handler_test.go b/eth/handler_test.go index 651a33a28c4..15094443c01 100644 --- a/eth/handler_test.go +++ b/eth/handler_test.go @@ -475,7 +475,7 @@ func testDAOChallenge(t *testing.T, localForked, remoteForked bool, timeout bool config = ¶ms.ChainConfig{DAOForkBlock: big.NewInt(1), DAOForkSupport: localForked} gspec = &core.Genesis{Config: config} genesis = gspec.MustCommit(db) - blockchain, _ = core.NewBlockChain(db, nil, config, pow, vm.Config{}) + blockchain, _ = core.NewBlockChain(db, nil, config, pow, vm.Config{}, nil) ) pm, err := NewProtocolManager(config, downloader.FullSync, ethconfig.Defaults.NetworkId, evmux, new(testTxPool), pow, blockchain, db) if err != nil { diff --git a/eth/helper_test.go b/eth/helper_test.go index c883e6f6245..9cd24fb6a63 100644 --- a/eth/helper_test.go +++ b/eth/helper_test.go @@ -61,7 +61,7 @@ func newTestProtocolManager(mode downloader.SyncMode, blocks int, generator func Alloc: types.GenesisAlloc{testBank: {Balance: big.NewInt(1000000)}}, } genesis = gspec.MustCommit(db) - blockchain, _ = core.NewBlockChain(db, nil, gspec.Config, engine, vm.Config{}) + blockchain, _ = core.NewBlockChain(db, nil, gspec.Config, engine, vm.Config{}, nil) ) chain, _ := core.GenerateChain(gspec.Config, genesis, ethash.NewFaker(), db, blocks, generator) if _, err := blockchain.InsertChain(chain); err != nil { diff --git a/eth/sync.go b/eth/sync.go index 5a0ea512f55..74c6cfb4dd3 100644 --- a/eth/sync.go +++ b/eth/sync.go @@ -17,11 +17,13 @@ package eth import ( + "math/big" "math/rand" "sync/atomic" "time" "github.com/XinFinOrg/XDPoSChain/common" + "github.com/XinFinOrg/XDPoSChain/core/rawdb" "github.com/XinFinOrg/XDPoSChain/core/types" "github.com/XinFinOrg/XDPoSChain/eth/downloader" "github.com/XinFinOrg/XDPoSChain/log" @@ -169,25 +171,12 @@ func (pm *ProtocolManager) synchronise(peer *peer) { return } // Make sure the peer's TD is higher than our own - currentBlock := pm.blockchain.CurrentBlock() - td := pm.blockchain.GetTd(currentBlock.Hash(), currentBlock.NumberU64()) + mode, ourTD := pm.modeAndLocalHead() + // currentBlock := pm.blockchain.CurrentBlock() + // td := pm.blockchain.GetTd(currentBlock.Hash(), currentBlock.NumberU64()) pHead, pTd := peer.Head() - if pTd.Cmp(td) <= 0 { - return - } - // Otherwise try to sync with the downloader - mode := downloader.FullSync - if atomic.LoadUint32(&pm.fastSync) == 1 { - // Fast sync was explicitly requested, and explicitly granted - mode = downloader.FastSync - } else if currentBlock.NumberU64() == 0 && pm.blockchain.CurrentFastBlock().NumberU64() > 0 { - // The database seems empty as the current block is the genesis. Yet the fast - // block is ahead, so fast sync was enabled for this node at a certain point. - // The only scenario where this can happen is if the user manually (or via a - // bad block) rolled back a fast sync node below the sync point. In this case - // however it's safe to reenable fast sync. - atomic.StoreUint32(&pm.fastSync, 1) - mode = downloader.FastSync + if pTd.Cmp(ourTD) <= 0 { + return // We're in sync. } if mode == downloader.FastSync { @@ -195,6 +184,22 @@ func (pm *ProtocolManager) synchronise(peer *peer) { if pm.blockchain.GetTdByHash(pm.blockchain.CurrentFastBlock().Hash()).Cmp(pTd) >= 0 { return } + // Before launch the fast sync, we have to ensure user uses the same + // txlookup limit. + // The main concern here is: during the fast sync Geth won't index the + // block(generate tx indices) before the HEAD-limit. But if user changes + // the limit in the next fast sync(e.g. user kill Geth manually and + // restart) then it will be hard for Geth to figure out the oldest block + // has been indexed. So here for the user-experience wise, it's non-optimal + // that user can't change limit during the fast sync. If changed, Geth + // will just blindly use the original one. + limit := pm.blockchain.TxLookupLimit() + if stored := rawdb.ReadFastTxLookupLimit(pm.chaindb); stored == nil { + rawdb.WriteFastTxLookupLimit(pm.chaindb, limit) + } else if *stored != limit { + pm.blockchain.SetTxLookupLimit(*stored) + log.Warn("Update txLookup limit", "provided", limit, "updated", *stored) + } } // Run the sync cycle, and disable fast sync if we've went past the pivot block @@ -206,13 +211,40 @@ func (pm *ProtocolManager) synchronise(peer *peer) { atomic.StoreUint32(&pm.fastSync, 0) } atomic.StoreUint32(&pm.acceptTxs, 1) // Mark initial sync done - //if head := pm.blockchain.CurrentBlock(); head.NumberU64() > 0 { - // // We've completed a sync cycle, notify all peers of new state. This path is - // // essential in star-topology networks where a gateway node needs to notify - // // all its out-of-date peers of the availability of a new block. This failure - // // scenario will most often crop up in private and hackathon networks with - // // degenerate connectivity, but it should be healthy for the mainnet too to - // // more reliably update peers or the local TD state. - // go pm.BroadcastBlock(head, false) - //} +} + +func (pm *ProtocolManager) modeAndLocalHead() (downloader.SyncMode, *big.Int) { + // If we're in fast sync mode, return that directly + if atomic.LoadUint32(&pm.fastSync) == 1 { + block := pm.blockchain.CurrentFastBlock() + td := pm.blockchain.GetTdByHash(block.Hash()) + return downloader.FastSync, td + } + + currentBlock := pm.blockchain.CurrentBlock() + + // The database seems empty as the current block is the genesis. Yet the fast + // block is ahead, so fast sync was enabled for this node at a certain point. + // The only scenario where this can happen is if the user manually (or via a + // bad block) rolled back a fast sync node below the sync point. In this case + // however it's safe to reenable fast sync. + if currentBlock.NumberU64() == 0 && pm.blockchain.CurrentFastBlock().NumberU64() > 0 { + atomic.StoreUint32(&pm.fastSync, 1) + td := pm.blockchain.GetTd(currentBlock.Hash(), currentBlock.NumberU64()) + return downloader.FastSync, td + } + + // We are probably in full sync, but we might have rewound to before the + // fast sync pivot, check if we should reenable + if pivot := rawdb.ReadLastPivotNumber(pm.chaindb); pivot != nil { + if currentBlock.NumberU64() < *pivot { + block := pm.blockchain.CurrentFastBlock() + td := pm.blockchain.GetTdByHash(block.Hash()) + return downloader.FastSync, td + } + } + + // Nope, we're really full syncing + td := pm.blockchain.GetTd(currentBlock.Hash(), currentBlock.NumberU64()) + return downloader.FullSync, td } diff --git a/ethdb/database.go b/ethdb/database.go index 634bd1de2d9..5bdc578e995 100644 --- a/ethdb/database.go +++ b/ethdb/database.go @@ -18,8 +18,9 @@ package ethdb import ( - "github.com/XinFinOrg/XDPoSChain/common" "io" + + "github.com/XinFinOrg/XDPoSChain/common" ) // KeyValueReader wraps the Has and Get method of a backing data store. @@ -40,8 +41,8 @@ type KeyValueWriter interface { Delete(key []byte) error } -// Stater wraps the Stat method of a backing data store. -type Stater interface { +// KeyValueStater wraps the Stat method of a backing data store. +type KeyValueStater interface { // Stat returns a particular internal stat of the database. Stat(property string) (string, error) } @@ -63,15 +64,15 @@ type Compacter interface { type KeyValueStore interface { KeyValueReader KeyValueWriter + KeyValueStater Batcher Iteratee - Stater Compacter io.Closer } -// AncientReader contains the methods required to read from immutable ancient data. -type AncientReader interface { +// AncientReaderOp contains the methods required to read from immutable ancient data. +type AncientReaderOp interface { // HasAncient returns an indicator whether the specified data exists in the // ancient store. HasAncient(kind string, number uint64) (bool, error) @@ -79,24 +80,78 @@ type AncientReader interface { // Ancient retrieves an ancient binary blob from the append-only immutable files. Ancient(kind string, number uint64) ([]byte, error) + // AncientRange retrieves multiple items in sequence, starting from the index 'start'. + // It will return + // - at most 'count' items, + // - if maxBytes is specified: at least 1 item (even if exceeding the maxByteSize), + // but will otherwise return as many items as fit into maxByteSize. + // - if maxBytes is not specified, 'count' items will be returned if they are present + AncientRange(kind string, start, count, maxBytes uint64) ([][]byte, error) + // Ancients returns the ancient item numbers in the ancient store. Ancients() (uint64, error) + // Tail returns the number of first stored item in the freezer. + // This number can also be interpreted as the total deleted item numbers. + Tail() (uint64, error) + // AncientSize returns the ancient size of the specified category. AncientSize(kind string) (uint64, error) } +// AncientReader is the extended ancient reader interface including 'batched' or 'atomic' reading. +type AncientReader interface { + AncientReaderOp + + // ReadAncients runs the given read operation while ensuring that no writes take place + // on the underlying freezer. + ReadAncients(fn func(AncientReaderOp) error) (err error) +} + // AncientWriter contains the methods required to write to immutable ancient data. type AncientWriter interface { - // AppendAncient injects all binary blobs belong to block at the end of the - // append-only immutable table files. - AppendAncient(number uint64, hash, header, body, receipt, td []byte) error - - // TruncateAncients discards all but the first n ancient data from the ancient store. - TruncateAncients(n uint64) error + // ModifyAncients runs a write operation on the ancient store. + // If the function returns an error, any changes to the underlying store are reverted. + // The integer return value is the total size of the written data. + ModifyAncients(func(AncientWriteOp) error) (int64, error) + + // TruncateHead discards all but the first n ancient data from the ancient store. + // After the truncation, the latest item can be accessed it item_n-1(start from 0). + TruncateHead(n uint64) error + + // TruncateTail discards the first n ancient data from the ancient store. The already + // deleted items are ignored. After the truncation, the earliest item can be accessed + // is item_n(start from 0). The deleted items may not be removed from the ancient store + // immediately, but only when the accumulated deleted data reach the threshold then + // will be removed all together. + // + // Note that data marked as non-prunable will still be retained and remain accessible. + TruncateTail(n uint64) error // Sync flushes all in-memory ancient store data to disk. Sync() error + + // MigrateTable processes and migrates entries of a given table to a new format. + // The second argument is a function that takes a raw entry and returns it + // in the newest format. + MigrateTable(string, func([]byte) ([]byte, error)) error +} + +// AncientWriteOp is given to the function argument of ModifyAncients. +type AncientWriteOp interface { + // Append adds an RLP-encoded item. + Append(kind string, number uint64, item interface{}) error + + // AppendRaw adds an item without RLP-encoding it. + AppendRaw(kind string, number uint64, item []byte) error +} + +// AncientStater wraps the Stat method of a backing data store. +type AncientStater interface { + // AncientDatadir returns the path of root ancient directory. Empty string + // will be returned if ancient store is not enabled at all. The returned + // path can be used to construct the path of other freezers. + AncientDatadir() (string, error) } // Reader contains the methods required to read data from both key-value as well as @@ -106,31 +161,20 @@ type Reader interface { AncientReader } -// Writer contains the methods required to write data to both key-value as well as -// immutable ancient data. -type Writer interface { - KeyValueWriter - AncientWriter -} - // AncientStore contains all the methods required to allow handling different // ancient data stores backing immutable chain data store. type AncientStore interface { AncientReader AncientWriter + AncientStater io.Closer } // Database contains all the methods required by the high level database to not // only access the key-value data store but also the chain freezer. type Database interface { - Reader - Writer - Batcher - Iteratee - Stater - Compacter - io.Closer + KeyValueStore + AncientStore } // XDCxDatabase interface diff --git a/go.mod b/go.mod index fdc5d7ef2b4..a3af771fae8 100644 --- a/go.mod +++ b/go.mod @@ -22,10 +22,9 @@ require ( github.com/olekukonko/tablewriter v0.0.5 github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 github.com/pkg/errors v0.9.1 - github.com/prometheus/prometheus v1.7.2-0.20170814170113-3101606756c5 github.com/rs/cors v1.7.0 github.com/steakknife/bloomfilter v0.0.0-20180922174646-6819c0d2a570 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 golang.org/x/crypto v0.29.0 golang.org/x/sync v0.9.0 @@ -47,6 +46,7 @@ require ( github.com/fsnotify/fsnotify v1.8.0 github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 github.com/go-yaml/yaml v2.1.0+incompatible + github.com/gofrs/flock v0.12.1 github.com/google/gofuzz v1.2.0 github.com/google/uuid v1.6.0 github.com/influxdata/influxdb-client-go/v2 v2.4.0 diff --git a/go.sum b/go.sum index 9bf08fa73bc..1b92f073232 100644 --- a/go.sum +++ b/go.sum @@ -70,6 +70,8 @@ github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyL github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -177,8 +179,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/prometheus v1.7.2-0.20170814170113-3101606756c5 h1:K2PKeDFZidfjUWpXk05Gbxhwm8Rnz1l4O+u/bbbcCvc= -github.com/prometheus/prometheus v1.7.2-0.20170814170113-3101606756c5/go.mod h1:oAIUtOny2rjMX0OWN5vPR5/q/twIROJvdqnQKDdil/s= github.com/protolambda/bls12-381-util v0.0.0-20220416220906-d8552aa452c7 h1:cZC+usqsYgHtlBaGulVnZ1hfKAi8iWtujBnRLQE698c= github.com/protolambda/bls12-381-util v0.0.0-20220416220906-d8552aa452c7/go.mod h1:IToEjHuttnUzwZI5KBSM/LOOW3qLbbrHOEfp3SbECGY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= @@ -201,8 +201,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 70922718656..5755cde2090 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -648,7 +648,7 @@ func (s *PublicBlockChainAPI) GetTransactionAndReceiptProof(ctx context.Context, func (s *PublicBlockChainAPI) GetBlockByNumber(ctx context.Context, blockNr rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) { block, err := s.b.BlockByNumber(ctx, blockNr) if block != nil { - response, err := s.rpcOutputBlock(block, true, fullTx, ctx) + response, err := s.rpcOutputBlock(ctx, block, true, fullTx) if err == nil && blockNr == rpc.PendingBlockNumber { // Pending blocks need to nil out a few fields for _, field := range []string{"hash", "nonce", "miner"} { @@ -665,7 +665,7 @@ func (s *PublicBlockChainAPI) GetBlockByNumber(ctx context.Context, blockNr rpc. func (s *PublicBlockChainAPI) GetBlockByHash(ctx context.Context, blockHash common.Hash, fullTx bool) (map[string]interface{}, error) { block, err := s.b.GetBlock(ctx, blockHash) if block != nil { - return s.rpcOutputBlock(block, true, fullTx, ctx) + return s.rpcOutputBlock(ctx, block, true, fullTx) } return nil, err } @@ -681,7 +681,7 @@ func (s *PublicBlockChainAPI) GetUncleByBlockNumberAndIndex(ctx context.Context, return nil, nil } block = types.NewBlockWithHeader(uncles[index]) - return s.rpcOutputBlock(block, false, false, ctx) + return s.rpcOutputBlock(ctx, block, false, false) } return nil, err } @@ -698,7 +698,7 @@ func (s *PublicBlockChainAPI) GetUncleByBlockHashAndIndex(ctx context.Context, b return nil, nil } block = types.NewBlockWithHeader(uncles[index]) - return s.rpcOutputBlock(block, false, false, ctx) + return s.rpcOutputBlock(ctx, block, false, false) } return nil, err } @@ -1669,10 +1669,10 @@ func RPCMarshalHeader(head *types.Header) map[string]interface{} { // rpcOutputBlock converts the given block to the RPC output which depends on fullTx. If inclTx is true transactions are // returned. When fullTx is true the returned block contains full transaction details, otherwise it will only contain // transaction hashes. -func (s *PublicBlockChainAPI) rpcOutputBlock(b *types.Block, inclTx bool, fullTx bool, ctx context.Context) (map[string]interface{}, error) { +func (s *PublicBlockChainAPI) rpcOutputBlock(ctx context.Context, b *types.Block, inclTx bool, fullTx bool) (map[string]interface{}, error) { fields := RPCMarshalHeader(b.Header()) fields["size"] = hexutil.Uint64(b.Size()) - fields["totalDifficulty"] = (*hexutil.Big)(s.b.GetTd(context.Background(), b.Hash())) + fields["totalDifficulty"] = (*hexutil.Big)(s.b.GetTd(ctx, b.Hash())) if inclTx { formatTx := func(tx *types.Transaction) (interface{}, error) { @@ -2217,8 +2217,8 @@ func (s *PublicTransactionPoolAPI) GetRawTransactionByHash(ctx context.Context, // GetTransactionReceipt returns the transaction receipt for the given transaction hash. func (s *PublicTransactionPoolAPI) GetTransactionReceipt(ctx context.Context, hash common.Hash) (map[string]interface{}, error) { - tx, blockHash, blockNumber, index := rawdb.ReadTransaction(s.b.ChainDb(), hash) - if tx == nil { + tx, blockHash, blockNumber, index, err := s.b.GetTransaction(ctx, hash) + if err != nil { // When the transaction doesn't exist, the RPC method should return JSON null // as per specification. return nil, nil diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index e4cdf283da8..16e059b8203 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -78,6 +78,7 @@ type Backend interface { // TxPool API SendTx(ctx context.Context, signedTx *types.Transaction) error + GetTransaction(ctx context.Context, txHash common.Hash) (*types.Transaction, common.Hash, uint64, uint64, error) GetPoolTransactions() (types.Transactions, error) GetPoolTransaction(txHash common.Hash) *types.Transaction GetPoolNonce(ctx context.Context, addr common.Address) (uint64, error) diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index 960b81d0763..d809faa7037 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -346,7 +346,7 @@ func (b *backendMock) GetReceipts(ctx context.Context, hash common.Hash) (types. return nil, nil } -func (b *backendMock) GetTd(ctx context.Context, hash common.Hash) *big.Int { +func (b *backendMock) GetTd(context.Context, common.Hash) *big.Int { return nil } diff --git a/node/node.go b/node/node.go index 4fbff9690c5..50d85f32c1a 100644 --- a/node/node.go +++ b/node/node.go @@ -33,7 +33,7 @@ import ( "github.com/XinFinOrg/XDPoSChain/log" "github.com/XinFinOrg/XDPoSChain/p2p" "github.com/XinFinOrg/XDPoSChain/rpc" - "github.com/prometheus/prometheus/util/flock" + "github.com/gofrs/flock" ) // Node is a container on which services can be registered. @@ -45,13 +45,12 @@ type Node struct { keyDir string // key store directory keyDirTemp bool // If true, key directory will be removed by Stop - ephemKeystore string // if non-empty, the key directory that will be removed by Stop - dirLock flock.Releaser // prevents concurrent use of instance directory - stop chan struct{} // Channel to wait for termination notifications - - server *p2p.Server // Currently running P2P networking layer - startStopLock sync.Mutex // Start/Stop are protected by an additional lock - state int // Tracks state of node lifecycle + ephemKeystore string // if non-empty, the key directory that will be removed by Stop + dirLock *flock.Flock // prevents concurrent use of instance directory + stop chan struct{} // Channel to wait for termination notifications + server *p2p.Server // Currently running P2P networking layer + startStopLock sync.Mutex // Start/Stop are protected by an additional lock + state int // Tracks state of node lifecycle lock sync.Mutex lifecycles []Lifecycle // All registered backends, services, and auxiliary services that have a lifecycle @@ -318,20 +317,20 @@ func (n *Node) openDataDir() error { } // Lock the instance directory to prevent concurrent use by another instance as well as // accidental use of the instance directory as a database. - release, _, err := flock.New(filepath.Join(instdir, "LOCK")) - if err != nil { - return convertFileLockError(err) + n.dirLock = flock.New(filepath.Join(instdir, "LOCK")) + + if locked, err := n.dirLock.TryLock(); err != nil { + return err + } else if !locked { + return ErrDatadirUsed } - n.dirLock = release return nil } func (n *Node) closeDataDir() { // Release instance directory lock. - if n.dirLock != nil { - if err := n.dirLock.Release(); err != nil { - n.log.Error("Can't release datadir lock", "err", err) - } + if n.dirLock != nil && n.dirLock.Locked() { + n.dirLock.Unlock() n.dirLock = nil } } @@ -572,11 +571,38 @@ func (n *Node) OpenDatabase(name string, cache, handles int, namespace string, r return db, err } +// OpenDatabaseWithFreezer opens an existing database with the given name (or +// creates one if no previous can be found) from within the node's data directory, +// also attaching a chain freezer to it that moves ancient chain data from the +// database to immutable append-only files. If the node is an ephemeral one, a +// memory database is returned. +func (n *Node) OpenDatabaseWithFreezer(name string, cache, handles int, ancient, namespace string, readonly bool) (ethdb.Database, error) { + var db ethdb.Database + var err error + if n.config.DataDir == "" { + db = rawdb.NewMemoryDatabase() + } else { + db, err = rawdb.NewLevelDBDatabaseWithFreezer(n.ResolvePath(name), cache, handles, n.ResolveAncient(name, ancient), namespace, readonly) + } + return db, err +} + // ResolvePath returns the absolute path of a resource in the instance directory. func (n *Node) ResolvePath(x string) string { return n.config.ResolvePath(x) } +// ResolveAncient returns the absolute path of the root ancient directory. +func (n *Node) ResolveAncient(name string, ancient string) string { + switch { + case ancient == "": + ancient = filepath.Join(n.ResolvePath(name), "ancient") + case !filepath.IsAbs(ancient): + ancient = n.ResolvePath(ancient) + } + return ancient +} + // closeTrackingDB wraps the Close method of a database. When the database is closed by the // service, the wrapper removes it from the node's database map. This ensures that Node // won't auto-close the database if it is closed by the service that opened it. diff --git a/params/network_params.go b/params/network_params.go index 20eb2535469..193130a0a5e 100644 --- a/params/network_params.go +++ b/params/network_params.go @@ -27,4 +27,17 @@ const ( // BloomConfirms is the number of confirmation blocks before a bloom section is // considered probably final and its rotated bits are calculated. BloomConfirms = 256 + + // ImmutabilityThreshold is the number of blocks after which a chain segment is + // FullImmutabilityThreshold is the number of blocks after which a chain segment is + // considered immutable (i.e. soft finality). It is used by the downloader as a + // hard limit against deep ancestors, by the blockchain against deep reorgs, by + // the freezer as the cutoff treshold and by clique as the snapshot trust limit. + FullImmutabilityThreshold = 90000 + + // LightImmutabilityThreshold is the number of blocks after which a header chain + // segment is considered immutable for light client(i.e. soft finality). It is used by + // the downloader as a hard limit against deep ancestors, by the blockchain against deep + // reorgs, by the light pruner as the pruning validity guarantee. + LightImmutabilityThreshold = 30000 ) diff --git a/tests/block_test_util.go b/tests/block_test_util.go index 505f86ccc43..d9254c820a4 100644 --- a/tests/block_test_util.go +++ b/tests/block_test_util.go @@ -114,7 +114,7 @@ func (t *BlockTest) Run() error { return fmt.Errorf("genesis block state root does not match test: computed=%x, test=%x", gblock.Root().Bytes(), t.json.Genesis.StateRoot) } - chain, err := core.NewBlockChain(db, nil, config, ethash.NewShared(), vm.Config{}) + chain, err := core.NewBlockChain(db, nil, config, ethash.NewShared(), vm.Config{}, nil) if err != nil { return err } diff --git a/trie/sync.go b/trie/sync.go index f34cf4d68ed..9c14c7f72b9 100644 --- a/trie/sync.go +++ b/trie/sync.go @@ -156,7 +156,7 @@ func (s *Sync) AddCodeEntry(hash common.Hash, depth int, parent common.Hash) { if s.membatch.hasCode(hash) { return } - if s.bloom.Contains(hash[:]) { + if s.bloom == nil || s.bloom.Contains(hash[:]) { // Bloom filter says this might be a duplicate, double check. // If database says yes, the blob is present for sure. // Note we only check the existence with new code scheme, fast