mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-24 21:56:43 +00:00
Merge branch 'master' of github.com:berachain/bera-geth
This commit is contained in:
commit
526cd5f03c
54 changed files with 4133 additions and 318 deletions
|
|
@ -150,7 +150,7 @@ All hex encoded values must be prefixed with `0x`.
|
|||
|
||||
#### Create new password protected account
|
||||
|
||||
The signer will generate a new private key, encrypt it according to [web3 keystore spec](https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition) and store it in the keystore directory.
|
||||
The signer will generate a new private key, encrypt it according to [web3 keystore spec](https://ethereum.org/en/developers/docs/data-structures-and-encoding/web3-secret-storage/) and store it in the keystore directory.
|
||||
The client is responsible for creating a backup of the keystore. If the keystore is lost there is no method of retrieving lost accounts.
|
||||
|
||||
#### Arguments
|
||||
|
|
|
|||
|
|
@ -773,15 +773,8 @@ func downloadEra(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
func parseRange(s string) (start uint64, end uint64, ok bool) {
|
||||
if m, _ := regexp.MatchString("[0-9]+", s); m {
|
||||
start, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
end = start
|
||||
return start, end, true
|
||||
}
|
||||
if m, _ := regexp.MatchString("[0-9]+-[0-9]+", s); m {
|
||||
log.Info("Parsing block range", "input", s)
|
||||
if m, _ := regexp.MatchString("^[0-9]+-[0-9]+$", s); m {
|
||||
s1, s2, _ := strings.Cut(s, "-")
|
||||
start, err := strconv.ParseUint(s1, 10, 64)
|
||||
if err != nil {
|
||||
|
|
@ -791,6 +784,19 @@ func parseRange(s string) (start uint64, end uint64, ok bool) {
|
|||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
if start > end {
|
||||
return 0, 0, false
|
||||
}
|
||||
log.Info("Parsing block range", "start", start, "end", end)
|
||||
return start, end, true
|
||||
}
|
||||
if m, _ := regexp.MatchString("^[0-9]+$", s); m {
|
||||
start, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
end = start
|
||||
log.Info("Parsing single block range", "block", start)
|
||||
return start, end, true
|
||||
}
|
||||
return 0, 0, false
|
||||
|
|
|
|||
98
cmd/geth/chaincmd_test.go
Normal file
98
cmd/geth/chaincmd_test.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright 2025 The go-ethereum Authors
|
||||
// This file is part of go-ethereum.
|
||||
//
|
||||
// go-ethereum is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// go-ethereum 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseRange(t *testing.T) {
|
||||
var cases = []struct {
|
||||
input string
|
||||
valid bool
|
||||
expStart uint64
|
||||
expEnd uint64
|
||||
}{
|
||||
{
|
||||
input: "0",
|
||||
valid: true,
|
||||
expStart: 0,
|
||||
expEnd: 0,
|
||||
},
|
||||
{
|
||||
input: "500",
|
||||
valid: true,
|
||||
expStart: 500,
|
||||
expEnd: 500,
|
||||
},
|
||||
{
|
||||
input: "-1",
|
||||
valid: false,
|
||||
expStart: 0,
|
||||
expEnd: 0,
|
||||
},
|
||||
{
|
||||
input: "1-1",
|
||||
valid: true,
|
||||
expStart: 1,
|
||||
expEnd: 1,
|
||||
},
|
||||
{
|
||||
input: "0-1",
|
||||
valid: true,
|
||||
expStart: 0,
|
||||
expEnd: 1,
|
||||
},
|
||||
{
|
||||
input: "1-0",
|
||||
valid: false,
|
||||
expStart: 0,
|
||||
expEnd: 0,
|
||||
},
|
||||
{
|
||||
input: "1-1000",
|
||||
valid: true,
|
||||
expStart: 1,
|
||||
expEnd: 1000,
|
||||
},
|
||||
{
|
||||
input: "1-1-",
|
||||
valid: false,
|
||||
expStart: 0,
|
||||
expEnd: 0,
|
||||
},
|
||||
{
|
||||
input: "-1-1",
|
||||
valid: false,
|
||||
expStart: 0,
|
||||
expEnd: 0,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
start, end, valid := parseRange(c.input)
|
||||
if valid != c.valid {
|
||||
t.Errorf("Unexpected result, want: %t, got: %t", c.valid, valid)
|
||||
continue
|
||||
}
|
||||
if valid {
|
||||
if c.expStart != start {
|
||||
t.Errorf("Unexpected start, want: %d, got: %d", c.expStart, start)
|
||||
}
|
||||
if c.expEnd != end {
|
||||
t.Errorf("Unexpected end, want: %d, got: %d", c.expEnd, end)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -220,6 +220,27 @@ func verifyState(ctx *cli.Context) error {
|
|||
triedb := utils.MakeTrieDatabase(ctx, chaindb, false, true, false)
|
||||
defer triedb.Close()
|
||||
|
||||
var (
|
||||
err error
|
||||
root = headBlock.Root()
|
||||
)
|
||||
if ctx.NArg() == 1 {
|
||||
root, err = parseRoot(ctx.Args().First())
|
||||
if err != nil {
|
||||
log.Error("Failed to resolve state root", "err", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if triedb.Scheme() == rawdb.PathScheme {
|
||||
if err := triedb.VerifyState(root); err != nil {
|
||||
log.Error("Failed to verify state", "root", root, "err", err)
|
||||
return err
|
||||
}
|
||||
log.Info("Verified the state", "root", root)
|
||||
|
||||
// TODO(rjl493456442) implement dangling checks in pathdb.
|
||||
return nil
|
||||
} else {
|
||||
snapConfig := snapshot.Config{
|
||||
CacheSize: 256,
|
||||
Recovery: false,
|
||||
|
|
@ -231,24 +252,13 @@ func verifyState(ctx *cli.Context) error {
|
|||
log.Error("Failed to open snapshot tree", "err", err)
|
||||
return err
|
||||
}
|
||||
if ctx.NArg() > 1 {
|
||||
log.Error("Too many arguments given")
|
||||
return errors.New("too many arguments")
|
||||
}
|
||||
var root = headBlock.Root()
|
||||
if ctx.NArg() == 1 {
|
||||
root, err = parseRoot(ctx.Args().First())
|
||||
if err != nil {
|
||||
log.Error("Failed to resolve state root", "err", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := snaptree.Verify(root); err != nil {
|
||||
log.Error("Failed to verify state", "root", root, "err", err)
|
||||
return err
|
||||
}
|
||||
log.Info("Verified the state", "root", root)
|
||||
return snapshot.CheckDanglingStorage(chaindb)
|
||||
}
|
||||
}
|
||||
|
||||
// checkDanglingStorage iterates the snap storage data, and verifies that all
|
||||
|
|
|
|||
|
|
@ -1672,11 +1672,6 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
|
|||
cfg.TransactionHistory = 0
|
||||
log.Warn("Disabled transaction unindexing for archive node")
|
||||
}
|
||||
|
||||
if cfg.StateScheme != rawdb.HashScheme {
|
||||
cfg.StateScheme = rawdb.HashScheme
|
||||
log.Warn("Forcing hash state-scheme for archive mode")
|
||||
}
|
||||
}
|
||||
if ctx.IsSet(LogHistoryFlag.Name) {
|
||||
cfg.LogHistory = ctx.Uint64(LogHistoryFlag.Name)
|
||||
|
|
@ -1907,11 +1902,11 @@ func MakeBeaconLightConfig(ctx *cli.Context) bparams.ClientConfig {
|
|||
if c, err := hexutil.Decode(ctx.String(BeaconGenesisRootFlag.Name)); err == nil && len(c) <= 32 {
|
||||
copy(config.GenesisValidatorsRoot[:len(c)], c)
|
||||
} else {
|
||||
Fatalf("Invalid hex string", "beacon.genesis.gvroot", ctx.String(BeaconGenesisRootFlag.Name), "error", err)
|
||||
Fatalf("Could not parse --%s: %v", BeaconGenesisRootFlag.Name, err)
|
||||
}
|
||||
configFile := ctx.String(BeaconConfigFlag.Name)
|
||||
if err := config.ChainConfig.LoadForks(configFile); err != nil {
|
||||
Fatalf("Could not load beacon chain config", "file", configFile, "error", err)
|
||||
Fatalf("Could not load beacon chain config '%s': %v", configFile, err)
|
||||
}
|
||||
log.Info("Using custom beacon chain config", "file", configFile)
|
||||
} else {
|
||||
|
|
@ -1928,17 +1923,17 @@ func MakeBeaconLightConfig(ctx *cli.Context) bparams.ClientConfig {
|
|||
// are saved to the specified file.
|
||||
if ctx.IsSet(BeaconCheckpointFileFlag.Name) {
|
||||
if _, err := config.SetCheckpointFile(ctx.String(BeaconCheckpointFileFlag.Name)); err != nil {
|
||||
Fatalf("Could not load beacon checkpoint file", "beacon.checkpoint.file", ctx.String(BeaconCheckpointFileFlag.Name), "error", err)
|
||||
Fatalf("Could not load beacon checkpoint file '%s': %v", ctx.String(BeaconCheckpointFileFlag.Name), err)
|
||||
}
|
||||
}
|
||||
if ctx.IsSet(BeaconCheckpointFlag.Name) {
|
||||
hex := ctx.String(BeaconCheckpointFlag.Name)
|
||||
c, err := hexutil.Decode(hex)
|
||||
if err != nil {
|
||||
Fatalf("Invalid hex string", "beacon.checkpoint", hex, "error", err)
|
||||
Fatalf("Could not parse --%s: %v", BeaconCheckpointFlag.Name, err)
|
||||
}
|
||||
if len(c) != 32 {
|
||||
Fatalf("Invalid hex string length", "beacon.checkpoint", hex, "length", len(c))
|
||||
Fatalf("Could not parse --%s: invalid length %d, want 32", BeaconCheckpointFlag.Name, len(c))
|
||||
}
|
||||
copy(config.Checkpoint[:len(c)], c)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -391,8 +391,12 @@ func (beacon *Beacon) FinalizeAndAssemble(chain consensus.ChainHeaderReader, hea
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening pre-state tree root: %w", err)
|
||||
}
|
||||
postTrie := state.GetTrie()
|
||||
if postTrie == nil {
|
||||
return nil, errors.New("post-state tree is not available")
|
||||
}
|
||||
vktPreTrie, okpre := preTrie.(*trie.VerkleTrie)
|
||||
vktPostTrie, okpost := state.GetTrie().(*trie.VerkleTrie)
|
||||
vktPostTrie, okpost := postTrie.(*trie.VerkleTrie)
|
||||
|
||||
// The witness is only attached iff both parent and current block are
|
||||
// using verkle tree.
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ type BlockChainConfig struct {
|
|||
TrieCleanLimit int // Memory allowance (MB) to use for caching trie nodes in memory
|
||||
TrieDirtyLimit int // Memory limit (MB) at which to start flushing dirty trie nodes to disk
|
||||
TrieTimeLimit time.Duration // Time limit after which to flush the current in-memory trie to disk
|
||||
TrieNoAsyncFlush bool // Whether the asynchronous buffer flushing is disallowed
|
||||
|
||||
Preimages bool // Whether to store preimage of trie key to the disk
|
||||
StateHistory uint64 // Number of blocks from head whose state histories are reserved.
|
||||
|
|
@ -210,7 +211,7 @@ func DefaultConfig() *BlockChainConfig {
|
|||
}
|
||||
}
|
||||
|
||||
// WithArchive enabled/disables archive mode on the config.
|
||||
// WithArchive enables/disables archive mode on the config.
|
||||
func (cfg BlockChainConfig) WithArchive(on bool) *BlockChainConfig {
|
||||
cfg.ArchiveMode = on
|
||||
return &cfg
|
||||
|
|
@ -222,6 +223,12 @@ func (cfg BlockChainConfig) WithStateScheme(scheme string) *BlockChainConfig {
|
|||
return &cfg
|
||||
}
|
||||
|
||||
// WithNoAsyncFlush enables/disables asynchronous buffer flushing mode on the config.
|
||||
func (cfg BlockChainConfig) WithNoAsyncFlush(on bool) *BlockChainConfig {
|
||||
cfg.TrieNoAsyncFlush = on
|
||||
return &cfg
|
||||
}
|
||||
|
||||
// triedbConfig derives the configures for trie database.
|
||||
func (cfg *BlockChainConfig) triedbConfig(isVerkle bool) *triedb.Config {
|
||||
config := &triedb.Config{
|
||||
|
|
@ -236,6 +243,7 @@ func (cfg *BlockChainConfig) triedbConfig(isVerkle bool) *triedb.Config {
|
|||
if cfg.StateScheme == rawdb.PathScheme {
|
||||
config.PathDB = &pathdb.Config{
|
||||
StateHistory: cfg.StateHistory,
|
||||
EnableStateIndexing: cfg.ArchiveMode,
|
||||
TrieCleanSize: cfg.TrieCleanLimit * 1024 * 1024,
|
||||
StateCleanSize: cfg.SnapshotLimit * 1024 * 1024,
|
||||
|
||||
|
|
@ -243,6 +251,7 @@ func (cfg *BlockChainConfig) triedbConfig(isVerkle bool) *triedb.Config {
|
|||
// for flushing both trie data and state data to disk. The config name
|
||||
// should be updated to eliminate the confusion.
|
||||
WriteBufferSize: cfg.TrieDirtyLimit * 1024 * 1024,
|
||||
NoAsyncFlush: cfg.TrieNoAsyncFlush,
|
||||
}
|
||||
}
|
||||
return config
|
||||
|
|
@ -1236,7 +1245,7 @@ func (bc *BlockChain) stopWithoutSaving() {
|
|||
bc.scope.Close()
|
||||
|
||||
// Signal shutdown to all goroutines.
|
||||
bc.StopInsert()
|
||||
bc.InterruptInsert(true)
|
||||
|
||||
// Now wait for all chain modifications to end and persistent goroutines to exit.
|
||||
//
|
||||
|
|
@ -1310,11 +1319,15 @@ func (bc *BlockChain) Stop() {
|
|||
log.Info("Blockchain stopped")
|
||||
}
|
||||
|
||||
// StopInsert interrupts all insertion methods, causing them to return
|
||||
// errInsertionInterrupted as soon as possible. Insertion is permanently disabled after
|
||||
// calling this method.
|
||||
func (bc *BlockChain) StopInsert() {
|
||||
// InterruptInsert interrupts all insertion methods, causing them to return
|
||||
// errInsertionInterrupted as soon as possible, or resume the chain insertion
|
||||
// if required.
|
||||
func (bc *BlockChain) InterruptInsert(on bool) {
|
||||
if on {
|
||||
bc.procInterrupt.Store(true)
|
||||
} else {
|
||||
bc.procInterrupt.Store(false)
|
||||
}
|
||||
}
|
||||
|
||||
// insertStopped returns true after StopInsert has been called.
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ func (basic *snapshotTestBasic) prepare(t *testing.T) (*BlockChain, []*types.Blo
|
|||
}
|
||||
engine = ethash.NewFullFaker()
|
||||
)
|
||||
chain, err := NewBlockChain(db, gspec, engine, DefaultConfig().WithStateScheme(basic.scheme))
|
||||
chain, err := NewBlockChain(db, gspec, engine, DefaultConfig().WithStateScheme(basic.scheme).WithNoAsyncFlush(true))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create chain: %v", err)
|
||||
}
|
||||
|
|
@ -572,7 +572,7 @@ func TestHighCommitCrashWithNewSnapshot(t *testing.T) {
|
|||
//
|
||||
// Expected head header : C8
|
||||
// Expected head fast block: C8
|
||||
// Expected head block : G (Hash mode), C6 (Hash mode)
|
||||
// Expected head block : G (Hash mode), C6 (Path mode)
|
||||
// Expected snapshot disk : C4 (Hash mode)
|
||||
for _, scheme := range []string{rawdb.HashScheme, rawdb.PathScheme} {
|
||||
expHead := uint64(0)
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ func (b *BlockGen) addTx(bc *BlockChain, vmConfig vm.Config, tx *types.Transacti
|
|||
}
|
||||
// Merge the tx-local access event into the "block-local" one, in order to collect
|
||||
// all values, so that the witness can be built.
|
||||
if b.statedb.GetTrie().IsVerkle() {
|
||||
if b.statedb.Database().TrieDB().IsVerkle() {
|
||||
b.statedb.AccessEvents().Merge(evm.AccessEvents)
|
||||
}
|
||||
b.txs = append(b.txs, tx)
|
||||
|
|
|
|||
|
|
@ -276,7 +276,9 @@ func newDbConfig(scheme string) *triedb.Config {
|
|||
if scheme == rawdb.HashScheme {
|
||||
return triedb.HashDefaults
|
||||
}
|
||||
return &triedb.Config{PathDB: pathdb.Defaults}
|
||||
config := *pathdb.Defaults
|
||||
config.NoAsyncFlush = true
|
||||
return &triedb.Config{PathDB: &config}
|
||||
}
|
||||
|
||||
func TestVerkleGenesisCommit(t *testing.T) {
|
||||
|
|
@ -333,7 +335,14 @@ func TestVerkleGenesisCommit(t *testing.T) {
|
|||
}
|
||||
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
triedb := triedb.NewDatabase(db, triedb.VerkleDefaults)
|
||||
|
||||
config := *pathdb.Defaults
|
||||
config.NoAsyncFlush = true
|
||||
|
||||
triedb := triedb.NewDatabase(db, &triedb.Config{
|
||||
IsVerkle: true,
|
||||
PathDB: &config,
|
||||
})
|
||||
block := genesis.MustCommit(db, triedb)
|
||||
if !bytes.Equal(block.Root().Bytes(), expected) {
|
||||
t.Fatalf("invalid genesis state root, expected %x, got %x", expected, block.Root())
|
||||
|
|
|
|||
179
core/rawdb/accessors_history.go
Normal file
179
core/rawdb/accessors_history.go
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
// Copyright 2025 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package rawdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
// ReadStateHistoryIndexMetadata retrieves the metadata of state history index.
|
||||
func ReadStateHistoryIndexMetadata(db ethdb.KeyValueReader) []byte {
|
||||
data, _ := db.Get(headStateHistoryIndexKey)
|
||||
return data
|
||||
}
|
||||
|
||||
// WriteStateHistoryIndexMetadata stores the metadata of state history index
|
||||
// into database.
|
||||
func WriteStateHistoryIndexMetadata(db ethdb.KeyValueWriter, blob []byte) {
|
||||
if err := db.Put(headStateHistoryIndexKey, blob); err != nil {
|
||||
log.Crit("Failed to store the metadata of state history index", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteStateHistoryIndexMetadata removes the metadata of state history index.
|
||||
func DeleteStateHistoryIndexMetadata(db ethdb.KeyValueWriter) {
|
||||
if err := db.Delete(headStateHistoryIndexKey); err != nil {
|
||||
log.Crit("Failed to delete the metadata of state history index", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadAccountHistoryIndex retrieves the account history index with the provided
|
||||
// account address.
|
||||
func ReadAccountHistoryIndex(db ethdb.KeyValueReader, addressHash common.Hash) []byte {
|
||||
data, err := db.Get(accountHistoryIndexKey(addressHash))
|
||||
if err != nil || len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// WriteAccountHistoryIndex writes the provided account history index into database.
|
||||
func WriteAccountHistoryIndex(db ethdb.KeyValueWriter, addressHash common.Hash, data []byte) {
|
||||
if err := db.Put(accountHistoryIndexKey(addressHash), data); err != nil {
|
||||
log.Crit("Failed to store account history index", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteAccountHistoryIndex deletes the specified account history index from
|
||||
// the database.
|
||||
func DeleteAccountHistoryIndex(db ethdb.KeyValueWriter, addressHash common.Hash) {
|
||||
if err := db.Delete(accountHistoryIndexKey(addressHash)); err != nil {
|
||||
log.Crit("Failed to delete account history index", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadStorageHistoryIndex retrieves the storage history index with the provided
|
||||
// account address and storage key hash.
|
||||
func ReadStorageHistoryIndex(db ethdb.KeyValueReader, addressHash common.Hash, storageHash common.Hash) []byte {
|
||||
data, err := db.Get(storageHistoryIndexKey(addressHash, storageHash))
|
||||
if err != nil || len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// WriteStorageHistoryIndex writes the provided storage history index into database.
|
||||
func WriteStorageHistoryIndex(db ethdb.KeyValueWriter, addressHash common.Hash, storageHash common.Hash, data []byte) {
|
||||
if err := db.Put(storageHistoryIndexKey(addressHash, storageHash), data); err != nil {
|
||||
log.Crit("Failed to store storage history index", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteStorageHistoryIndex deletes the specified state index from the database.
|
||||
func DeleteStorageHistoryIndex(db ethdb.KeyValueWriter, addressHash common.Hash, storageHash common.Hash) {
|
||||
if err := db.Delete(storageHistoryIndexKey(addressHash, storageHash)); err != nil {
|
||||
log.Crit("Failed to delete storage history index", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadAccountHistoryIndexBlock retrieves the index block with the provided
|
||||
// account address along with the block id.
|
||||
func ReadAccountHistoryIndexBlock(db ethdb.KeyValueReader, addressHash common.Hash, blockID uint32) []byte {
|
||||
data, err := db.Get(accountHistoryIndexBlockKey(addressHash, blockID))
|
||||
if err != nil || len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// WriteAccountHistoryIndexBlock writes the provided index block into database.
|
||||
func WriteAccountHistoryIndexBlock(db ethdb.KeyValueWriter, addressHash common.Hash, blockID uint32, data []byte) {
|
||||
if err := db.Put(accountHistoryIndexBlockKey(addressHash, blockID), data); err != nil {
|
||||
log.Crit("Failed to store account index block", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteAccountHistoryIndexBlock deletes the specified index block from the database.
|
||||
func DeleteAccountHistoryIndexBlock(db ethdb.KeyValueWriter, addressHash common.Hash, blockID uint32) {
|
||||
if err := db.Delete(accountHistoryIndexBlockKey(addressHash, blockID)); err != nil {
|
||||
log.Crit("Failed to delete account index block", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadStorageHistoryIndexBlock retrieves the index block with the provided state
|
||||
// identifier along with the block id.
|
||||
func ReadStorageHistoryIndexBlock(db ethdb.KeyValueReader, addressHash common.Hash, storageHash common.Hash, blockID uint32) []byte {
|
||||
data, err := db.Get(storageHistoryIndexBlockKey(addressHash, storageHash, blockID))
|
||||
if err != nil || len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// WriteStorageHistoryIndexBlock writes the provided index block into database.
|
||||
func WriteStorageHistoryIndexBlock(db ethdb.KeyValueWriter, addressHash common.Hash, storageHash common.Hash, id uint32, data []byte) {
|
||||
if err := db.Put(storageHistoryIndexBlockKey(addressHash, storageHash, id), data); err != nil {
|
||||
log.Crit("Failed to store storage index block", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteStorageHistoryIndexBlock deletes the specified index block from the database.
|
||||
func DeleteStorageHistoryIndexBlock(db ethdb.KeyValueWriter, addressHash common.Hash, storageHash common.Hash, id uint32) {
|
||||
if err := db.Delete(storageHistoryIndexBlockKey(addressHash, storageHash, id)); err != nil {
|
||||
log.Crit("Failed to delete storage index block", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// increaseKey increase the input key by one bit. Return nil if the entire
|
||||
// addition operation overflows.
|
||||
func increaseKey(key []byte) []byte {
|
||||
for i := len(key) - 1; i >= 0; i-- {
|
||||
key[i]++
|
||||
if key[i] != 0x0 {
|
||||
return key
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteStateHistoryIndex completely removes all history indexing data, including
|
||||
// indexes for accounts and storages.
|
||||
//
|
||||
// Note, this method assumes the storage space with prefix `StateHistoryIndexPrefix`
|
||||
// is exclusively occupied by the history indexing data!
|
||||
func DeleteStateHistoryIndex(db ethdb.KeyValueRangeDeleter) {
|
||||
start := StateHistoryIndexPrefix
|
||||
limit := increaseKey(bytes.Clone(StateHistoryIndexPrefix))
|
||||
|
||||
// Try to remove the data in the range by a loop, as the leveldb
|
||||
// doesn't support the native range deletion.
|
||||
for {
|
||||
err := db.DeleteRange(start, limit)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ethdb.ErrTooManyKeys) {
|
||||
continue
|
||||
}
|
||||
log.Crit("Failed to delete history index range", "err", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ package rawdb
|
|||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
|
|
@ -255,6 +256,36 @@ func ReadStateHistory(db ethdb.AncientReaderOp, id uint64) ([]byte, []byte, []by
|
|||
return meta, accountIndex, storageIndex, accountData, storageData, nil
|
||||
}
|
||||
|
||||
// ReadStateHistoryList retrieves a list of state histories from database with
|
||||
// specific range. Compute the position of state history in freezer by minus one
|
||||
// since the id of first state history starts from one(zero for initial state).
|
||||
func ReadStateHistoryList(db ethdb.AncientReaderOp, start uint64, count uint64) ([][]byte, [][]byte, [][]byte, [][]byte, [][]byte, error) {
|
||||
metaList, err := db.AncientRange(stateHistoryMeta, start-1, count, 0)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
aIndexList, err := db.AncientRange(stateHistoryAccountIndex, start-1, count, 0)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
sIndexList, err := db.AncientRange(stateHistoryStorageIndex, start-1, count, 0)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
aDataList, err := db.AncientRange(stateHistoryAccountData, start-1, count, 0)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
sDataList, err := db.AncientRange(stateHistoryStorageData, start-1, count, 0)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
if len(metaList) != len(aIndexList) || len(metaList) != len(sIndexList) || len(metaList) != len(aDataList) || len(metaList) != len(sDataList) {
|
||||
return nil, nil, nil, nil, nil, errors.New("state history is corrupted")
|
||||
}
|
||||
return metaList, aIndexList, sIndexList, aDataList, sDataList, nil
|
||||
}
|
||||
|
||||
// WriteStateHistory writes the provided state history to database. Compute the
|
||||
// position of state history in freezer by minus one since the id of first state
|
||||
// history starts from one(zero for initial state).
|
||||
|
|
|
|||
|
|
@ -412,6 +412,9 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
|
|||
filterMapLastBlock stat
|
||||
filterMapBlockLV stat
|
||||
|
||||
// Path-mode archive data
|
||||
stateIndex stat
|
||||
|
||||
// Verkle statistics
|
||||
verkleTries stat
|
||||
verkleStateLookups stat
|
||||
|
|
@ -489,6 +492,10 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
|
|||
case bytes.HasPrefix(key, bloomBitsMetaPrefix) && len(key) < len(bloomBitsMetaPrefix)+8:
|
||||
bloomBits.Add(size)
|
||||
|
||||
// Path-based historic state indexes
|
||||
case bytes.HasPrefix(key, StateHistoryIndexPrefix) && len(key) >= len(StateHistoryIndexPrefix)+common.HashLength:
|
||||
stateIndex.Add(size)
|
||||
|
||||
// Verkle trie data is detected, determine the sub-category
|
||||
case bytes.HasPrefix(key, VerklePrefix):
|
||||
remain := key[len(VerklePrefix):]
|
||||
|
|
@ -544,6 +551,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
|
|||
{"Key-Value store", "Path trie state lookups", stateLookups.Size(), stateLookups.Count()},
|
||||
{"Key-Value store", "Path trie account nodes", accountTries.Size(), accountTries.Count()},
|
||||
{"Key-Value store", "Path trie storage nodes", storageTries.Size(), storageTries.Count()},
|
||||
{"Key-Value store", "Path state history indexes", stateIndex.Size(), stateIndex.Count()},
|
||||
{"Key-Value store", "Verkle trie nodes", verkleTries.Size(), verkleTries.Count()},
|
||||
{"Key-Value store", "Verkle trie state lookups", verkleStateLookups.Size(), verkleStateLookups.Count()},
|
||||
{"Key-Value store", "Trie preimages", preimages.Size(), preimages.Count()},
|
||||
|
|
@ -591,7 +599,7 @@ var knownMetadataKeys = [][]byte{
|
|||
snapshotGeneratorKey, snapshotRecoveryKey, txIndexTailKey, fastTxLookupLimitKey,
|
||||
uncleanShutdownKey, badBlockKey, transitionStatusKey, skeletonSyncStatusKey,
|
||||
persistentStateIDKey, trieJournalKey, snapshotSyncStatusKey, snapSyncStatusFlagKey,
|
||||
filterMapsRangeKey,
|
||||
filterMapsRangeKey, headStateHistoryIndexKey,
|
||||
}
|
||||
|
||||
// printChainMetadata prints out chain metadata to stderr.
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ var (
|
|||
// trieJournalKey tracks the in-memory trie node layers across restarts.
|
||||
trieJournalKey = []byte("TrieJournal")
|
||||
|
||||
// headStateHistoryIndexKey tracks the ID of the latest state history that has
|
||||
// been indexed.
|
||||
headStateHistoryIndexKey = []byte("LastStateHistoryIndex")
|
||||
|
||||
// txIndexTailKey tracks the oldest block whose transactions have been indexed.
|
||||
txIndexTailKey = []byte("TransactionIndexTail")
|
||||
|
||||
|
|
@ -117,6 +121,13 @@ var (
|
|||
TrieNodeStoragePrefix = []byte("O") // TrieNodeStoragePrefix + accountHash + hexPath -> trie node
|
||||
stateIDPrefix = []byte("L") // stateIDPrefix + state root -> state id
|
||||
|
||||
// State history indexing within path-based storage scheme
|
||||
StateHistoryIndexPrefix = []byte("m") // The global prefix of state history index data
|
||||
StateHistoryAccountMetadataPrefix = []byte("ma") // StateHistoryAccountMetadataPrefix + account address hash => account metadata
|
||||
StateHistoryStorageMetadataPrefix = []byte("ms") // StateHistoryStorageMetadataPrefix + account address hash + storage slot hash => slot metadata
|
||||
StateHistoryAccountBlockPrefix = []byte("mba") // StateHistoryAccountBlockPrefix + account address hash + block_number => account block
|
||||
StateHistoryStorageBlockPrefix = []byte("mbs") // StateHistoryStorageBlockPrefix + account address hash + storage slot hash + block_number => slot block
|
||||
|
||||
// VerklePrefix is the database prefix for Verkle trie data, which includes:
|
||||
// (a) Trie nodes
|
||||
// (b) In-memory trie node journal
|
||||
|
|
@ -362,3 +373,27 @@ func filterMapBlockLVKey(number uint64) []byte {
|
|||
binary.BigEndian.PutUint64(key[l:], number)
|
||||
return key
|
||||
}
|
||||
|
||||
// accountHistoryIndexKey = StateHistoryAccountMetadataPrefix + addressHash
|
||||
func accountHistoryIndexKey(addressHash common.Hash) []byte {
|
||||
return append(StateHistoryAccountMetadataPrefix, addressHash.Bytes()...)
|
||||
}
|
||||
|
||||
// storageHistoryIndexKey = StateHistoryStorageMetadataPrefix + addressHash + storageHash
|
||||
func storageHistoryIndexKey(addressHash common.Hash, storageHash common.Hash) []byte {
|
||||
return append(append(StateHistoryStorageMetadataPrefix, addressHash.Bytes()...), storageHash.Bytes()...)
|
||||
}
|
||||
|
||||
// accountHistoryIndexBlockKey = StateHistoryAccountBlockPrefix + addressHash + blockID
|
||||
func accountHistoryIndexBlockKey(addressHash common.Hash, blockID uint32) []byte {
|
||||
var buf [4]byte
|
||||
binary.BigEndian.PutUint32(buf[:], blockID)
|
||||
return append(append(StateHistoryAccountBlockPrefix, addressHash.Bytes()...), buf[:]...)
|
||||
}
|
||||
|
||||
// storageHistoryIndexBlockKey = StateHistoryStorageBlockPrefix + addressHash + storageHash + blockID
|
||||
func storageHistoryIndexBlockKey(addressHash common.Hash, storageHash common.Hash, blockID uint32) []byte {
|
||||
var buf [4]byte
|
||||
binary.BigEndian.PutUint32(buf[:], blockID)
|
||||
return append(append(append(StateHistoryStorageBlockPrefix, addressHash.Bytes()...), storageHash.Bytes()...), buf[:]...)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,6 +112,9 @@ func (d iterativeDump) OnRoot(root common.Hash) {
|
|||
|
||||
// DumpToCollector iterates the state according to the given options and inserts
|
||||
// the items into a collector for aggregation or serialization.
|
||||
//
|
||||
// The state iterator is still trie-based and can be converted to snapshot-based
|
||||
// once the state snapshot is fully integrated into database. TODO(rjl493456442).
|
||||
func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []byte) {
|
||||
// Sanitize the input to allow nil configs
|
||||
if conf == nil {
|
||||
|
|
@ -123,15 +126,20 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
|
|||
start = time.Now()
|
||||
logged = time.Now()
|
||||
)
|
||||
log.Info("Trie dumping started", "root", s.trie.Hash())
|
||||
c.OnRoot(s.trie.Hash())
|
||||
log.Info("Trie dumping started", "root", s.originalRoot)
|
||||
c.OnRoot(s.originalRoot)
|
||||
|
||||
trieIt, err := s.trie.NodeIterator(conf.Start)
|
||||
tr, err := s.db.OpenTrie(s.originalRoot)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
trieIt, err := tr.NodeIterator(conf.Start)
|
||||
if err != nil {
|
||||
log.Error("Trie dumping error", "err", err)
|
||||
return nil
|
||||
}
|
||||
it := trie.NewIterator(trieIt)
|
||||
|
||||
for it.Next() {
|
||||
var data types.StateAccount
|
||||
if err := rlp.DecodeBytes(it.Value, &data); err != nil {
|
||||
|
|
@ -147,7 +155,7 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
|
|||
}
|
||||
address *common.Address
|
||||
addr common.Address
|
||||
addrBytes = s.trie.GetKey(it.Key)
|
||||
addrBytes = tr.GetKey(it.Key)
|
||||
)
|
||||
if addrBytes == nil {
|
||||
missingPreimages++
|
||||
|
|
@ -165,12 +173,13 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
|
|||
}
|
||||
if !conf.SkipStorage {
|
||||
account.Storage = make(map[common.Hash]string)
|
||||
tr, err := obj.getTrie()
|
||||
|
||||
storageTr, err := s.db.OpenStorageTrie(s.originalRoot, addr, obj.Root(), tr)
|
||||
if err != nil {
|
||||
log.Error("Failed to load storage trie", "err", err)
|
||||
continue
|
||||
}
|
||||
trieIt, err := tr.NodeIterator(nil)
|
||||
trieIt, err := storageTr.NodeIterator(nil)
|
||||
if err != nil {
|
||||
log.Error("Failed to create trie iterator", "err", err)
|
||||
continue
|
||||
|
|
@ -182,7 +191,7 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
|
|||
log.Error("Failed to decode the value returned by iterator", "error", err)
|
||||
continue
|
||||
}
|
||||
key := s.trie.GetKey(storageIt.Key)
|
||||
key := storageTr.GetKey(storageIt.Key)
|
||||
if key == nil {
|
||||
continue
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import (
|
|||
// required in order to resolve the contract address.
|
||||
type nodeIterator struct {
|
||||
state *StateDB // State being iterated
|
||||
tr Trie // Primary account trie for traversal
|
||||
|
||||
stateIt trie.NodeIterator // Primary iterator for the global state trie
|
||||
dataIt trie.NodeIterator // Secondary iterator for the data trie of a contract
|
||||
|
|
@ -75,13 +76,20 @@ func (it *nodeIterator) step() error {
|
|||
if it.state == nil {
|
||||
return nil
|
||||
}
|
||||
// Initialize the iterator if we've just started
|
||||
var err error
|
||||
if it.stateIt == nil {
|
||||
it.stateIt, err = it.state.trie.NodeIterator(nil)
|
||||
if it.tr == nil {
|
||||
tr, err := it.state.db.OpenTrie(it.state.originalRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
it.tr = tr
|
||||
}
|
||||
// Initialize the iterator if we've just started
|
||||
if it.stateIt == nil {
|
||||
stateIt, err := it.tr.NodeIterator(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
it.stateIt = stateIt
|
||||
}
|
||||
// If we had data nodes previously, we surely have at least state nodes
|
||||
if it.dataIt != nil {
|
||||
|
|
@ -116,14 +124,14 @@ func (it *nodeIterator) step() error {
|
|||
return err
|
||||
}
|
||||
// Lookup the preimage of account hash
|
||||
preimage := it.state.trie.GetKey(it.stateIt.LeafKey())
|
||||
preimage := it.tr.GetKey(it.stateIt.LeafKey())
|
||||
if preimage == nil {
|
||||
return errors.New("account address is not available")
|
||||
}
|
||||
address := common.BytesToAddress(preimage)
|
||||
|
||||
// Traverse the storage slots belong to the account
|
||||
dataTrie, err := it.state.db.OpenStorageTrie(it.state.originalRoot, address, account.Root, it.state.trie)
|
||||
dataTrie, err := it.state.db.OpenStorageTrie(it.state.originalRoot, address, account.Root, it.tr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ func newHelper(scheme string) *testHelper {
|
|||
if scheme == rawdb.PathScheme {
|
||||
config.PathDB = &pathdb.Config{
|
||||
SnapshotNoBuild: true,
|
||||
NoAsyncFlush: true,
|
||||
} // disable caching
|
||||
} else {
|
||||
config.HashDB = &hashdb.Config{} // disable caching
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ func (s *stateObject) touch() {
|
|||
// subsequent reads to expand the same trie instead of reloading from disk.
|
||||
func (s *stateObject) getTrie() (Trie, error) {
|
||||
if s.trie == nil {
|
||||
// Assumes the primary account trie is already loaded
|
||||
tr, err := s.db.db.OpenStorageTrie(s.db.originalRoot, s.address, s.data.Root, s.db.trie)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -54,8 +54,6 @@ func TestDump(t *testing.T) {
|
|||
obj3.SetBalance(uint256.NewInt(44))
|
||||
|
||||
// write some of them to the trie
|
||||
s.state.updateStateObject(obj1)
|
||||
s.state.updateStateObject(obj2)
|
||||
root, _ := s.state.Commit(0, false, false)
|
||||
|
||||
// check that DumpToCollector contains the state objects that are in trie
|
||||
|
|
@ -114,8 +112,6 @@ func TestIterativeDump(t *testing.T) {
|
|||
obj4.AddBalance(uint256.NewInt(1337))
|
||||
|
||||
// write some of them to the trie
|
||||
s.state.updateStateObject(obj1)
|
||||
s.state.updateStateObject(obj2)
|
||||
root, _ := s.state.Commit(0, false, false)
|
||||
s.state, _ = New(root, tdb)
|
||||
|
||||
|
|
|
|||
|
|
@ -79,8 +79,8 @@ func (m *mutation) isDelete() bool {
|
|||
type StateDB struct {
|
||||
db Database
|
||||
prefetcher *triePrefetcher
|
||||
trie Trie
|
||||
reader Reader
|
||||
trie Trie // it's resolved on first access
|
||||
|
||||
// originalRoot is the pre-state root, before any changes were made.
|
||||
// It will be updated when the Commit is called.
|
||||
|
|
@ -169,13 +169,8 @@ func New(root common.Hash, db Database) (*StateDB, error) {
|
|||
// NewWithReader creates a new state for the specified state root. Unlike New,
|
||||
// this function accepts an additional Reader which is bound to the given root.
|
||||
func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, error) {
|
||||
tr, err := db.OpenTrie(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdb := &StateDB{
|
||||
db: db,
|
||||
trie: tr,
|
||||
originalRoot: root,
|
||||
reader: reader,
|
||||
stateObjects: make(map[common.Address]*stateObject),
|
||||
|
|
@ -664,7 +659,6 @@ func (s *StateDB) Copy() *StateDB {
|
|||
// Copy all the basic fields, initialize the memory ones
|
||||
state := &StateDB{
|
||||
db: s.db,
|
||||
trie: mustCopyTrie(s.trie),
|
||||
reader: s.reader,
|
||||
originalRoot: s.originalRoot,
|
||||
stateObjects: make(map[common.Address]*stateObject, len(s.stateObjects)),
|
||||
|
|
@ -688,6 +682,9 @@ func (s *StateDB) Copy() *StateDB {
|
|||
transientStorage: s.transientStorage.Copy(),
|
||||
journal: s.journal.copy(),
|
||||
}
|
||||
if s.trie != nil {
|
||||
state.trie = mustCopyTrie(s.trie)
|
||||
}
|
||||
if s.witness != nil {
|
||||
state.witness = s.witness.Copy()
|
||||
}
|
||||
|
|
@ -783,6 +780,20 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
|
|||
// Finalise all the dirty storage states and write them into the tries
|
||||
s.Finalise(deleteEmptyObjects)
|
||||
|
||||
// Initialize the trie if it's not constructed yet. If the prefetch
|
||||
// is enabled, the trie constructed below will be replaced by the
|
||||
// prefetched one.
|
||||
//
|
||||
// This operation must be done before state object storage hashing,
|
||||
// as it assumes the main trie is already loaded.
|
||||
if s.trie == nil {
|
||||
tr, err := s.db.OpenTrie(s.originalRoot)
|
||||
if err != nil {
|
||||
s.setError(err)
|
||||
return common.Hash{}
|
||||
}
|
||||
s.trie = tr
|
||||
}
|
||||
// If there was a trie prefetcher operating, terminate it async so that the
|
||||
// individual storage tries can be updated as soon as the disk load finishes.
|
||||
if s.prefetcher != nil {
|
||||
|
|
|
|||
|
|
@ -171,7 +171,6 @@ func TestCopy(t *testing.T) {
|
|||
for i := byte(0); i < 255; i++ {
|
||||
obj := orig.getOrNewStateObject(common.BytesToAddress([]byte{i}))
|
||||
obj.AddBalance(uint256.NewInt(uint64(i)))
|
||||
orig.updateStateObject(obj)
|
||||
}
|
||||
orig.Finalise(false)
|
||||
|
||||
|
|
@ -190,10 +189,6 @@ func TestCopy(t *testing.T) {
|
|||
origObj.AddBalance(uint256.NewInt(2 * uint64(i)))
|
||||
copyObj.AddBalance(uint256.NewInt(3 * uint64(i)))
|
||||
ccopyObj.AddBalance(uint256.NewInt(4 * uint64(i)))
|
||||
|
||||
orig.updateStateObject(origObj)
|
||||
copy.updateStateObject(copyObj)
|
||||
ccopy.updateStateObject(copyObj)
|
||||
}
|
||||
|
||||
// Finalise the changes on all concurrently
|
||||
|
|
@ -238,7 +233,6 @@ func TestCopyWithDirtyJournal(t *testing.T) {
|
|||
obj := orig.getOrNewStateObject(common.BytesToAddress([]byte{i}))
|
||||
obj.AddBalance(uint256.NewInt(uint64(i)))
|
||||
obj.data.Root = common.HexToHash("0xdeadbeef")
|
||||
orig.updateStateObject(obj)
|
||||
}
|
||||
root, _ := orig.Commit(0, true, false)
|
||||
orig, _ = New(root, db)
|
||||
|
|
@ -248,8 +242,6 @@ func TestCopyWithDirtyJournal(t *testing.T) {
|
|||
obj := orig.getOrNewStateObject(common.BytesToAddress([]byte{i}))
|
||||
amount := uint256.NewInt(uint64(i))
|
||||
obj.SetBalance(new(uint256.Int).Sub(obj.Balance(), amount))
|
||||
|
||||
orig.updateStateObject(obj)
|
||||
}
|
||||
cpy := orig.Copy()
|
||||
|
||||
|
|
@ -284,7 +276,6 @@ func TestCopyObjectState(t *testing.T) {
|
|||
obj := orig.getOrNewStateObject(common.BytesToAddress([]byte{i}))
|
||||
obj.AddBalance(uint256.NewInt(uint64(i)))
|
||||
obj.data.Root = common.HexToHash("0xdeadbeef")
|
||||
orig.updateStateObject(obj)
|
||||
}
|
||||
orig.Finalise(true)
|
||||
cpy := orig.Copy()
|
||||
|
|
@ -573,7 +564,7 @@ func forEachStorage(s *StateDB, addr common.Address, cb func(key, value common.H
|
|||
)
|
||||
|
||||
for it.Next() {
|
||||
key := common.BytesToHash(s.trie.GetKey(it.Key))
|
||||
key := common.BytesToHash(tr.GetKey(it.Key))
|
||||
visited[key] = true
|
||||
if value, dirty := so.dirtyStorage[key]; dirty {
|
||||
if !cb(key, value) {
|
||||
|
|
@ -982,6 +973,7 @@ func testMissingTrieNodes(t *testing.T, scheme string) {
|
|||
TrieCleanSize: 0,
|
||||
StateCleanSize: 0,
|
||||
WriteBufferSize: 0,
|
||||
NoAsyncFlush: true,
|
||||
}}) // disable caching
|
||||
} else {
|
||||
tdb = triedb.NewDatabase(memDb, &triedb.Config{HashDB: &hashdb.Config{
|
||||
|
|
@ -1004,18 +996,25 @@ func testMissingTrieNodes(t *testing.T, scheme string) {
|
|||
// force-flush
|
||||
tdb.Commit(root, false)
|
||||
}
|
||||
// Create a new state on the old root
|
||||
state, _ = New(root, db)
|
||||
// Now we clear out the memdb
|
||||
it := memDb.NewIterator(nil, nil)
|
||||
for it.Next() {
|
||||
k := it.Key()
|
||||
// Leave the root intact
|
||||
if scheme == rawdb.HashScheme {
|
||||
if !bytes.Equal(k, root[:]) {
|
||||
t.Logf("key: %x", k)
|
||||
memDb.Delete(k)
|
||||
}
|
||||
}
|
||||
if scheme == rawdb.PathScheme {
|
||||
rk := k[len(rawdb.TrieNodeAccountPrefix):]
|
||||
if len(rk) != 0 {
|
||||
t.Logf("key: %x", k)
|
||||
memDb.Delete(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
state, _ = New(root, db)
|
||||
balance := state.GetBalance(addr)
|
||||
// The removed elem should lead to it returning zero balance
|
||||
if exp, got := uint64(0), balance.Uint64(); got != exp {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,9 @@ func makeTestState(scheme string) (ethdb.Database, Database, *triedb.Database, c
|
|||
// Create an empty state
|
||||
config := &triedb.Config{Preimages: true}
|
||||
if scheme == rawdb.PathScheme {
|
||||
config.PathDB = pathdb.Defaults
|
||||
pconfig := *pathdb.Defaults
|
||||
pconfig.NoAsyncFlush = true
|
||||
config.PathDB = &pconfig
|
||||
} else {
|
||||
config.HashDB = hashdb.Defaults
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c
|
|||
workers errgroup.Group
|
||||
reader = statedb.Reader()
|
||||
)
|
||||
workers.SetLimit(4 * runtime.NumCPU() / 5) // Aggressively run the prefetching
|
||||
workers.SetLimit(max(1, 4*runtime.NumCPU()/5)) // Aggressively run the prefetching
|
||||
|
||||
// Iterate over and process the individual transactions
|
||||
for i, tx := range block.Transactions() {
|
||||
|
|
|
|||
|
|
@ -161,10 +161,9 @@ func ApplyTransactionWithEVM(msg *Message, gp *GasPool, statedb *state.StateDB,
|
|||
|
||||
// Merge the tx-local access event into the "block-local" one, in order to collect
|
||||
// all values, so that the witness can be built.
|
||||
if statedb.GetTrie().IsVerkle() {
|
||||
if statedb.Database().TrieDB().IsVerkle() {
|
||||
statedb.AccessEvents().Merge(evm.AccessEvents)
|
||||
}
|
||||
|
||||
return MakeReceipt(evm, result, statedb, blockNumber, blockHash, blockTime, tx, *usedGas, root), nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -199,8 +199,8 @@ type BlockChain interface {
|
|||
// InsertChain inserts a batch of blocks into the local chain.
|
||||
InsertChain(types.Blocks) (int, error)
|
||||
|
||||
// StopInsert interrupts the inserting process.
|
||||
StopInsert()
|
||||
// InterruptInsert whether disables the chain insertion.
|
||||
InterruptInsert(on bool)
|
||||
|
||||
// InsertReceiptChain inserts a batch of blocks along with their receipts
|
||||
// into the local chain. Blocks older than the specified `ancientLimit`
|
||||
|
|
@ -630,16 +630,15 @@ func (d *Downloader) cancel() {
|
|||
// Cancel aborts all of the operations and waits for all download goroutines to
|
||||
// finish before returning.
|
||||
func (d *Downloader) Cancel() {
|
||||
d.blockchain.InterruptInsert(true)
|
||||
d.cancel()
|
||||
d.cancelWg.Wait()
|
||||
d.blockchain.InterruptInsert(false)
|
||||
}
|
||||
|
||||
// Terminate interrupts the downloader, canceling all pending operations.
|
||||
// The downloader cannot be reused after calling Terminate.
|
||||
func (d *Downloader) Terminate() {
|
||||
// Signal to stop inserting in-flight blocks
|
||||
d.blockchain.StopInsert()
|
||||
|
||||
// Close the termination channel (make sure double close is allowed)
|
||||
d.quitLock.Lock()
|
||||
select {
|
||||
|
|
|
|||
135
eth/tracers/internal/tracetest/testdata/prestate_tracer/7702_delegate.json
vendored
Normal file
135
eth/tracers/internal/tracetest/testdata/prestate_tracer/7702_delegate.json
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -81,6 +81,10 @@
|
|||
"0x0000000000000000000000000000000000000000": {
|
||||
"balance": "0x272e0528"
|
||||
},
|
||||
"0x000000000000000000000000000000000000bbbb": {
|
||||
"balance": "0x0",
|
||||
"code": "0x6042604255"
|
||||
},
|
||||
"0x703c4b2bd70c169f5717101caee543299fc946c7": {
|
||||
"balance": "0xde0b6b3a7640000",
|
||||
"storage": {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ type prestateTracer struct {
|
|||
post stateMap
|
||||
to common.Address
|
||||
config prestateTracerConfig
|
||||
chainConfig *params.ChainConfig
|
||||
interrupt atomic.Bool // Atomic flag to signal execution interruption
|
||||
reason error // Textual reason for the interruption
|
||||
created map[common.Address]bool
|
||||
|
|
@ -93,6 +94,7 @@ func newPrestateTracer(ctx *tracers.Context, cfg json.RawMessage, chainConfig *p
|
|||
pre: stateMap{},
|
||||
post: stateMap{},
|
||||
config: config,
|
||||
chainConfig: chainConfig,
|
||||
created: make(map[common.Address]bool),
|
||||
deleted: make(map[common.Address]bool),
|
||||
}
|
||||
|
|
@ -133,6 +135,13 @@ func (t *prestateTracer) OnOpcode(pc uint64, opcode byte, gas, cost uint64, scop
|
|||
case stackLen >= 5 && (op == vm.DELEGATECALL || op == vm.CALL || op == vm.STATICCALL || op == vm.CALLCODE):
|
||||
addr := common.Address(stackData[stackLen-2].Bytes20())
|
||||
t.lookupAccount(addr)
|
||||
// Lookup the delegation target
|
||||
if t.chainConfig.IsPrague(t.env.BlockNumber, t.env.Time) {
|
||||
code := t.env.StateDB.GetCode(addr)
|
||||
if target, ok := types.ParseDelegation(code); ok {
|
||||
t.lookupAccount(target)
|
||||
}
|
||||
}
|
||||
case op == vm.CREATE:
|
||||
nonce := t.env.StateDB.GetNonce(caller)
|
||||
addr := crypto.CreateAddress(caller, nonce)
|
||||
|
|
@ -161,6 +170,13 @@ func (t *prestateTracer) OnTxStart(env *tracing.VMContext, tx *types.Transaction
|
|||
t.created[t.to] = true
|
||||
} else {
|
||||
t.to = *tx.To()
|
||||
// Lookup the delegation target
|
||||
if t.chainConfig.IsPrague(t.env.BlockNumber, t.env.Time) {
|
||||
code := t.env.StateDB.GetCode(t.to)
|
||||
if target, ok := types.ParseDelegation(code); ok {
|
||||
t.lookupAccount(target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.lookupAccount(from)
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ type Database struct {
|
|||
estimatedCompDebtGauge *metrics.Gauge // Gauge for tracking the number of bytes that need to be compacted
|
||||
liveCompGauge *metrics.Gauge // Gauge for tracking the number of in-progress compactions
|
||||
liveCompSizeGauge *metrics.Gauge // Gauge for tracking the size of in-progress compactions
|
||||
liveIterGauge *metrics.Gauge // Gauge for tracking the number of live database iterators
|
||||
levelsGauge []*metrics.Gauge // Gauge for tracking the number of tables in levels
|
||||
|
||||
quitLock sync.RWMutex // Mutex protecting the quit channel and the closed flag
|
||||
|
|
@ -326,6 +327,7 @@ func New(file string, cache int, handles int, namespace string, readonly bool) (
|
|||
db.estimatedCompDebtGauge = metrics.GetOrRegisterGauge(namespace+"compact/estimateDebt", nil)
|
||||
db.liveCompGauge = metrics.GetOrRegisterGauge(namespace+"compact/live/count", nil)
|
||||
db.liveCompSizeGauge = metrics.GetOrRegisterGauge(namespace+"compact/live/size", nil)
|
||||
db.liveIterGauge = metrics.GetOrRegisterGauge(namespace+"iter/count", nil)
|
||||
|
||||
// Start up the metrics gathering and return
|
||||
go db.meter(metricsGatheringInterval, namespace)
|
||||
|
|
@ -582,6 +584,7 @@ func (d *Database) meter(refresh time.Duration, namespace string) {
|
|||
d.seekCompGauge.Update(stats.Compact.ReadCount)
|
||||
d.liveCompGauge.Update(stats.Compact.NumInProgress)
|
||||
d.liveCompSizeGauge.Update(stats.Compact.InProgressBytes)
|
||||
d.liveIterGauge.Update(stats.TableIters)
|
||||
|
||||
d.liveMemTablesGauge.Update(stats.MemTable.Count)
|
||||
d.zombieMemTablesGauge.Update(stats.MemTable.ZombieCount)
|
||||
|
|
|
|||
|
|
@ -1831,8 +1831,16 @@ func (api *DebugAPI) ChaindbCompact() error {
|
|||
}
|
||||
|
||||
// SetHead rewinds the head of the blockchain to a previous block.
|
||||
func (api *DebugAPI) SetHead(number hexutil.Uint64) {
|
||||
func (api *DebugAPI) SetHead(number hexutil.Uint64) error {
|
||||
header := api.b.CurrentHeader()
|
||||
if header == nil {
|
||||
return errors.New("current header is not available")
|
||||
}
|
||||
if header.Number.Uint64() <= uint64(number) {
|
||||
return errors.New("not allowed to rewind to a future block")
|
||||
}
|
||||
api.b.SetHead(uint64(number))
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetAPI offers network related RPC methods
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ type Config struct {
|
|||
|
||||
// DefaultConfig contains default settings for miner.
|
||||
var DefaultConfig = Config{
|
||||
GasCeil: 36_000_000,
|
||||
GasCeil: 45_000_000,
|
||||
GasPrice: big.NewInt(params.GWei / 1000),
|
||||
|
||||
// The default recommit time is chosen as two seconds since
|
||||
|
|
|
|||
|
|
@ -317,6 +317,16 @@ func (db *Database) Journal(root common.Hash) error {
|
|||
return pdb.Journal(root)
|
||||
}
|
||||
|
||||
// VerifyState traverses the flat states specified by the given state root and
|
||||
// ensures they are matched with each other.
|
||||
func (db *Database) VerifyState(root common.Hash) error {
|
||||
pdb, ok := db.backend.(*pathdb.Database)
|
||||
if !ok {
|
||||
return errors.New("not supported")
|
||||
}
|
||||
return pdb.VerifyState(root)
|
||||
}
|
||||
|
||||
// AccountIterator creates a new account iterator for the specified root hash and
|
||||
// seeks to a starting account hash.
|
||||
func (db *Database) AccountIterator(root common.Hash, seek common.Hash) (pathdb.AccountIterator, error) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package pathdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
|
|
@ -37,6 +38,13 @@ type buffer struct {
|
|||
limit uint64 // The maximum memory allowance in bytes
|
||||
nodes *nodeSet // Aggregated trie node set
|
||||
states *stateSet // Aggregated state set
|
||||
|
||||
// done is the notifier whether the content in buffer has been flushed or not.
|
||||
// This channel is nil if the buffer is not frozen.
|
||||
done chan struct{}
|
||||
|
||||
// flushErr memorizes the error if any exception occurs during flushing
|
||||
flushErr error
|
||||
}
|
||||
|
||||
// newBuffer initializes the buffer with the provided states and trie nodes.
|
||||
|
|
@ -61,7 +69,7 @@ func (b *buffer) account(hash common.Hash) ([]byte, bool) {
|
|||
return b.states.account(hash)
|
||||
}
|
||||
|
||||
// storage retrieves the storage slot with account address hash and slot key.
|
||||
// storage retrieves the storage slot with account address hash and slot key hash.
|
||||
func (b *buffer) storage(addrHash common.Hash, storageHash common.Hash) ([]byte, bool) {
|
||||
return b.states.storage(addrHash, storageHash)
|
||||
}
|
||||
|
|
@ -124,12 +132,29 @@ func (b *buffer) size() uint64 {
|
|||
|
||||
// flush persists the in-memory dirty trie node into the disk if the configured
|
||||
// memory threshold is reached. Note, all data must be written atomically.
|
||||
func (b *buffer) flush(root common.Hash, db ethdb.KeyValueStore, freezer ethdb.AncientWriter, progress []byte, nodesCache, statesCache *fastcache.Cache, id uint64) error {
|
||||
func (b *buffer) flush(root common.Hash, db ethdb.KeyValueStore, freezer ethdb.AncientWriter, progress []byte, nodesCache, statesCache *fastcache.Cache, id uint64, postFlush func()) {
|
||||
if b.done != nil {
|
||||
panic("duplicated flush operation")
|
||||
}
|
||||
b.done = make(chan struct{}) // allocate the channel for notification
|
||||
|
||||
// Schedule the background thread to construct the batch, which usually
|
||||
// take a few seconds.
|
||||
go func() {
|
||||
defer func() {
|
||||
if postFlush != nil {
|
||||
postFlush()
|
||||
}
|
||||
close(b.done)
|
||||
}()
|
||||
|
||||
// Ensure the target state id is aligned with the internal counter.
|
||||
head := rawdb.ReadPersistentStateID(db)
|
||||
if head+b.layers != id {
|
||||
return fmt.Errorf("buffer layers (%d) cannot be applied on top of persisted state id (%d) to reach requested state id (%d)", b.layers, head, id)
|
||||
b.flushErr = fmt.Errorf("buffer layers (%d) cannot be applied on top of persisted state id (%d) to reach requested state id (%d)", b.layers, head, id)
|
||||
return
|
||||
}
|
||||
|
||||
// Terminate the state snapshot generation if it's active
|
||||
var (
|
||||
start = time.Now()
|
||||
|
|
@ -142,7 +167,8 @@ func (b *buffer) flush(root common.Hash, db ethdb.KeyValueStore, freezer ethdb.A
|
|||
// available for state rollback.
|
||||
if freezer != nil {
|
||||
if err := freezer.SyncAncient(); err != nil {
|
||||
return err
|
||||
b.flushErr = err
|
||||
return
|
||||
}
|
||||
}
|
||||
nodes := b.nodes.write(batch, nodesCache)
|
||||
|
|
@ -153,14 +179,31 @@ func (b *buffer) flush(root common.Hash, db ethdb.KeyValueStore, freezer ethdb.A
|
|||
// Flush all mutations in a single batch
|
||||
size := batch.ValueSize()
|
||||
if err := batch.Write(); err != nil {
|
||||
return err
|
||||
b.flushErr = err
|
||||
return
|
||||
}
|
||||
commitBytesMeter.Mark(int64(size))
|
||||
commitNodesMeter.Mark(int64(nodes))
|
||||
commitAccountsMeter.Mark(int64(accounts))
|
||||
commitStoragesMeter.Mark(int64(slots))
|
||||
commitTimeTimer.UpdateSince(start)
|
||||
b.reset()
|
||||
|
||||
// The content in the frozen buffer is kept for consequent state access,
|
||||
// TODO (rjl493456442) measure the gc overhead for holding this struct.
|
||||
// TODO (rjl493456442) can we somehow get rid of it after flushing??
|
||||
// TODO (rjl493456442) buffer itself is not thread-safe, add the lock
|
||||
// protection if try to reset the buffer here.
|
||||
// b.reset()
|
||||
log.Debug("Persisted buffer content", "nodes", nodes, "accounts", accounts, "slots", slots, "bytes", common.StorageSize(size), "elapsed", common.PrettyDuration(time.Since(start)))
|
||||
return nil
|
||||
}()
|
||||
}
|
||||
|
||||
// waitFlush blocks until the buffer has been fully flushed and returns any
|
||||
// stored errors that occurred during the process.
|
||||
func (b *buffer) waitFlush() error {
|
||||
if b.done == nil {
|
||||
return errors.New("the buffer is not frozen")
|
||||
}
|
||||
<-b.done
|
||||
return b.flushErr
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,11 +115,16 @@ type layer interface {
|
|||
// Config contains the settings for database.
|
||||
type Config struct {
|
||||
StateHistory uint64 // Number of recent blocks to maintain state history for
|
||||
EnableStateIndexing bool // Whether to enable state history indexing for external state access
|
||||
TrieCleanSize int // Maximum memory allowance (in bytes) for caching clean trie nodes
|
||||
StateCleanSize int // Maximum memory allowance (in bytes) for caching clean state data
|
||||
WriteBufferSize int // Maximum memory allowance (in bytes) for write buffer
|
||||
ReadOnly bool // Flag whether the database is opened in read only mode
|
||||
SnapshotNoBuild bool // Flag Whether the background generation is allowed
|
||||
|
||||
// Testing configurations
|
||||
SnapshotNoBuild bool // Flag Whether the state generation is allowed
|
||||
NoAsyncFlush bool // Flag whether the background buffer flushing is allowed
|
||||
NoAsyncGeneration bool // Flag whether the background generation is allowed
|
||||
}
|
||||
|
||||
// sanitize checks the provided user configurations and changes anything that's
|
||||
|
|
@ -145,7 +150,12 @@ func (c *Config) fields() []interface{} {
|
|||
list = append(list, "triecache", common.StorageSize(c.TrieCleanSize))
|
||||
list = append(list, "statecache", common.StorageSize(c.StateCleanSize))
|
||||
list = append(list, "buffer", common.StorageSize(c.WriteBufferSize))
|
||||
list = append(list, "history", c.StateHistory)
|
||||
|
||||
if c.StateHistory == 0 {
|
||||
list = append(list, "history", "entire chain")
|
||||
} else {
|
||||
list = append(list, "history", fmt.Sprintf("last %d blocks", c.StateHistory))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
|
|
@ -209,6 +219,7 @@ type Database struct {
|
|||
tree *layerTree // The group for all known layers
|
||||
freezer ethdb.ResettableAncientStore // Freezer for storing trie histories, nil possible in tests
|
||||
lock sync.RWMutex // Lock to prevent mutations from happening at the same time
|
||||
indexer *historyIndexer // History indexer
|
||||
}
|
||||
|
||||
// New attempts to load an already existing layer from a persistent key-value
|
||||
|
|
@ -258,6 +269,11 @@ func New(diskdb ethdb.Database, config *Config, isVerkle bool) *Database {
|
|||
if err := db.setStateGenerator(); err != nil {
|
||||
log.Crit("Failed to setup the generator", "err", err)
|
||||
}
|
||||
// TODO (rjl493456442) disable the background indexing in read-only mode
|
||||
if db.freezer != nil && db.config.EnableStateIndexing {
|
||||
db.indexer = newHistoryIndexer(db.diskdb, db.freezer, db.tree.bottom().stateID())
|
||||
log.Info("Enabled state history indexing")
|
||||
}
|
||||
fields := config.fields()
|
||||
if db.isVerkle {
|
||||
fields = append(fields, "verkle", true)
|
||||
|
|
@ -295,6 +311,11 @@ func (db *Database) repairHistory() error {
|
|||
log.Crit("Failed to retrieve head of state history", "err", err)
|
||||
}
|
||||
if frozen != 0 {
|
||||
// TODO(rjl493456442) would be better to group them into a batch.
|
||||
//
|
||||
// Purge all state history indexing data first
|
||||
rawdb.DeleteStateHistoryIndexMetadata(db.diskdb)
|
||||
rawdb.DeleteStateHistoryIndex(db.diskdb)
|
||||
err := db.freezer.Reset()
|
||||
if err != nil {
|
||||
log.Crit("Failed to reset state histories", "err", err)
|
||||
|
|
@ -366,6 +387,12 @@ func (db *Database) setStateGenerator() error {
|
|||
}
|
||||
stats.log("Starting snapshot generation", root, generator.Marker)
|
||||
dl.generator.run(root)
|
||||
|
||||
// Block until the generation completes. It's the feature used in
|
||||
// unit tests.
|
||||
if db.config.NoAsyncGeneration {
|
||||
<-dl.generator.done
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -434,8 +461,8 @@ func (db *Database) Disable() error {
|
|||
// Terminate the state generator if it's active and mark the disk layer
|
||||
// as stale to prevent access to persistent state.
|
||||
disk := db.tree.bottom()
|
||||
if disk.generator != nil {
|
||||
disk.generator.stop()
|
||||
if err := disk.terminate(); err != nil {
|
||||
return err
|
||||
}
|
||||
disk.markStale()
|
||||
|
||||
|
|
@ -477,6 +504,11 @@ func (db *Database) Enable(root common.Hash) error {
|
|||
// mappings can be huge and might take a while to clear
|
||||
// them, just leave them in disk and wait for overwriting.
|
||||
if db.freezer != nil {
|
||||
// TODO(rjl493456442) would be better to group them into a batch.
|
||||
//
|
||||
// Purge all state history indexing data first
|
||||
rawdb.DeleteStateHistoryIndexMetadata(db.diskdb)
|
||||
rawdb.DeleteStateHistoryIndex(db.diskdb)
|
||||
if err := db.freezer.Reset(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -592,13 +624,19 @@ func (db *Database) Close() error {
|
|||
// following mutations.
|
||||
db.readOnly = true
|
||||
|
||||
// Terminate the background generation if it's active
|
||||
disk := db.tree.bottom()
|
||||
if disk.generator != nil {
|
||||
disk.generator.stop()
|
||||
// Block until the background flushing is finished. It must
|
||||
// be done before terminating the potential background snapshot
|
||||
// generator.
|
||||
dl := db.tree.bottom()
|
||||
if err := dl.terminate(); err != nil {
|
||||
return err
|
||||
}
|
||||
disk.resetCache() // release the memory held by clean cache
|
||||
dl.resetCache() // release the memory held by clean cache
|
||||
|
||||
// Terminate the background state history indexer
|
||||
if db.indexer != nil {
|
||||
db.indexer.close()
|
||||
}
|
||||
// Close the attached state history freezer.
|
||||
if db.freezer == nil {
|
||||
return nil
|
||||
|
|
@ -662,16 +700,6 @@ func (db *Database) HistoryRange() (uint64, uint64, error) {
|
|||
return historyRange(db.freezer)
|
||||
}
|
||||
|
||||
// waitGeneration waits until the background generation is finished. It assumes
|
||||
// that the generation is permitted; otherwise, it will block indefinitely.
|
||||
func (db *Database) waitGeneration() {
|
||||
gen := db.tree.bottom().generator
|
||||
if gen == nil || gen.completed() {
|
||||
return
|
||||
}
|
||||
<-gen.done
|
||||
}
|
||||
|
||||
// AccountIterator creates a new account iterator for the specified root hash and
|
||||
// seeks to a starting account hash.
|
||||
func (db *Database) AccountIterator(root common.Hash, seek common.Hash) (AccountIterator, error) {
|
||||
|
|
@ -681,7 +709,7 @@ func (db *Database) AccountIterator(root common.Hash, seek common.Hash) (Account
|
|||
if wait {
|
||||
return nil, errDatabaseWaitSync
|
||||
}
|
||||
if gen := db.tree.bottom().generator; gen != nil && !gen.completed() {
|
||||
if !db.tree.bottom().genComplete() {
|
||||
return nil, errNotConstructed
|
||||
}
|
||||
return newFastAccountIterator(db, root, seek)
|
||||
|
|
@ -696,7 +724,7 @@ func (db *Database) StorageIterator(root common.Hash, account common.Hash, seek
|
|||
if wait {
|
||||
return nil, errDatabaseWaitSync
|
||||
}
|
||||
if gen := db.tree.bottom().generator; gen != nil && !gen.completed() {
|
||||
if !db.tree.bottom().genComplete() {
|
||||
return nil, errNotConstructed
|
||||
}
|
||||
return newFastStorageIterator(db, root, account, seek)
|
||||
|
|
|
|||
|
|
@ -121,14 +121,16 @@ type tester struct {
|
|||
snapStorages map[common.Hash]map[common.Hash]map[common.Hash][]byte // Keyed by the hash of account address and the hash of storage key
|
||||
}
|
||||
|
||||
func newTester(t *testing.T, historyLimit uint64, isVerkle bool, layers int) *tester {
|
||||
func newTester(t *testing.T, historyLimit uint64, isVerkle bool, layers int, enableIndex bool) *tester {
|
||||
var (
|
||||
disk, _ = rawdb.Open(rawdb.NewMemoryDatabase(), rawdb.OpenOptions{Ancient: t.TempDir()})
|
||||
db = New(disk, &Config{
|
||||
StateHistory: historyLimit,
|
||||
EnableStateIndexing: enableIndex,
|
||||
TrieCleanSize: 256 * 1024,
|
||||
StateCleanSize: 256 * 1024,
|
||||
WriteBufferSize: 256 * 1024,
|
||||
NoAsyncFlush: true,
|
||||
}, isVerkle)
|
||||
|
||||
obj = &tester{
|
||||
|
|
@ -163,6 +165,20 @@ func (t *tester) hashPreimage(hash common.Hash) common.Hash {
|
|||
return common.BytesToHash(t.preimages[hash])
|
||||
}
|
||||
|
||||
func (t *tester) extend(layers int) {
|
||||
for i := 0; i < layers; i++ {
|
||||
var parent = types.EmptyRootHash
|
||||
if len(t.roots) != 0 {
|
||||
parent = t.roots[len(t.roots)-1]
|
||||
}
|
||||
root, nodes, states := t.generate(parent, true)
|
||||
if err := t.db.Update(root, parent, uint64(i), nodes, states); err != nil {
|
||||
panic(fmt.Errorf("failed to update state changes, err: %w", err))
|
||||
}
|
||||
t.roots = append(t.roots, root)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tester) release() {
|
||||
t.db.Close()
|
||||
t.db.diskdb.Close()
|
||||
|
|
@ -450,7 +466,7 @@ func TestDatabaseRollback(t *testing.T) {
|
|||
}()
|
||||
|
||||
// Verify state histories
|
||||
tester := newTester(t, 0, false, 32)
|
||||
tester := newTester(t, 0, false, 32, false)
|
||||
defer tester.release()
|
||||
|
||||
if err := tester.verifyHistory(); err != nil {
|
||||
|
|
@ -484,7 +500,7 @@ func TestDatabaseRecoverable(t *testing.T) {
|
|||
}()
|
||||
|
||||
var (
|
||||
tester = newTester(t, 0, false, 12)
|
||||
tester = newTester(t, 0, false, 12, false)
|
||||
index = tester.bottomIndex()
|
||||
)
|
||||
defer tester.release()
|
||||
|
|
@ -528,7 +544,7 @@ func TestDisable(t *testing.T) {
|
|||
maxDiffLayers = 128
|
||||
}()
|
||||
|
||||
tester := newTester(t, 0, false, 32)
|
||||
tester := newTester(t, 0, false, 32, false)
|
||||
defer tester.release()
|
||||
|
||||
stored := crypto.Keccak256Hash(rawdb.ReadAccountTrieNode(tester.db.diskdb, nil))
|
||||
|
|
@ -570,7 +586,7 @@ func TestCommit(t *testing.T) {
|
|||
maxDiffLayers = 128
|
||||
}()
|
||||
|
||||
tester := newTester(t, 0, false, 12)
|
||||
tester := newTester(t, 0, false, 12, false)
|
||||
defer tester.release()
|
||||
|
||||
if err := tester.db.Commit(tester.lastHash(), false); err != nil {
|
||||
|
|
@ -600,7 +616,7 @@ func TestJournal(t *testing.T) {
|
|||
maxDiffLayers = 128
|
||||
}()
|
||||
|
||||
tester := newTester(t, 0, false, 12)
|
||||
tester := newTester(t, 0, false, 12, false)
|
||||
defer tester.release()
|
||||
|
||||
if err := tester.db.Journal(tester.lastHash()); err != nil {
|
||||
|
|
@ -630,7 +646,7 @@ func TestCorruptedJournal(t *testing.T) {
|
|||
maxDiffLayers = 128
|
||||
}()
|
||||
|
||||
tester := newTester(t, 0, false, 12)
|
||||
tester := newTester(t, 0, false, 12, false)
|
||||
defer tester.release()
|
||||
|
||||
if err := tester.db.Journal(tester.lastHash()); err != nil {
|
||||
|
|
@ -678,7 +694,7 @@ func TestTailTruncateHistory(t *testing.T) {
|
|||
maxDiffLayers = 128
|
||||
}()
|
||||
|
||||
tester := newTester(t, 10, false, 12)
|
||||
tester := newTester(t, 10, false, 12, false)
|
||||
defer tester.release()
|
||||
|
||||
tester.db.Close()
|
||||
|
|
|
|||
|
|
@ -41,17 +41,20 @@ type diskLayer struct {
|
|||
nodes *fastcache.Cache // GC friendly memory cache of clean nodes
|
||||
states *fastcache.Cache // GC friendly memory cache of clean states
|
||||
|
||||
buffer *buffer // Dirty buffer to aggregate writes of nodes and states
|
||||
buffer *buffer // Live buffer to aggregate writes
|
||||
frozen *buffer // Frozen node buffer waiting for flushing
|
||||
|
||||
stale bool // Signals that the layer became stale (state progressed)
|
||||
lock sync.RWMutex // Lock used to protect stale flag and genMarker
|
||||
|
||||
// The generator is set if the state snapshot was not fully completed,
|
||||
// regardless of whether the background generation is running or not.
|
||||
// It should only be unset if the generation completes.
|
||||
generator *generator
|
||||
}
|
||||
|
||||
// newDiskLayer creates a new disk layer based on the passing arguments.
|
||||
func newDiskLayer(root common.Hash, id uint64, db *Database, nodes *fastcache.Cache, states *fastcache.Cache, buffer *buffer) *diskLayer {
|
||||
func newDiskLayer(root common.Hash, id uint64, db *Database, nodes *fastcache.Cache, states *fastcache.Cache, buffer *buffer, frozen *buffer) *diskLayer {
|
||||
// Initialize the clean caches if the memory allowance is not zero
|
||||
// or reuse the provided caches if they are not nil (inherited from
|
||||
// the original disk layer).
|
||||
|
|
@ -68,6 +71,7 @@ func newDiskLayer(root common.Hash, id uint64, db *Database, nodes *fastcache.Ca
|
|||
nodes: nodes,
|
||||
states: states,
|
||||
buffer: buffer,
|
||||
frozen: frozen,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -114,17 +118,20 @@ func (dl *diskLayer) node(owner common.Hash, path []byte, depth int) ([]byte, co
|
|||
if dl.stale {
|
||||
return nil, common.Hash{}, nil, errSnapshotStale
|
||||
}
|
||||
// Try to retrieve the trie node from the not-yet-written
|
||||
// node buffer first. Note the buffer is lock free since
|
||||
// it's impossible to mutate the buffer before tagging the
|
||||
// layer as stale.
|
||||
n, found := dl.buffer.node(owner, path)
|
||||
// Try to retrieve the trie node from the not-yet-written node buffer first
|
||||
// (both the live one and the frozen one). Note the buffer is lock free since
|
||||
// it's impossible to mutate the buffer before tagging the layer as stale.
|
||||
for _, buffer := range []*buffer{dl.buffer, dl.frozen} {
|
||||
if buffer != nil {
|
||||
n, found := buffer.node(owner, path)
|
||||
if found {
|
||||
dirtyNodeHitMeter.Mark(1)
|
||||
dirtyNodeReadMeter.Mark(int64(len(n.Blob)))
|
||||
dirtyNodeHitDepthHist.Update(int64(depth))
|
||||
return n.Blob, n.Hash, &nodeLoc{loc: locDirtyCache, depth: depth}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
dirtyNodeMissMeter.Mark(1)
|
||||
|
||||
// Try to retrieve the trie node from the clean memory cache
|
||||
|
|
@ -144,6 +151,11 @@ func (dl *diskLayer) node(owner common.Hash, path []byte, depth int) ([]byte, co
|
|||
} else {
|
||||
blob = rawdb.ReadStorageTrieNode(dl.db.diskdb, owner, path)
|
||||
}
|
||||
// Store the resolved data in the clean cache. The background buffer flusher
|
||||
// may also write to the clean cache concurrently, but two writers cannot
|
||||
// write the same item with different content. If the item already exists,
|
||||
// it will be found in the frozen buffer, eliminating the need to check the
|
||||
// database.
|
||||
if dl.nodes != nil && len(blob) > 0 {
|
||||
dl.nodes.Set(key, blob)
|
||||
cleanNodeWriteMeter.Mark(int64(len(blob)))
|
||||
|
|
@ -162,11 +174,12 @@ func (dl *diskLayer) account(hash common.Hash, depth int) ([]byte, error) {
|
|||
if dl.stale {
|
||||
return nil, errSnapshotStale
|
||||
}
|
||||
// Try to retrieve the account from the not-yet-written
|
||||
// node buffer first. Note the buffer is lock free since
|
||||
// it's impossible to mutate the buffer before tagging the
|
||||
// layer as stale.
|
||||
blob, found := dl.buffer.account(hash)
|
||||
// Try to retrieve the trie node from the not-yet-written node buffer first
|
||||
// (both the live one and the frozen one). Note the buffer is lock free since
|
||||
// it's impossible to mutate the buffer before tagging the layer as stale.
|
||||
for _, buffer := range []*buffer{dl.buffer, dl.frozen} {
|
||||
if buffer != nil {
|
||||
blob, found := buffer.account(hash)
|
||||
if found {
|
||||
dirtyStateHitMeter.Mark(1)
|
||||
dirtyStateReadMeter.Mark(int64(len(blob)))
|
||||
|
|
@ -179,6 +192,8 @@ func (dl *diskLayer) account(hash common.Hash, depth int) ([]byte, error) {
|
|||
}
|
||||
return blob, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
dirtyStateMissMeter.Mark(1)
|
||||
|
||||
// If the layer is being generated, ensure the requested account has
|
||||
|
|
@ -203,7 +218,13 @@ func (dl *diskLayer) account(hash common.Hash, depth int) ([]byte, error) {
|
|||
cleanStateMissMeter.Mark(1)
|
||||
}
|
||||
// Try to retrieve the account from the disk.
|
||||
blob = rawdb.ReadAccountSnapshot(dl.db.diskdb, hash)
|
||||
blob := rawdb.ReadAccountSnapshot(dl.db.diskdb, hash)
|
||||
|
||||
// Store the resolved data in the clean cache. The background buffer flusher
|
||||
// may also write to the clean cache concurrently, but two writers cannot
|
||||
// write the same item with different content. If the item already exists,
|
||||
// it will be found in the frozen buffer, eliminating the need to check the
|
||||
// database.
|
||||
if dl.states != nil {
|
||||
dl.states.Set(hash[:], blob)
|
||||
cleanStateWriteMeter.Mark(int64(len(blob)))
|
||||
|
|
@ -231,11 +252,12 @@ func (dl *diskLayer) storage(accountHash, storageHash common.Hash, depth int) ([
|
|||
if dl.stale {
|
||||
return nil, errSnapshotStale
|
||||
}
|
||||
// Try to retrieve the storage slot from the not-yet-written
|
||||
// node buffer first. Note the buffer is lock free since
|
||||
// it's impossible to mutate the buffer before tagging the
|
||||
// layer as stale.
|
||||
if blob, found := dl.buffer.storage(accountHash, storageHash); found {
|
||||
// Try to retrieve the trie node from the not-yet-written node buffer first
|
||||
// (both the live one and the frozen one). Note the buffer is lock free since
|
||||
// it's impossible to mutate the buffer before tagging the layer as stale.
|
||||
for _, buffer := range []*buffer{dl.buffer, dl.frozen} {
|
||||
if buffer != nil {
|
||||
if blob, found := buffer.storage(accountHash, storageHash); found {
|
||||
dirtyStateHitMeter.Mark(1)
|
||||
dirtyStateReadMeter.Mark(int64(len(blob)))
|
||||
dirtyStateHitDepthHist.Update(int64(depth))
|
||||
|
|
@ -247,6 +269,8 @@ func (dl *diskLayer) storage(accountHash, storageHash common.Hash, depth int) ([
|
|||
}
|
||||
return blob, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
dirtyStateMissMeter.Mark(1)
|
||||
|
||||
// If the layer is being generated, ensure the requested storage slot
|
||||
|
|
@ -273,6 +297,12 @@ func (dl *diskLayer) storage(accountHash, storageHash common.Hash, depth int) ([
|
|||
}
|
||||
// Try to retrieve the account from the disk
|
||||
blob := rawdb.ReadStorageSnapshot(dl.db.diskdb, accountHash, storageHash)
|
||||
|
||||
// Store the resolved data in the clean cache. The background buffer flusher
|
||||
// may also write to the clean cache concurrently, but two writers cannot
|
||||
// write the same item with different content. If the item already exists,
|
||||
// it will be found in the frozen buffer, eliminating the need to check the
|
||||
// database.
|
||||
if dl.states != nil {
|
||||
dl.states.Set(key, blob)
|
||||
cleanStateWriteMeter.Mark(int64(len(blob)))
|
||||
|
|
@ -325,6 +355,12 @@ func (dl *diskLayer) commit(bottom *diffLayer, force bool) (*diskLayer, error) {
|
|||
overflow = true
|
||||
oldest = bottom.stateID() - limit + 1 // track the id of history **after truncation**
|
||||
}
|
||||
// Notify the state history indexer for newly created history
|
||||
if dl.db.indexer != nil {
|
||||
if err := dl.db.indexer.extend(bottom.stateID()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
// Mark the diskLayer as stale before applying any mutations on top.
|
||||
dl.stale = true
|
||||
|
|
@ -341,7 +377,8 @@ func (dl *diskLayer) commit(bottom *diffLayer, force bool) (*diskLayer, error) {
|
|||
// truncation) surpasses the persisted state ID, we take the necessary action
|
||||
// of forcibly committing the cached dirty states to ensure that the persisted
|
||||
// state ID remains higher.
|
||||
if !force && rawdb.ReadPersistentStateID(dl.db.diskdb) < oldest {
|
||||
persistedID := rawdb.ReadPersistentStateID(dl.db.diskdb)
|
||||
if !force && persistedID < oldest {
|
||||
force = true
|
||||
}
|
||||
// Merge the trie nodes and flat states of the bottom-most diff layer into the
|
||||
|
|
@ -351,12 +388,25 @@ func (dl *diskLayer) commit(bottom *diffLayer, force bool) (*diskLayer, error) {
|
|||
// Terminate the background state snapshot generation before mutating the
|
||||
// persistent state.
|
||||
if combined.full() || force {
|
||||
// Wait until the previous frozen buffer is fully flushed
|
||||
if dl.frozen != nil {
|
||||
if err := dl.frozen.waitFlush(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Release the frozen buffer and the internally referenced maps will
|
||||
// be reclaimed by GC.
|
||||
dl.frozen = nil
|
||||
|
||||
// Terminate the background state snapshot generator before flushing
|
||||
// to prevent data race.
|
||||
var progress []byte
|
||||
if dl.generator != nil {
|
||||
dl.generator.stop()
|
||||
progress = dl.generator.progressMarker()
|
||||
var (
|
||||
progress []byte
|
||||
gen = dl.generator
|
||||
)
|
||||
if gen != nil {
|
||||
gen.stop()
|
||||
progress = gen.progressMarker()
|
||||
|
||||
// If the snapshot has been fully generated, unset the generator
|
||||
if progress == nil {
|
||||
|
|
@ -365,18 +415,33 @@ func (dl *diskLayer) commit(bottom *diffLayer, force bool) (*diskLayer, error) {
|
|||
log.Info("Paused snapshot generation")
|
||||
}
|
||||
}
|
||||
// Flush the content in combined buffer. Any state data after the progress
|
||||
// marker will be ignored, as the generator will pick it up later.
|
||||
if err := combined.flush(bottom.root, dl.db.diskdb, dl.db.freezer, progress, dl.nodes, dl.states, bottom.stateID()); err != nil {
|
||||
|
||||
// Freeze the live buffer and schedule background flushing
|
||||
dl.frozen = combined
|
||||
dl.frozen.flush(bottom.root, dl.db.diskdb, dl.db.freezer, progress, dl.nodes, dl.states, bottom.stateID(), func() {
|
||||
// Resume the background generation if it's not completed yet.
|
||||
// The generator is assumed to be available if the progress is
|
||||
// not nil.
|
||||
//
|
||||
// Notably, the generator will be shared and linked by all the
|
||||
// disk layer instances, regardless of the generation is terminated
|
||||
// or not.
|
||||
if progress != nil {
|
||||
gen.run(bottom.root)
|
||||
}
|
||||
})
|
||||
// Block until the frozen buffer is fully flushed out if the async flushing
|
||||
// is not allowed, or if the oldest history surpasses the persisted state ID.
|
||||
if dl.db.config.NoAsyncFlush || persistedID < oldest {
|
||||
if err := dl.frozen.waitFlush(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Resume the background generation if it's not completed yet
|
||||
if progress != nil {
|
||||
dl.generator.run(bottom.root)
|
||||
dl.frozen = nil
|
||||
}
|
||||
combined = newBuffer(dl.db.config.WriteBufferSize, nil, nil, 0)
|
||||
}
|
||||
// Link the generator if snapshot is not yet completed
|
||||
ndl := newDiskLayer(bottom.root, bottom.stateID(), dl.db, dl.nodes, dl.states, combined)
|
||||
ndl := newDiskLayer(bottom.root, bottom.stateID(), dl.db, dl.nodes, dl.states, combined, dl.frozen)
|
||||
if dl.generator != nil {
|
||||
ndl.setGenerator(dl.generator)
|
||||
}
|
||||
|
|
@ -418,6 +483,12 @@ func (dl *diskLayer) revert(h *history) (*diskLayer, error) {
|
|||
|
||||
dl.stale = true
|
||||
|
||||
// Unindex the corresponding state history
|
||||
if dl.db.indexer != nil {
|
||||
if err := dl.db.indexer.shorten(dl.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// State change may be applied to node buffer, or the persistent
|
||||
// state, depends on if node buffer is empty or not. If the node
|
||||
// buffer is not empty, it means that the state transition that
|
||||
|
|
@ -428,7 +499,7 @@ func (dl *diskLayer) revert(h *history) (*diskLayer, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ndl := newDiskLayer(h.meta.parent, dl.id-1, dl.db, dl.nodes, dl.states, dl.buffer)
|
||||
ndl := newDiskLayer(h.meta.parent, dl.id-1, dl.db, dl.nodes, dl.states, dl.buffer, dl.frozen)
|
||||
|
||||
// Link the generator if it exists
|
||||
if dl.generator != nil {
|
||||
|
|
@ -437,7 +508,19 @@ func (dl *diskLayer) revert(h *history) (*diskLayer, error) {
|
|||
log.Debug("Reverted data in write buffer", "oldroot", h.meta.root, "newroot", h.meta.parent, "elapsed", common.PrettyDuration(time.Since(start)))
|
||||
return ndl, nil
|
||||
}
|
||||
// Terminate the generation before writing any data into database
|
||||
// Block until the frozen buffer is fully flushed
|
||||
if dl.frozen != nil {
|
||||
if err := dl.frozen.waitFlush(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Unset the frozen buffer if it exists, otherwise these "reverted"
|
||||
// states will still be accessible after revert in frozen buffer.
|
||||
dl.frozen = nil
|
||||
}
|
||||
|
||||
// Terminate the generator before writing any data to the database.
|
||||
// This must be done after flushing the frozen buffer, as the generator
|
||||
// may be restarted at the end of the flush process.
|
||||
var progress []byte
|
||||
if dl.generator != nil {
|
||||
dl.generator.stop()
|
||||
|
|
@ -455,7 +538,7 @@ func (dl *diskLayer) revert(h *history) (*diskLayer, error) {
|
|||
}
|
||||
// Link the generator and resume generation if the snapshot is not yet
|
||||
// fully completed.
|
||||
ndl := newDiskLayer(h.meta.parent, dl.id-1, dl.db, dl.nodes, dl.states, dl.buffer)
|
||||
ndl := newDiskLayer(h.meta.parent, dl.id-1, dl.db, dl.nodes, dl.states, dl.buffer, dl.frozen)
|
||||
if dl.generator != nil && !dl.generator.completed() {
|
||||
ndl.generator = dl.generator
|
||||
ndl.generator.run(h.meta.parent)
|
||||
|
|
@ -500,3 +583,41 @@ func (dl *diskLayer) genMarker() []byte {
|
|||
}
|
||||
return dl.generator.progressMarker()
|
||||
}
|
||||
|
||||
// genComplete returns a flag indicating whether the state snapshot has been
|
||||
// fully generated.
|
||||
func (dl *diskLayer) genComplete() bool {
|
||||
dl.lock.RLock()
|
||||
defer dl.lock.RUnlock()
|
||||
|
||||
return dl.genMarker() == nil
|
||||
}
|
||||
|
||||
// waitFlush blocks until the background buffer flush is completed.
|
||||
func (dl *diskLayer) waitFlush() error {
|
||||
dl.lock.RLock()
|
||||
defer dl.lock.RUnlock()
|
||||
|
||||
if dl.frozen == nil {
|
||||
return nil
|
||||
}
|
||||
return dl.frozen.waitFlush()
|
||||
}
|
||||
|
||||
// terminate releases the frozen buffer if it's not nil and terminates the
|
||||
// background state generator.
|
||||
func (dl *diskLayer) terminate() error {
|
||||
dl.lock.Lock()
|
||||
defer dl.lock.Unlock()
|
||||
|
||||
if dl.frozen != nil {
|
||||
if err := dl.frozen.waitFlush(); err != nil {
|
||||
return err
|
||||
}
|
||||
dl.frozen = nil
|
||||
}
|
||||
if dl.generator != nil {
|
||||
dl.generator.stop()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ func generateSnapshot(triedb *Database, root common.Hash, noBuild bool) *diskLay
|
|||
stats = &generatorStats{start: time.Now()}
|
||||
genMarker = []byte{} // Initialized but empty!
|
||||
)
|
||||
dl := newDiskLayer(root, 0, triedb, nil, nil, newBuffer(triedb.config.WriteBufferSize, nil, nil, 0))
|
||||
dl := newDiskLayer(root, 0, triedb, nil, nil, newBuffer(triedb.config.WriteBufferSize, nil, nil, 0), nil)
|
||||
dl.setGenerator(newGenerator(triedb.diskdb, noBuild, genMarker, stats))
|
||||
|
||||
if !noBuild {
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ func newGenTester() *genTester {
|
|||
disk := rawdb.NewMemoryDatabase()
|
||||
config := *Defaults
|
||||
config.SnapshotNoBuild = true // no background generation
|
||||
config.NoAsyncFlush = true // no async flush
|
||||
db := New(disk, &config, false)
|
||||
tr, _ := trie.New(trie.StateTrieID(types.EmptyRootHash), db)
|
||||
return &genTester{
|
||||
|
|
|
|||
|
|
@ -505,25 +505,41 @@ func (h *history) decode(accountData, storageData, accountIndexes, storageIndexe
|
|||
|
||||
// readHistory reads and decodes the state history object by the given id.
|
||||
func readHistory(reader ethdb.AncientReader, id uint64) (*history, error) {
|
||||
blob := rawdb.ReadStateHistoryMeta(reader, id)
|
||||
if len(blob) == 0 {
|
||||
return nil, fmt.Errorf("state history not found %d", id)
|
||||
mData, accountIndexes, storageIndexes, accountData, storageData, err := rawdb.ReadStateHistory(reader, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m meta
|
||||
if err := m.decode(blob); err != nil {
|
||||
if err := m.decode(mData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var (
|
||||
dec = history{meta: &m}
|
||||
accountData = rawdb.ReadStateAccountHistory(reader, id)
|
||||
storageData = rawdb.ReadStateStorageHistory(reader, id)
|
||||
accountIndexes = rawdb.ReadStateAccountIndex(reader, id)
|
||||
storageIndexes = rawdb.ReadStateStorageIndex(reader, id)
|
||||
)
|
||||
if err := dec.decode(accountData, storageData, accountIndexes, storageIndexes); err != nil {
|
||||
h := history{meta: &m}
|
||||
if err := h.decode(accountData, storageData, accountIndexes, storageIndexes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dec, nil
|
||||
return &h, nil
|
||||
}
|
||||
|
||||
// readHistories reads and decodes a list of state histories with the specific
|
||||
// history range.
|
||||
func readHistories(freezer ethdb.AncientReader, start uint64, count uint64) ([]*history, error) {
|
||||
var histories []*history
|
||||
metaList, aIndexList, sIndexList, aDataList, sDataList, err := rawdb.ReadStateHistoryList(freezer, start, count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := 0; i < len(metaList); i++ {
|
||||
var m meta
|
||||
if err := m.decode(metaList[i]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h := history{meta: &m}
|
||||
if err := h.decode(aDataList[i], sDataList[i], aIndexList[i], sIndexList[i]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
histories = append(histories, &h)
|
||||
}
|
||||
return histories, nil
|
||||
}
|
||||
|
||||
// writeHistory persists the state history with the provided state set.
|
||||
|
|
|
|||
436
triedb/pathdb/history_index.go
Normal file
436
triedb/pathdb/history_index.go
Normal file
|
|
@ -0,0 +1,436 @@
|
|||
// Copyright 2025 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 <http://www.gnu.org/licenses/
|
||||
|
||||
package pathdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
)
|
||||
|
||||
// parseIndex parses the index data with the supplied byte stream. The index data
|
||||
// is a list of fixed-sized metadata. Empty metadata is regarded as invalid.
|
||||
func parseIndex(blob []byte) ([]*indexBlockDesc, error) {
|
||||
if len(blob) == 0 {
|
||||
return nil, errors.New("empty state history index")
|
||||
}
|
||||
if len(blob)%indexBlockDescSize != 0 {
|
||||
return nil, fmt.Errorf("corrupted state index, len: %d", len(blob))
|
||||
}
|
||||
var (
|
||||
lastID uint32
|
||||
descList []*indexBlockDesc
|
||||
)
|
||||
for i := 0; i < len(blob)/indexBlockDescSize; i++ {
|
||||
var desc indexBlockDesc
|
||||
desc.decode(blob[i*indexBlockDescSize : (i+1)*indexBlockDescSize])
|
||||
if desc.empty() {
|
||||
return nil, errors.New("empty state history index block")
|
||||
}
|
||||
if lastID != 0 {
|
||||
if lastID+1 != desc.id {
|
||||
return nil, fmt.Errorf("index block id is out of order, last-id: %d, this-id: %d", lastID, desc.id)
|
||||
}
|
||||
// Theoretically, order should be validated between consecutive index blocks,
|
||||
// ensuring that elements within them are strictly ordered. However, since
|
||||
// tracking the minimum element in each block has non-trivial storage overhead,
|
||||
// this check is optimistically omitted.
|
||||
//
|
||||
// TODO(rjl493456442) the minimal element can be resolved from the index block,
|
||||
// evaluate the check cost (mostly IO overhead).
|
||||
|
||||
/* if desc.min <= lastMax {
|
||||
return nil, fmt.Errorf("index block range is out of order, last-max: %d, this-min: %d", lastMax, desc.min)
|
||||
}*/
|
||||
}
|
||||
lastID = desc.id
|
||||
descList = append(descList, &desc)
|
||||
}
|
||||
return descList, nil
|
||||
}
|
||||
|
||||
// indexReader is the structure to look up the state history index records
|
||||
// associated with the specific state element.
|
||||
type indexReader struct {
|
||||
db ethdb.KeyValueReader
|
||||
descList []*indexBlockDesc
|
||||
readers map[uint32]*blockReader
|
||||
state stateIdent
|
||||
}
|
||||
|
||||
// loadIndexData loads the index data associated with the specified state.
|
||||
func loadIndexData(db ethdb.KeyValueReader, state stateIdent) ([]*indexBlockDesc, error) {
|
||||
var blob []byte
|
||||
if state.account {
|
||||
blob = rawdb.ReadAccountHistoryIndex(db, state.addressHash)
|
||||
} else {
|
||||
blob = rawdb.ReadStorageHistoryIndex(db, state.addressHash, state.storageHash)
|
||||
}
|
||||
if len(blob) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return parseIndex(blob)
|
||||
}
|
||||
|
||||
// newIndexReader constructs a index reader for the specified state. Reader with
|
||||
// empty data is allowed.
|
||||
func newIndexReader(db ethdb.KeyValueReader, state stateIdent) (*indexReader, error) {
|
||||
descList, err := loadIndexData(db, state)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &indexReader{
|
||||
descList: descList,
|
||||
readers: make(map[uint32]*blockReader),
|
||||
db: db,
|
||||
state: state,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// refresh reloads the last section of index data to account for any additional
|
||||
// elements that may have been written to disk.
|
||||
func (r *indexReader) refresh() error {
|
||||
// Release the reader for the last section of index data, as its content
|
||||
// may have been modified by additional elements written to the disk.
|
||||
if len(r.descList) != 0 {
|
||||
last := r.descList[len(r.descList)-1]
|
||||
if !last.full() {
|
||||
delete(r.readers, last.id)
|
||||
}
|
||||
}
|
||||
descList, err := loadIndexData(r.db, r.state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.descList = descList
|
||||
return nil
|
||||
}
|
||||
|
||||
// readGreaterThan locates the first element that is greater than the specified
|
||||
// id. If no such element is found, MaxUint64 is returned.
|
||||
func (r *indexReader) readGreaterThan(id uint64) (uint64, error) {
|
||||
index := sort.Search(len(r.descList), func(i int) bool {
|
||||
return id < r.descList[i].max
|
||||
})
|
||||
if index == len(r.descList) {
|
||||
return math.MaxUint64, nil
|
||||
}
|
||||
desc := r.descList[index]
|
||||
|
||||
br, ok := r.readers[desc.id]
|
||||
if !ok {
|
||||
var (
|
||||
err error
|
||||
blob []byte
|
||||
)
|
||||
if r.state.account {
|
||||
blob = rawdb.ReadAccountHistoryIndexBlock(r.db, r.state.addressHash, desc.id)
|
||||
} else {
|
||||
blob = rawdb.ReadStorageHistoryIndexBlock(r.db, r.state.addressHash, r.state.storageHash, desc.id)
|
||||
}
|
||||
br, err = newBlockReader(blob)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
r.readers[desc.id] = br
|
||||
}
|
||||
// The supplied ID is not greater than block.max, ensuring that an element
|
||||
// satisfying the condition can be found.
|
||||
return br.readGreaterThan(id)
|
||||
}
|
||||
|
||||
// indexWriter is responsible for writing index data for a specific state (either
|
||||
// an account or a storage slot). The state index follows a two-layer structure:
|
||||
// the first layer consists of a list of fixed-size metadata, each linked to a
|
||||
// second-layer block. The index data (monotonically increasing list of state
|
||||
// history ids) is stored in these second-layer index blocks, which are size
|
||||
// limited.
|
||||
type indexWriter struct {
|
||||
descList []*indexBlockDesc // The list of index block descriptions
|
||||
bw *blockWriter // The live index block writer
|
||||
frozen []*blockWriter // The finalized index block writers, waiting for flush
|
||||
lastID uint64 // The ID of the latest tracked history
|
||||
state stateIdent
|
||||
db ethdb.KeyValueReader
|
||||
}
|
||||
|
||||
// newIndexWriter constructs the index writer for the specified state.
|
||||
func newIndexWriter(db ethdb.KeyValueReader, state stateIdent) (*indexWriter, error) {
|
||||
var blob []byte
|
||||
if state.account {
|
||||
blob = rawdb.ReadAccountHistoryIndex(db, state.addressHash)
|
||||
} else {
|
||||
blob = rawdb.ReadStorageHistoryIndex(db, state.addressHash, state.storageHash)
|
||||
}
|
||||
if len(blob) == 0 {
|
||||
desc := newIndexBlockDesc(0)
|
||||
bw, _ := newBlockWriter(nil, desc)
|
||||
return &indexWriter{
|
||||
descList: []*indexBlockDesc{desc},
|
||||
bw: bw,
|
||||
state: state,
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
descList, err := parseIndex(blob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var (
|
||||
indexBlock []byte
|
||||
lastDesc = descList[len(descList)-1]
|
||||
)
|
||||
if state.account {
|
||||
indexBlock = rawdb.ReadAccountHistoryIndexBlock(db, state.addressHash, lastDesc.id)
|
||||
} else {
|
||||
indexBlock = rawdb.ReadStorageHistoryIndexBlock(db, state.addressHash, state.storageHash, lastDesc.id)
|
||||
}
|
||||
bw, err := newBlockWriter(indexBlock, lastDesc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &indexWriter{
|
||||
descList: descList,
|
||||
lastID: lastDesc.max,
|
||||
bw: bw,
|
||||
state: state,
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// append adds the new element into the index writer.
|
||||
func (w *indexWriter) append(id uint64) error {
|
||||
if id <= w.lastID {
|
||||
return fmt.Errorf("append element out of order, last: %d, this: %d", w.lastID, id)
|
||||
}
|
||||
if w.bw.full() {
|
||||
if err := w.rotate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := w.bw.append(id); err != nil {
|
||||
return err
|
||||
}
|
||||
w.lastID = id
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// rotate creates a new index block for storing index records from scratch
|
||||
// and caches the current full index block for finalization.
|
||||
func (w *indexWriter) rotate() error {
|
||||
var (
|
||||
err error
|
||||
desc = newIndexBlockDesc(w.bw.desc.id + 1)
|
||||
)
|
||||
w.frozen = append(w.frozen, w.bw)
|
||||
w.bw, err = newBlockWriter(nil, desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.descList = append(w.descList, desc)
|
||||
return nil
|
||||
}
|
||||
|
||||
// finish finalizes all the frozen index block writers along with the live one
|
||||
// if it's not empty, committing the index block data and the index meta into
|
||||
// the supplied batch.
|
||||
//
|
||||
// This function is safe to be called multiple times.
|
||||
func (w *indexWriter) finish(batch ethdb.Batch) {
|
||||
var (
|
||||
writers = append(w.frozen, w.bw)
|
||||
descList = w.descList
|
||||
)
|
||||
// The live index block writer might be empty if the entire index write
|
||||
// is created from scratch, remove it from committing.
|
||||
if w.bw.empty() {
|
||||
writers = writers[:len(writers)-1]
|
||||
descList = descList[:len(descList)-1]
|
||||
}
|
||||
if len(writers) == 0 {
|
||||
return // nothing to commit
|
||||
}
|
||||
for _, bw := range writers {
|
||||
if w.state.account {
|
||||
rawdb.WriteAccountHistoryIndexBlock(batch, w.state.addressHash, bw.desc.id, bw.finish())
|
||||
} else {
|
||||
rawdb.WriteStorageHistoryIndexBlock(batch, w.state.addressHash, w.state.storageHash, bw.desc.id, bw.finish())
|
||||
}
|
||||
}
|
||||
w.frozen = nil // release all the frozen writers
|
||||
|
||||
buf := make([]byte, 0, indexBlockDescSize*len(descList))
|
||||
for _, desc := range descList {
|
||||
buf = append(buf, desc.encode()...)
|
||||
}
|
||||
if w.state.account {
|
||||
rawdb.WriteAccountHistoryIndex(batch, w.state.addressHash, buf)
|
||||
} else {
|
||||
rawdb.WriteStorageHistoryIndex(batch, w.state.addressHash, w.state.storageHash, buf)
|
||||
}
|
||||
}
|
||||
|
||||
// indexDeleter is responsible for deleting index data for a specific state.
|
||||
type indexDeleter struct {
|
||||
descList []*indexBlockDesc // The list of index block descriptions
|
||||
bw *blockWriter // The live index block writer
|
||||
dropped []uint32 // The list of index block id waiting for deleting
|
||||
lastID uint64 // The ID of the latest tracked history
|
||||
state stateIdent
|
||||
db ethdb.KeyValueReader
|
||||
}
|
||||
|
||||
// newIndexDeleter constructs the index deleter for the specified state.
|
||||
func newIndexDeleter(db ethdb.KeyValueReader, state stateIdent) (*indexDeleter, error) {
|
||||
var blob []byte
|
||||
if state.account {
|
||||
blob = rawdb.ReadAccountHistoryIndex(db, state.addressHash)
|
||||
} else {
|
||||
blob = rawdb.ReadStorageHistoryIndex(db, state.addressHash, state.storageHash)
|
||||
}
|
||||
if len(blob) == 0 {
|
||||
// TODO(rjl493456442) we can probably return an error here,
|
||||
// deleter with no data is meaningless.
|
||||
desc := newIndexBlockDesc(0)
|
||||
bw, _ := newBlockWriter(nil, desc)
|
||||
return &indexDeleter{
|
||||
descList: []*indexBlockDesc{desc},
|
||||
bw: bw,
|
||||
state: state,
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
descList, err := parseIndex(blob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var (
|
||||
indexBlock []byte
|
||||
lastDesc = descList[len(descList)-1]
|
||||
)
|
||||
if state.account {
|
||||
indexBlock = rawdb.ReadAccountHistoryIndexBlock(db, state.addressHash, lastDesc.id)
|
||||
} else {
|
||||
indexBlock = rawdb.ReadStorageHistoryIndexBlock(db, state.addressHash, state.storageHash, lastDesc.id)
|
||||
}
|
||||
bw, err := newBlockWriter(indexBlock, lastDesc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &indexDeleter{
|
||||
descList: descList,
|
||||
lastID: lastDesc.max,
|
||||
bw: bw,
|
||||
state: state,
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// empty returns an flag indicating whether the state index is empty.
|
||||
func (d *indexDeleter) empty() bool {
|
||||
return d.bw.empty() && len(d.descList) == 1
|
||||
}
|
||||
|
||||
// pop removes the last written element from the index writer.
|
||||
func (d *indexDeleter) pop(id uint64) error {
|
||||
if id == 0 {
|
||||
return fmt.Errorf("zero history ID is not valid")
|
||||
}
|
||||
if id != d.lastID {
|
||||
return fmt.Errorf("pop element out of order, last: %d, this: %d", d.lastID, id)
|
||||
}
|
||||
if err := d.bw.pop(id); err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.bw.empty() {
|
||||
d.lastID = d.bw.desc.max
|
||||
return nil
|
||||
}
|
||||
// Discarding the last block writer if it becomes empty by popping an element
|
||||
d.dropped = append(d.dropped, d.descList[len(d.descList)-1].id)
|
||||
|
||||
// Reset the entire index writer if it becomes empty after popping an element
|
||||
if d.empty() {
|
||||
d.lastID = 0
|
||||
return nil
|
||||
}
|
||||
d.descList = d.descList[:len(d.descList)-1]
|
||||
|
||||
// Open the previous block writer for deleting
|
||||
var (
|
||||
indexBlock []byte
|
||||
lastDesc = d.descList[len(d.descList)-1]
|
||||
)
|
||||
if d.state.account {
|
||||
indexBlock = rawdb.ReadAccountHistoryIndexBlock(d.db, d.state.addressHash, lastDesc.id)
|
||||
} else {
|
||||
indexBlock = rawdb.ReadStorageHistoryIndexBlock(d.db, d.state.addressHash, d.state.storageHash, lastDesc.id)
|
||||
}
|
||||
bw, err := newBlockWriter(indexBlock, lastDesc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.bw = bw
|
||||
d.lastID = bw.desc.max
|
||||
return nil
|
||||
}
|
||||
|
||||
// finish deletes the empty index blocks and updates the index meta.
|
||||
//
|
||||
// This function is safe to be called multiple times.
|
||||
func (d *indexDeleter) finish(batch ethdb.Batch) {
|
||||
for _, id := range d.dropped {
|
||||
if d.state.account {
|
||||
rawdb.DeleteAccountHistoryIndexBlock(batch, d.state.addressHash, id)
|
||||
} else {
|
||||
rawdb.DeleteStorageHistoryIndexBlock(batch, d.state.addressHash, d.state.storageHash, id)
|
||||
}
|
||||
}
|
||||
d.dropped = nil
|
||||
|
||||
// Flush the content of last block writer, regardless it's dirty or not
|
||||
if !d.bw.empty() {
|
||||
if d.state.account {
|
||||
rawdb.WriteAccountHistoryIndexBlock(batch, d.state.addressHash, d.bw.desc.id, d.bw.finish())
|
||||
} else {
|
||||
rawdb.WriteStorageHistoryIndexBlock(batch, d.state.addressHash, d.state.storageHash, d.bw.desc.id, d.bw.finish())
|
||||
}
|
||||
}
|
||||
// Flush the index metadata into the supplied batch
|
||||
if d.empty() {
|
||||
if d.state.account {
|
||||
rawdb.DeleteAccountHistoryIndex(batch, d.state.addressHash)
|
||||
} else {
|
||||
rawdb.DeleteStorageHistoryIndex(batch, d.state.addressHash, d.state.storageHash)
|
||||
}
|
||||
} else {
|
||||
buf := make([]byte, 0, indexBlockDescSize*len(d.descList))
|
||||
for _, desc := range d.descList {
|
||||
buf = append(buf, desc.encode()...)
|
||||
}
|
||||
if d.state.account {
|
||||
rawdb.WriteAccountHistoryIndex(batch, d.state.addressHash, buf)
|
||||
} else {
|
||||
rawdb.WriteStorageHistoryIndex(batch, d.state.addressHash, d.state.storageHash, buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
405
triedb/pathdb/history_index_block.go
Normal file
405
triedb/pathdb/history_index_block.go
Normal file
|
|
@ -0,0 +1,405 @@
|
|||
// Copyright 2025 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 <http://www.gnu.org/licenses/
|
||||
|
||||
package pathdb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
)
|
||||
|
||||
const (
|
||||
indexBlockDescSize = 14 // The size of index block descriptor
|
||||
indexBlockEntriesCap = 4096 // The maximum number of entries can be grouped in a block
|
||||
indexBlockRestartLen = 256 // The restart interval length of index block
|
||||
historyIndexBatch = 1_000_000 // The number of state history indexes for constructing or deleting as batch
|
||||
)
|
||||
|
||||
// indexBlockDesc represents a descriptor for an index block, which contains a
|
||||
// list of state mutation records associated with a specific state (either an
|
||||
// account or a storage slot).
|
||||
type indexBlockDesc struct {
|
||||
max uint64 // The maximum state ID retained within the block
|
||||
entries uint16 // The number of state mutation records retained within the block
|
||||
id uint32 // The id of the index block
|
||||
}
|
||||
|
||||
func newIndexBlockDesc(id uint32) *indexBlockDesc {
|
||||
return &indexBlockDesc{id: id}
|
||||
}
|
||||
|
||||
// empty indicates whether the block is empty with no element retained.
|
||||
func (d *indexBlockDesc) empty() bool {
|
||||
return d.entries == 0
|
||||
}
|
||||
|
||||
// full indicates whether the number of elements in the block exceeds the
|
||||
// preconfigured limit.
|
||||
func (d *indexBlockDesc) full() bool {
|
||||
return d.entries >= indexBlockEntriesCap
|
||||
}
|
||||
|
||||
// encode packs index block descriptor into byte stream.
|
||||
func (d *indexBlockDesc) encode() []byte {
|
||||
var buf [indexBlockDescSize]byte
|
||||
binary.BigEndian.PutUint64(buf[0:8], d.max)
|
||||
binary.BigEndian.PutUint16(buf[8:10], d.entries)
|
||||
binary.BigEndian.PutUint32(buf[10:14], d.id)
|
||||
return buf[:]
|
||||
}
|
||||
|
||||
// decode unpacks index block descriptor from byte stream.
|
||||
func (d *indexBlockDesc) decode(blob []byte) {
|
||||
d.max = binary.BigEndian.Uint64(blob[:8])
|
||||
d.entries = binary.BigEndian.Uint16(blob[8:10])
|
||||
d.id = binary.BigEndian.Uint32(blob[10:14])
|
||||
}
|
||||
|
||||
// parseIndexBlock parses the index block with the supplied byte stream.
|
||||
// The index block format can be illustrated as below:
|
||||
//
|
||||
// +---->+------------------+
|
||||
// | | Chunk1 |
|
||||
// | +------------------+
|
||||
// | | ...... |
|
||||
// | +-->+------------------+
|
||||
// | | | ChunkN |
|
||||
// | | +------------------+
|
||||
// +-|---| Restart1 |
|
||||
// | | Restart... | 2N bytes
|
||||
// +---| RestartN |
|
||||
// +------------------+
|
||||
// | Restart count | 1 byte
|
||||
// +------------------+
|
||||
//
|
||||
// - Chunk list: A list of data chunks
|
||||
// - Restart list: A list of 2-byte pointers, each pointing to the start position of a chunk
|
||||
// - Restart count: The number of restarts in the block, stored at the end of the block (1 byte)
|
||||
//
|
||||
// Note: the pointer is encoded as a uint16, which is sufficient within a chunk.
|
||||
// A uint16 can cover offsets in the range [0, 65536), which is more than enough
|
||||
// to store 4096 integers.
|
||||
//
|
||||
// Each chunk begins with the full value of the first integer, followed by
|
||||
// subsequent integers representing the differences between the current value
|
||||
// and the preceding one. Integers are encoded with variable-size for best
|
||||
// storage efficiency. Each chunk can be illustrated as below.
|
||||
//
|
||||
// Restart ---> +----------------+
|
||||
// | Full integer |
|
||||
// +----------------+
|
||||
// | Diff with prev |
|
||||
// +----------------+
|
||||
// | ... |
|
||||
// +----------------+
|
||||
// | Diff with prev |
|
||||
// +----------------+
|
||||
//
|
||||
// Empty index block is regarded as invalid.
|
||||
func parseIndexBlock(blob []byte) ([]uint16, []byte, error) {
|
||||
if len(blob) < 1 {
|
||||
return nil, nil, fmt.Errorf("corrupted index block, len: %d", len(blob))
|
||||
}
|
||||
restartLen := blob[len(blob)-1]
|
||||
if restartLen == 0 {
|
||||
return nil, nil, errors.New("corrupted index block, no restart")
|
||||
}
|
||||
tailLen := int(restartLen)*2 + 1
|
||||
if len(blob) < tailLen {
|
||||
return nil, nil, fmt.Errorf("truncated restarts, size: %d, restarts: %d", len(blob), restartLen)
|
||||
}
|
||||
restarts := make([]uint16, 0, restartLen)
|
||||
for i := int(restartLen); i > 0; i-- {
|
||||
restart := binary.BigEndian.Uint16(blob[len(blob)-1-2*i:])
|
||||
restarts = append(restarts, restart)
|
||||
}
|
||||
// Validate that restart points are strictly ordered and within the valid
|
||||
// data range.
|
||||
var prev uint16
|
||||
for i := 0; i < len(restarts); i++ {
|
||||
if i != 0 {
|
||||
if restarts[i] <= prev {
|
||||
return nil, nil, fmt.Errorf("restart out of order, prev: %d, next: %d", prev, restarts[i])
|
||||
}
|
||||
}
|
||||
if int(restarts[i]) >= len(blob)-tailLen {
|
||||
return nil, nil, fmt.Errorf("invalid restart position, restart: %d, size: %d", restarts[i], len(blob)-tailLen)
|
||||
}
|
||||
prev = restarts[i]
|
||||
}
|
||||
return restarts, blob[:len(blob)-tailLen], nil
|
||||
}
|
||||
|
||||
// blockReader is the reader to access the element within a block.
|
||||
type blockReader struct {
|
||||
restarts []uint16
|
||||
data []byte
|
||||
}
|
||||
|
||||
// newBlockReader constructs the block reader with the supplied block data.
|
||||
func newBlockReader(blob []byte) (*blockReader, error) {
|
||||
restarts, data, err := parseIndexBlock(blob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &blockReader{
|
||||
restarts: restarts,
|
||||
data: data, // safe to own the slice
|
||||
}, nil
|
||||
}
|
||||
|
||||
// readGreaterThan locates the first element in the block that is greater than
|
||||
// the specified value. If no such element is found, MaxUint64 is returned.
|
||||
func (br *blockReader) readGreaterThan(id uint64) (uint64, error) {
|
||||
var err error
|
||||
index := sort.Search(len(br.restarts), func(i int) bool {
|
||||
item, n := binary.Uvarint(br.data[br.restarts[i]:])
|
||||
if n <= 0 {
|
||||
err = fmt.Errorf("failed to decode item at restart %d", br.restarts[i])
|
||||
}
|
||||
return item > id
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if index == 0 {
|
||||
item, _ := binary.Uvarint(br.data[br.restarts[0]:])
|
||||
return item, nil
|
||||
}
|
||||
var (
|
||||
start int
|
||||
limit int
|
||||
result uint64
|
||||
)
|
||||
if index == len(br.restarts) {
|
||||
// The element being searched falls within the last restart section,
|
||||
// there is no guarantee such element can be found.
|
||||
start = int(br.restarts[len(br.restarts)-1])
|
||||
limit = len(br.data)
|
||||
} else {
|
||||
// The element being searched falls within the non-last restart section,
|
||||
// such element can be found for sure.
|
||||
start = int(br.restarts[index-1])
|
||||
limit = int(br.restarts[index])
|
||||
}
|
||||
pos := start
|
||||
for pos < limit {
|
||||
x, n := binary.Uvarint(br.data[pos:])
|
||||
if pos == start {
|
||||
result = x
|
||||
} else {
|
||||
result += x
|
||||
}
|
||||
if result > id {
|
||||
return result, nil
|
||||
}
|
||||
pos += n
|
||||
}
|
||||
// The element which is greater than specified id is not found.
|
||||
if index == len(br.restarts) {
|
||||
return math.MaxUint64, nil
|
||||
}
|
||||
// The element which is the first one greater than the specified id
|
||||
// is exactly the one located at the restart point.
|
||||
item, _ := binary.Uvarint(br.data[br.restarts[index]:])
|
||||
return item, nil
|
||||
}
|
||||
|
||||
type blockWriter struct {
|
||||
desc *indexBlockDesc // Descriptor of the block
|
||||
restarts []uint16 // Offsets into the data slice, marking the start of each section
|
||||
scratch []byte // Buffer used for encoding full integers or value differences
|
||||
data []byte // Aggregated encoded data slice
|
||||
}
|
||||
|
||||
func newBlockWriter(blob []byte, desc *indexBlockDesc) (*blockWriter, error) {
|
||||
scratch := make([]byte, binary.MaxVarintLen64)
|
||||
if len(blob) == 0 {
|
||||
return &blockWriter{
|
||||
desc: desc,
|
||||
scratch: scratch,
|
||||
data: make([]byte, 0, 1024),
|
||||
}, nil
|
||||
}
|
||||
restarts, data, err := parseIndexBlock(blob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &blockWriter{
|
||||
desc: desc,
|
||||
restarts: restarts,
|
||||
scratch: scratch,
|
||||
data: data, // safe to own the slice
|
||||
}, nil
|
||||
}
|
||||
|
||||
// append adds a new element to the block. The new element must be greater than
|
||||
// the previous one. The provided ID is assumed to always be greater than 0.
|
||||
func (b *blockWriter) append(id uint64) error {
|
||||
if id == 0 {
|
||||
return errors.New("invalid zero id")
|
||||
}
|
||||
if id <= b.desc.max {
|
||||
return fmt.Errorf("append element out of order, last: %d, this: %d", b.desc.max, id)
|
||||
}
|
||||
// Rotate the current restart section if it's full
|
||||
if b.desc.entries%indexBlockRestartLen == 0 {
|
||||
// Save the offset within the data slice as the restart point
|
||||
// for the next section.
|
||||
b.restarts = append(b.restarts, uint16(len(b.data)))
|
||||
|
||||
// The restart point item can either be encoded in variable
|
||||
// size or fixed size. Although variable-size encoding is
|
||||
// slightly slower (2ns per operation), it is still relatively
|
||||
// fast, therefore, it's picked for better space efficiency.
|
||||
//
|
||||
// The first element in a restart range is encoded using its
|
||||
// full value.
|
||||
n := binary.PutUvarint(b.scratch[0:], id)
|
||||
b.data = append(b.data, b.scratch[:n]...)
|
||||
} else {
|
||||
// The current section is not full, append the element.
|
||||
// The element which is not the first one in the section
|
||||
// is encoded using the value difference from the preceding
|
||||
// element.
|
||||
n := binary.PutUvarint(b.scratch[0:], id-b.desc.max)
|
||||
b.data = append(b.data, b.scratch[:n]...)
|
||||
}
|
||||
b.desc.entries++
|
||||
|
||||
// The state history ID must be greater than 0.
|
||||
//if b.desc.min == 0 {
|
||||
// b.desc.min = id
|
||||
//}
|
||||
b.desc.max = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanSection traverses the specified section and terminates if fn returns true.
|
||||
func (b *blockWriter) scanSection(section int, fn func(uint64, int) bool) {
|
||||
var (
|
||||
value uint64
|
||||
start = int(b.restarts[section])
|
||||
pos = start
|
||||
limit int
|
||||
)
|
||||
if section == len(b.restarts)-1 {
|
||||
limit = len(b.data)
|
||||
} else {
|
||||
limit = int(b.restarts[section+1])
|
||||
}
|
||||
for pos < limit {
|
||||
x, n := binary.Uvarint(b.data[pos:])
|
||||
if pos == start {
|
||||
value = x
|
||||
} else {
|
||||
value += x
|
||||
}
|
||||
if fn(value, pos) {
|
||||
return
|
||||
}
|
||||
pos += n
|
||||
}
|
||||
}
|
||||
|
||||
// sectionLast returns the last element in the specified section.
|
||||
func (b *blockWriter) sectionLast(section int) uint64 {
|
||||
var n uint64
|
||||
b.scanSection(section, func(v uint64, _ int) bool {
|
||||
n = v
|
||||
return false
|
||||
})
|
||||
return n
|
||||
}
|
||||
|
||||
// sectionSearch looks up the specified value in the given section,
|
||||
// the position and the preceding value will be returned if found.
|
||||
func (b *blockWriter) sectionSearch(section int, n uint64) (found bool, prev uint64, pos int) {
|
||||
b.scanSection(section, func(v uint64, p int) bool {
|
||||
if n == v {
|
||||
pos = p
|
||||
found = true
|
||||
return true // terminate iteration
|
||||
}
|
||||
prev = v
|
||||
return false // continue iteration
|
||||
})
|
||||
return found, prev, pos
|
||||
}
|
||||
|
||||
// pop removes the last element from the block. The assumption is held that block
|
||||
// writer must be non-empty.
|
||||
func (b *blockWriter) pop(id uint64) error {
|
||||
if id == 0 {
|
||||
return errors.New("invalid zero id")
|
||||
}
|
||||
if id != b.desc.max {
|
||||
return fmt.Errorf("pop element out of order, last: %d, this: %d", b.desc.max, id)
|
||||
}
|
||||
// If there is only one entry left, the entire block should be reset
|
||||
if b.desc.entries == 1 {
|
||||
//b.desc.min = 0
|
||||
b.desc.max = 0
|
||||
b.desc.entries = 0
|
||||
b.restarts = nil
|
||||
b.data = b.data[:0]
|
||||
return nil
|
||||
}
|
||||
// Pop the last restart section if the section becomes empty after removing
|
||||
// one element.
|
||||
if b.desc.entries%indexBlockRestartLen == 1 {
|
||||
b.data = b.data[:b.restarts[len(b.restarts)-1]]
|
||||
b.restarts = b.restarts[:len(b.restarts)-1]
|
||||
b.desc.max = b.sectionLast(len(b.restarts) - 1)
|
||||
b.desc.entries -= 1
|
||||
return nil
|
||||
}
|
||||
// Look up the element preceding the one to be popped, in order to update
|
||||
// the maximum element in the block.
|
||||
found, prev, pos := b.sectionSearch(len(b.restarts)-1, id)
|
||||
if !found {
|
||||
return fmt.Errorf("pop element is not found, last: %d, this: %d", b.desc.max, id)
|
||||
}
|
||||
b.desc.max = prev
|
||||
b.data = b.data[:pos]
|
||||
b.desc.entries -= 1
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *blockWriter) empty() bool {
|
||||
return b.desc.empty()
|
||||
}
|
||||
|
||||
func (b *blockWriter) full() bool {
|
||||
return b.desc.full()
|
||||
}
|
||||
|
||||
// finish finalizes the index block encoding by appending the encoded restart points
|
||||
// and the restart counter to the end of the block.
|
||||
//
|
||||
// This function is safe to be called multiple times.
|
||||
func (b *blockWriter) finish() []byte {
|
||||
var buf []byte
|
||||
for _, number := range b.restarts {
|
||||
binary.BigEndian.PutUint16(b.scratch[:2], number)
|
||||
buf = append(buf, b.scratch[:2]...)
|
||||
}
|
||||
buf = append(buf, byte(len(b.restarts)))
|
||||
return append(b.data, buf...)
|
||||
}
|
||||
216
triedb/pathdb/history_index_block_test.go
Normal file
216
triedb/pathdb/history_index_block_test.go
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
// Copyright 2025 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 <http://www.gnu.org/licenses/
|
||||
|
||||
package pathdb
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/rand"
|
||||
"slices"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBlockReaderBasic(t *testing.T) {
|
||||
elements := []uint64{
|
||||
1, 5, 10, 11, 20,
|
||||
}
|
||||
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0))
|
||||
for i := 0; i < len(elements); i++ {
|
||||
bw.append(elements[i])
|
||||
}
|
||||
|
||||
br, err := newBlockReader(bw.finish())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct the block reader, %v", err)
|
||||
}
|
||||
cases := []struct {
|
||||
value uint64
|
||||
result uint64
|
||||
}{
|
||||
{0, 1},
|
||||
{1, 5},
|
||||
{10, 11},
|
||||
{19, 20},
|
||||
{20, math.MaxUint64},
|
||||
{21, math.MaxUint64},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got, err := br.readGreaterThan(c.value)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error, got %v", err)
|
||||
}
|
||||
if got != c.result {
|
||||
t.Fatalf("Unexpected result, got %v, wanted %v", got, c.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockReaderLarge(t *testing.T) {
|
||||
var elements []uint64
|
||||
for i := 0; i < 1000; i++ {
|
||||
elements = append(elements, rand.Uint64())
|
||||
}
|
||||
slices.Sort(elements)
|
||||
|
||||
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0))
|
||||
for i := 0; i < len(elements); i++ {
|
||||
bw.append(elements[i])
|
||||
}
|
||||
|
||||
br, err := newBlockReader(bw.finish())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct the block reader, %v", err)
|
||||
}
|
||||
for i := 0; i < 100; i++ {
|
||||
value := rand.Uint64()
|
||||
pos := sort.Search(len(elements), func(i int) bool {
|
||||
return elements[i] > value
|
||||
})
|
||||
got, err := br.readGreaterThan(value)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error, got %v", err)
|
||||
}
|
||||
if pos == len(elements) {
|
||||
if got != math.MaxUint64 {
|
||||
t.Fatalf("Unexpected result, got %d, wanted math.MaxUint64", got)
|
||||
}
|
||||
} else if got != elements[pos] {
|
||||
t.Fatalf("Unexpected result, got %d, wanted %d", got, elements[pos])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockWriterBasic(t *testing.T) {
|
||||
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0))
|
||||
if !bw.empty() {
|
||||
t.Fatal("expected empty block")
|
||||
}
|
||||
bw.append(2)
|
||||
if err := bw.append(1); err == nil {
|
||||
t.Fatal("out-of-order insertion is not expected")
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
bw.append(uint64(i + 3))
|
||||
}
|
||||
|
||||
bw, err := newBlockWriter(bw.finish(), newIndexBlockDesc(0))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct the block writer, %v", err)
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
if err := bw.append(uint64(i + 100)); err != nil {
|
||||
t.Fatalf("Failed to append value %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
bw.finish()
|
||||
}
|
||||
|
||||
func TestBlockWriterDelete(t *testing.T) {
|
||||
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0))
|
||||
for i := 0; i < 10; i++ {
|
||||
bw.append(uint64(i + 1))
|
||||
}
|
||||
// Pop unknown id, the request should be rejected
|
||||
if err := bw.pop(100); err == nil {
|
||||
t.Fatal("Expect error to occur for unknown id")
|
||||
}
|
||||
for i := 10; i >= 1; i-- {
|
||||
if err := bw.pop(uint64(i)); err != nil {
|
||||
t.Fatalf("Unexpected error for element popping, %v", err)
|
||||
}
|
||||
empty := i == 1
|
||||
if empty != bw.empty() {
|
||||
t.Fatalf("Emptiness is not matched, want: %T, got: %T", empty, bw.empty())
|
||||
}
|
||||
newMax := uint64(i - 1)
|
||||
if bw.desc.max != newMax {
|
||||
t.Fatalf("Maxmium element is not matched, want: %d, got: %d", newMax, bw.desc.max)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlcokWriterDeleteWithData(t *testing.T) {
|
||||
elements := []uint64{
|
||||
1, 5, 10, 11, 20,
|
||||
}
|
||||
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0))
|
||||
for i := 0; i < len(elements); i++ {
|
||||
bw.append(elements[i])
|
||||
}
|
||||
|
||||
// Re-construct the block writer with data
|
||||
desc := &indexBlockDesc{
|
||||
id: 0,
|
||||
max: 20,
|
||||
entries: 5,
|
||||
}
|
||||
bw, err := newBlockWriter(bw.finish(), desc)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct block writer %v", err)
|
||||
}
|
||||
for i := len(elements) - 1; i > 0; i-- {
|
||||
if err := bw.pop(elements[i]); err != nil {
|
||||
t.Fatalf("Failed to pop element, %v", err)
|
||||
}
|
||||
newTail := elements[i-1]
|
||||
|
||||
// Ensure the element can still be queried with no issue
|
||||
br, err := newBlockReader(bw.finish())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct the block reader, %v", err)
|
||||
}
|
||||
cases := []struct {
|
||||
value uint64
|
||||
result uint64
|
||||
}{
|
||||
{0, 1},
|
||||
{1, 5},
|
||||
{10, 11},
|
||||
{19, 20},
|
||||
{20, math.MaxUint64},
|
||||
{21, math.MaxUint64},
|
||||
}
|
||||
for _, c := range cases {
|
||||
want := c.result
|
||||
if c.value >= newTail {
|
||||
want = math.MaxUint64
|
||||
}
|
||||
got, err := br.readGreaterThan(c.value)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error, got %v", err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("Unexpected result, got %v, wanted %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorruptedIndexBlock(t *testing.T) {
|
||||
bw, _ := newBlockWriter(nil, newIndexBlockDesc(0))
|
||||
for i := 0; i < 10; i++ {
|
||||
bw.append(uint64(i + 1))
|
||||
}
|
||||
buf := bw.finish()
|
||||
|
||||
// Mutate the buffer manually
|
||||
buf[len(buf)-1]++
|
||||
_, err := newBlockWriter(buf, newIndexBlockDesc(0))
|
||||
if err == nil {
|
||||
t.Fatal("Corrupted index block data is not detected")
|
||||
}
|
||||
}
|
||||
292
triedb/pathdb/history_index_test.go
Normal file
292
triedb/pathdb/history_index_test.go
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
// Copyright 2025 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 <http://www.gnu.org/licenses/
|
||||
|
||||
package pathdb
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/rand"
|
||||
"slices"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
func TestIndexReaderBasic(t *testing.T) {
|
||||
elements := []uint64{
|
||||
1, 5, 10, 11, 20,
|
||||
}
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
bw, _ := newIndexWriter(db, newAccountIdent(common.Hash{0xa}))
|
||||
for i := 0; i < len(elements); i++ {
|
||||
bw.append(elements[i])
|
||||
}
|
||||
batch := db.NewBatch()
|
||||
bw.finish(batch)
|
||||
batch.Write()
|
||||
|
||||
br, err := newIndexReader(db, newAccountIdent(common.Hash{0xa}))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct the index reader, %v", err)
|
||||
}
|
||||
cases := []struct {
|
||||
value uint64
|
||||
result uint64
|
||||
}{
|
||||
{0, 1},
|
||||
{1, 5},
|
||||
{10, 11},
|
||||
{19, 20},
|
||||
{20, math.MaxUint64},
|
||||
{21, math.MaxUint64},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got, err := br.readGreaterThan(c.value)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error, got %v", err)
|
||||
}
|
||||
if got != c.result {
|
||||
t.Fatalf("Unexpected result, got %v, wanted %v", got, c.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexReaderLarge(t *testing.T) {
|
||||
var elements []uint64
|
||||
for i := 0; i < 10*indexBlockEntriesCap; i++ {
|
||||
elements = append(elements, rand.Uint64())
|
||||
}
|
||||
slices.Sort(elements)
|
||||
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
bw, _ := newIndexWriter(db, newAccountIdent(common.Hash{0xa}))
|
||||
for i := 0; i < len(elements); i++ {
|
||||
bw.append(elements[i])
|
||||
}
|
||||
batch := db.NewBatch()
|
||||
bw.finish(batch)
|
||||
batch.Write()
|
||||
|
||||
br, err := newIndexReader(db, newAccountIdent(common.Hash{0xa}))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct the index reader, %v", err)
|
||||
}
|
||||
for i := 0; i < 100; i++ {
|
||||
value := rand.Uint64()
|
||||
pos := sort.Search(len(elements), func(i int) bool {
|
||||
return elements[i] > value
|
||||
})
|
||||
got, err := br.readGreaterThan(value)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error, got %v", err)
|
||||
}
|
||||
if pos == len(elements) {
|
||||
if got != math.MaxUint64 {
|
||||
t.Fatalf("Unexpected result, got %d, wanted math.MaxUint64", got)
|
||||
}
|
||||
} else if got != elements[pos] {
|
||||
t.Fatalf("Unexpected result, got %d, wanted %d", got, elements[pos])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyIndexReader(t *testing.T) {
|
||||
br, err := newIndexReader(rawdb.NewMemoryDatabase(), newAccountIdent(common.Hash{0xa}))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct the index reader, %v", err)
|
||||
}
|
||||
res, err := br.readGreaterThan(100)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to query, %v", err)
|
||||
}
|
||||
if res != math.MaxUint64 {
|
||||
t.Fatalf("Unexpected result, got %d, wanted math.MaxUint64", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexWriterBasic(t *testing.T) {
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
iw, _ := newIndexWriter(db, newAccountIdent(common.Hash{0xa}))
|
||||
iw.append(2)
|
||||
if err := iw.append(1); err == nil {
|
||||
t.Fatal("out-of-order insertion is not expected")
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
iw.append(uint64(i + 3))
|
||||
}
|
||||
batch := db.NewBatch()
|
||||
iw.finish(batch)
|
||||
batch.Write()
|
||||
|
||||
iw, err := newIndexWriter(db, newAccountIdent(common.Hash{0xa}))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct the block writer, %v", err)
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
if err := iw.append(uint64(i + 100)); err != nil {
|
||||
t.Fatalf("Failed to append item, %v", err)
|
||||
}
|
||||
}
|
||||
iw.finish(db.NewBatch())
|
||||
}
|
||||
|
||||
func TestIndexWriterDelete(t *testing.T) {
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
iw, _ := newIndexWriter(db, newAccountIdent(common.Hash{0xa}))
|
||||
for i := 0; i < indexBlockEntriesCap*4; i++ {
|
||||
iw.append(uint64(i + 1))
|
||||
}
|
||||
batch := db.NewBatch()
|
||||
iw.finish(batch)
|
||||
batch.Write()
|
||||
|
||||
// Delete unknown id, the request should be rejected
|
||||
id, _ := newIndexDeleter(db, newAccountIdent(common.Hash{0xa}))
|
||||
if err := id.pop(indexBlockEntriesCap * 5); err == nil {
|
||||
t.Fatal("Expect error to occur for unknown id")
|
||||
}
|
||||
for i := indexBlockEntriesCap * 4; i >= 1; i-- {
|
||||
if err := id.pop(uint64(i)); err != nil {
|
||||
t.Fatalf("Unexpected error for element popping, %v", err)
|
||||
}
|
||||
if id.lastID != uint64(i-1) {
|
||||
t.Fatalf("Unexpected lastID, want: %d, got: %d", uint64(i-1), iw.lastID)
|
||||
}
|
||||
if rand.Intn(10) == 0 {
|
||||
batch := db.NewBatch()
|
||||
id.finish(batch)
|
||||
batch.Write()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchIndexerWrite(t *testing.T) {
|
||||
var (
|
||||
db = rawdb.NewMemoryDatabase()
|
||||
batch = newBatchIndexer(db, false)
|
||||
histories = makeHistories(10)
|
||||
)
|
||||
for i, h := range histories {
|
||||
if err := batch.process(h, uint64(i+1)); err != nil {
|
||||
t.Fatalf("Failed to process history, %v", err)
|
||||
}
|
||||
}
|
||||
if err := batch.finish(true); err != nil {
|
||||
t.Fatalf("Failed to finish batch indexer, %v", err)
|
||||
}
|
||||
metadata := loadIndexMetadata(db)
|
||||
if metadata == nil || metadata.Last != uint64(10) {
|
||||
t.Fatal("Unexpected index position")
|
||||
}
|
||||
var (
|
||||
accounts = make(map[common.Hash][]uint64)
|
||||
storages = make(map[common.Hash]map[common.Hash][]uint64)
|
||||
)
|
||||
for i, h := range histories {
|
||||
for _, addr := range h.accountList {
|
||||
addrHash := crypto.Keccak256Hash(addr.Bytes())
|
||||
accounts[addrHash] = append(accounts[addrHash], uint64(i+1))
|
||||
|
||||
if _, ok := storages[addrHash]; !ok {
|
||||
storages[addrHash] = make(map[common.Hash][]uint64)
|
||||
}
|
||||
for _, slot := range h.storageList[addr] {
|
||||
storages[addrHash][slot] = append(storages[addrHash][slot], uint64(i+1))
|
||||
}
|
||||
}
|
||||
}
|
||||
for addrHash, indexes := range accounts {
|
||||
ir, _ := newIndexReader(db, newAccountIdent(addrHash))
|
||||
for i := 0; i < len(indexes)-1; i++ {
|
||||
n, err := ir.readGreaterThan(indexes[i])
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read index, %v", err)
|
||||
}
|
||||
if n != indexes[i+1] {
|
||||
t.Fatalf("Unexpected result, want %d, got %d", indexes[i+1], n)
|
||||
}
|
||||
}
|
||||
n, err := ir.readGreaterThan(indexes[len(indexes)-1])
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read index, %v", err)
|
||||
}
|
||||
if n != math.MaxUint64 {
|
||||
t.Fatalf("Unexpected result, want math.MaxUint64, got %d", n)
|
||||
}
|
||||
}
|
||||
for addrHash, slots := range storages {
|
||||
for slotHash, indexes := range slots {
|
||||
ir, _ := newIndexReader(db, newStorageIdent(addrHash, slotHash))
|
||||
for i := 0; i < len(indexes)-1; i++ {
|
||||
n, err := ir.readGreaterThan(indexes[i])
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read index, %v", err)
|
||||
}
|
||||
if n != indexes[i+1] {
|
||||
t.Fatalf("Unexpected result, want %d, got %d", indexes[i+1], n)
|
||||
}
|
||||
}
|
||||
n, err := ir.readGreaterThan(indexes[len(indexes)-1])
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read index, %v", err)
|
||||
}
|
||||
if n != math.MaxUint64 {
|
||||
t.Fatalf("Unexpected result, want math.MaxUint64, got %d", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchIndexerDelete(t *testing.T) {
|
||||
var (
|
||||
db = rawdb.NewMemoryDatabase()
|
||||
bw = newBatchIndexer(db, false)
|
||||
histories = makeHistories(10)
|
||||
)
|
||||
// Index histories
|
||||
for i, h := range histories {
|
||||
if err := bw.process(h, uint64(i+1)); err != nil {
|
||||
t.Fatalf("Failed to process history, %v", err)
|
||||
}
|
||||
}
|
||||
if err := bw.finish(true); err != nil {
|
||||
t.Fatalf("Failed to finish batch indexer, %v", err)
|
||||
}
|
||||
|
||||
// Unindex histories
|
||||
bd := newBatchIndexer(db, true)
|
||||
for i := len(histories) - 1; i >= 0; i-- {
|
||||
if err := bd.process(histories[i], uint64(i+1)); err != nil {
|
||||
t.Fatalf("Failed to process history, %v", err)
|
||||
}
|
||||
}
|
||||
if err := bd.finish(true); err != nil {
|
||||
t.Fatalf("Failed to finish batch indexer, %v", err)
|
||||
}
|
||||
|
||||
metadata := loadIndexMetadata(db)
|
||||
if metadata != nil {
|
||||
t.Fatal("Unexpected index position")
|
||||
}
|
||||
it := db.NewIterator(rawdb.StateHistoryIndexPrefix, nil)
|
||||
for it.Next() {
|
||||
t.Fatal("Leftover history index data")
|
||||
}
|
||||
it.Release()
|
||||
}
|
||||
613
triedb/pathdb/history_indexer.go
Normal file
613
triedb/pathdb/history_indexer.go
Normal file
|
|
@ -0,0 +1,613 @@
|
|||
// Copyright 2025 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 <http://www.gnu.org/licenses/
|
||||
|
||||
package pathdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
// The batch size for reading state histories
|
||||
historyReadBatch = 1000
|
||||
|
||||
stateIndexV0 = uint8(0) // initial version of state index structure
|
||||
stateIndexVersion = stateIndexV0 // the current state index version
|
||||
)
|
||||
|
||||
type indexMetadata struct {
|
||||
Version uint8
|
||||
Last uint64
|
||||
}
|
||||
|
||||
func loadIndexMetadata(db ethdb.KeyValueReader) *indexMetadata {
|
||||
blob := rawdb.ReadStateHistoryIndexMetadata(db)
|
||||
if len(blob) == 0 {
|
||||
return nil
|
||||
}
|
||||
var m indexMetadata
|
||||
if err := rlp.DecodeBytes(blob, &m); err != nil {
|
||||
log.Error("Failed to decode index metadata", "err", err)
|
||||
return nil
|
||||
}
|
||||
return &m
|
||||
}
|
||||
|
||||
func storeIndexMetadata(db ethdb.KeyValueWriter, last uint64) {
|
||||
var m indexMetadata
|
||||
m.Version = stateIndexVersion
|
||||
m.Last = last
|
||||
blob, err := rlp.EncodeToBytes(m)
|
||||
if err != nil {
|
||||
log.Crit("Failed to encode index metadata", "err", err)
|
||||
}
|
||||
rawdb.WriteStateHistoryIndexMetadata(db, blob)
|
||||
}
|
||||
|
||||
// batchIndexer is a structure designed to perform batch indexing or unindexing
|
||||
// of state histories atomically.
|
||||
type batchIndexer struct {
|
||||
accounts map[common.Hash][]uint64 // History ID list, Keyed by account address
|
||||
storages map[common.Hash]map[common.Hash][]uint64 // History ID list, Keyed by account address and the hash of raw storage key
|
||||
counter int // The counter of processed states
|
||||
delete bool // Index or unindex mode
|
||||
lastID uint64 // The ID of latest processed history
|
||||
db ethdb.KeyValueStore
|
||||
}
|
||||
|
||||
// newBatchIndexer constructs the batch indexer with the supplied mode.
|
||||
func newBatchIndexer(db ethdb.KeyValueStore, delete bool) *batchIndexer {
|
||||
return &batchIndexer{
|
||||
accounts: make(map[common.Hash][]uint64),
|
||||
storages: make(map[common.Hash]map[common.Hash][]uint64),
|
||||
delete: delete,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// process iterates through the accounts and their associated storage slots in the
|
||||
// state history, tracking the mapping between state and history IDs.
|
||||
func (b *batchIndexer) process(h *history, historyID uint64) error {
|
||||
for _, address := range h.accountList {
|
||||
addrHash := crypto.Keccak256Hash(address.Bytes())
|
||||
b.counter += 1
|
||||
b.accounts[addrHash] = append(b.accounts[addrHash], historyID)
|
||||
|
||||
for _, slotKey := range h.storageList[address] {
|
||||
b.counter += 1
|
||||
if _, ok := b.storages[addrHash]; !ok {
|
||||
b.storages[addrHash] = make(map[common.Hash][]uint64)
|
||||
}
|
||||
// The hash of the storage slot key is used as the identifier because the
|
||||
// legacy history does not include the raw storage key, therefore, the
|
||||
// conversion from storage key to hash is necessary for non-v0 histories.
|
||||
slotHash := slotKey
|
||||
if h.meta.version != stateHistoryV0 {
|
||||
slotHash = crypto.Keccak256Hash(slotKey.Bytes())
|
||||
}
|
||||
b.storages[addrHash][slotHash] = append(b.storages[addrHash][slotHash], historyID)
|
||||
}
|
||||
}
|
||||
b.lastID = historyID
|
||||
return b.finish(false)
|
||||
}
|
||||
|
||||
// finish writes the accumulated state indexes into the disk if either the
|
||||
// memory limitation is reached or it's requested forcibly.
|
||||
func (b *batchIndexer) finish(force bool) error {
|
||||
if b.counter == 0 {
|
||||
return nil
|
||||
}
|
||||
if !force && b.counter < historyIndexBatch {
|
||||
return nil
|
||||
}
|
||||
var (
|
||||
batch = b.db.NewBatch()
|
||||
batchMu sync.RWMutex
|
||||
eg errgroup.Group
|
||||
)
|
||||
eg.SetLimit(runtime.NumCPU())
|
||||
|
||||
for addrHash, idList := range b.accounts {
|
||||
eg.Go(func() error {
|
||||
if !b.delete {
|
||||
iw, err := newIndexWriter(b.db, newAccountIdent(addrHash))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, n := range idList {
|
||||
if err := iw.append(n); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
batchMu.Lock()
|
||||
iw.finish(batch)
|
||||
batchMu.Unlock()
|
||||
} else {
|
||||
id, err := newIndexDeleter(b.db, newAccountIdent(addrHash))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, n := range idList {
|
||||
if err := id.pop(n); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
batchMu.Lock()
|
||||
id.finish(batch)
|
||||
batchMu.Unlock()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
for addrHash, slots := range b.storages {
|
||||
for storageHash, idList := range slots {
|
||||
eg.Go(func() error {
|
||||
if !b.delete {
|
||||
iw, err := newIndexWriter(b.db, newStorageIdent(addrHash, storageHash))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, n := range idList {
|
||||
if err := iw.append(n); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
batchMu.Lock()
|
||||
iw.finish(batch)
|
||||
batchMu.Unlock()
|
||||
} else {
|
||||
id, err := newIndexDeleter(b.db, newStorageIdent(addrHash, storageHash))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, n := range idList {
|
||||
if err := id.pop(n); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
batchMu.Lock()
|
||||
id.finish(batch)
|
||||
batchMu.Unlock()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Update the position of last indexed state history
|
||||
if !b.delete {
|
||||
storeIndexMetadata(batch, b.lastID)
|
||||
} else {
|
||||
if b.lastID == 1 {
|
||||
rawdb.DeleteStateHistoryIndexMetadata(batch)
|
||||
} else {
|
||||
storeIndexMetadata(batch, b.lastID-1)
|
||||
}
|
||||
}
|
||||
if err := batch.Write(); err != nil {
|
||||
return err
|
||||
}
|
||||
b.counter = 0
|
||||
b.accounts = make(map[common.Hash][]uint64)
|
||||
b.storages = make(map[common.Hash]map[common.Hash][]uint64)
|
||||
return nil
|
||||
}
|
||||
|
||||
// indexSingle processes the state history with the specified ID for indexing.
|
||||
func indexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.AncientReader) error {
|
||||
defer func(start time.Time) {
|
||||
indexHistoryTimer.UpdateSince(start)
|
||||
}(time.Now())
|
||||
|
||||
metadata := loadIndexMetadata(db)
|
||||
if metadata == nil || metadata.Last+1 != historyID {
|
||||
last := "null"
|
||||
if metadata != nil {
|
||||
last = fmt.Sprintf("%v", metadata.Last)
|
||||
}
|
||||
return fmt.Errorf("history indexing is out of order, last: %s, requested: %d", last, historyID)
|
||||
}
|
||||
h, err := readHistory(freezer, historyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b := newBatchIndexer(db, false)
|
||||
if err := b.process(h, historyID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.finish(true); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debug("Indexed state history", "id", historyID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// unindexSingle processes the state history with the specified ID for unindexing.
|
||||
func unindexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.AncientReader) error {
|
||||
defer func(start time.Time) {
|
||||
unindexHistoryTimer.UpdateSince(start)
|
||||
}(time.Now())
|
||||
|
||||
metadata := loadIndexMetadata(db)
|
||||
if metadata == nil || metadata.Last != historyID {
|
||||
last := "null"
|
||||
if metadata != nil {
|
||||
last = fmt.Sprintf("%v", metadata.Last)
|
||||
}
|
||||
return fmt.Errorf("history unindexing is out of order, last: %s, requested: %d", last, historyID)
|
||||
}
|
||||
h, err := readHistory(freezer, historyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b := newBatchIndexer(db, true)
|
||||
if err := b.process(h, historyID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.finish(true); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debug("Unindexed state history", "id", historyID)
|
||||
return nil
|
||||
}
|
||||
|
||||
type interruptSignal struct {
|
||||
newLastID uint64
|
||||
result chan error
|
||||
}
|
||||
|
||||
// indexIniter is responsible for completing the indexing of remaining state
|
||||
// histories in batch. It runs as a one-time background thread and terminates
|
||||
// once all available state histories are indexed.
|
||||
//
|
||||
// Afterward, new state histories should be indexed synchronously alongside
|
||||
// the state data itself, ensuring both the history and its index are available.
|
||||
// If a state history is removed due to a rollback, the associated indexes should
|
||||
// be unmarked accordingly.
|
||||
type indexIniter struct {
|
||||
disk ethdb.KeyValueStore
|
||||
freezer ethdb.AncientStore
|
||||
interrupt chan *interruptSignal
|
||||
done chan struct{}
|
||||
closed chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func newIndexIniter(disk ethdb.KeyValueStore, freezer ethdb.AncientStore, lastID uint64) *indexIniter {
|
||||
initer := &indexIniter{
|
||||
disk: disk,
|
||||
freezer: freezer,
|
||||
interrupt: make(chan *interruptSignal),
|
||||
done: make(chan struct{}),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
initer.wg.Add(1)
|
||||
go initer.run(lastID)
|
||||
return initer
|
||||
}
|
||||
|
||||
func (i *indexIniter) close() {
|
||||
select {
|
||||
case <-i.closed:
|
||||
return
|
||||
default:
|
||||
close(i.closed)
|
||||
i.wg.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
func (i *indexIniter) inited() bool {
|
||||
select {
|
||||
case <-i.closed:
|
||||
return false
|
||||
case <-i.done:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (i *indexIniter) run(lastID uint64) {
|
||||
defer i.wg.Done()
|
||||
|
||||
// Launch background indexing thread
|
||||
var (
|
||||
done = make(chan struct{})
|
||||
interrupt = new(atomic.Int32)
|
||||
|
||||
// checkDone indicates whether all requested state histories
|
||||
// have been fully indexed.
|
||||
checkDone = func() bool {
|
||||
metadata := loadIndexMetadata(i.disk)
|
||||
return metadata != nil && metadata.Last == lastID
|
||||
}
|
||||
)
|
||||
go i.index(done, interrupt, lastID)
|
||||
|
||||
for {
|
||||
select {
|
||||
case signal := <-i.interrupt:
|
||||
// The indexing limit can only be extended or shortened continuously.
|
||||
if signal.newLastID != lastID+1 && signal.newLastID != lastID-1 {
|
||||
signal.result <- fmt.Errorf("invalid history id, last: %d, got: %d", lastID, signal.newLastID)
|
||||
continue
|
||||
}
|
||||
// The index limit is extended by one, update the limit without
|
||||
// interrupting the current background process.
|
||||
if signal.newLastID == lastID+1 {
|
||||
lastID = signal.newLastID
|
||||
signal.result <- nil
|
||||
log.Debug("Extended state history range", "last", lastID)
|
||||
continue
|
||||
}
|
||||
// The index limit is shortened by one, interrupt the current background
|
||||
// process and relaunch with new target.
|
||||
interrupt.Store(1)
|
||||
<-done
|
||||
|
||||
// If all state histories, including the one to be reverted, have
|
||||
// been fully indexed, unindex it here and shut down the initializer.
|
||||
if checkDone() {
|
||||
log.Info("Truncate the extra history", "id", lastID)
|
||||
if err := unindexSingle(lastID, i.disk, i.freezer); err != nil {
|
||||
signal.result <- err
|
||||
return
|
||||
}
|
||||
close(i.done)
|
||||
signal.result <- nil
|
||||
log.Info("State histories have been fully indexed", "last", lastID-1)
|
||||
return
|
||||
}
|
||||
// Adjust the indexing target and relaunch the process
|
||||
lastID = signal.newLastID
|
||||
done, interrupt = make(chan struct{}), new(atomic.Int32)
|
||||
go i.index(done, interrupt, lastID)
|
||||
log.Debug("Shortened state history range", "last", lastID)
|
||||
|
||||
case <-done:
|
||||
if checkDone() {
|
||||
close(i.done)
|
||||
log.Info("State histories have been fully indexed", "last", lastID)
|
||||
return
|
||||
}
|
||||
// Relaunch the background runner if some tasks are left
|
||||
done, interrupt = make(chan struct{}), new(atomic.Int32)
|
||||
go i.index(done, interrupt, lastID)
|
||||
|
||||
case <-i.closed:
|
||||
interrupt.Store(1)
|
||||
log.Info("Waiting background history index initer to exit")
|
||||
<-done
|
||||
|
||||
if checkDone() {
|
||||
close(i.done)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// next returns the ID of the next state history to be indexed.
|
||||
func (i *indexIniter) next() (uint64, error) {
|
||||
tail, err := i.freezer.Tail()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
tailID := tail + 1 // compute the id of the oldest history
|
||||
|
||||
// Start indexing from scratch if nothing has been indexed
|
||||
metadata := loadIndexMetadata(i.disk)
|
||||
if metadata == nil {
|
||||
log.Debug("Initialize state history indexing from scratch", "id", tailID)
|
||||
return tailID, nil
|
||||
}
|
||||
// Resume indexing from the last interrupted position
|
||||
if metadata.Last+1 >= tailID {
|
||||
log.Debug("Resume state history indexing", "id", metadata.Last+1, "tail", tailID)
|
||||
return metadata.Last + 1, nil
|
||||
}
|
||||
// History has been shortened without indexing. Discard the gapped segment
|
||||
// in the history and shift to the first available element.
|
||||
//
|
||||
// The missing indexes corresponding to the gapped histories won't be visible.
|
||||
// It's fine to leave them unindexed.
|
||||
log.Info("History gap detected, discard old segment", "oldHead", metadata.Last, "newHead", tailID)
|
||||
return tailID, nil
|
||||
}
|
||||
|
||||
func (i *indexIniter) index(done chan struct{}, interrupt *atomic.Int32, lastID uint64) {
|
||||
defer close(done)
|
||||
|
||||
beginID, err := i.next()
|
||||
if err != nil {
|
||||
log.Error("Failed to find next state history for indexing", "err", err)
|
||||
return
|
||||
}
|
||||
// All available state histories have been indexed, and the last indexed one
|
||||
// exceeds the most recent available state history. This situation may occur
|
||||
// when the state is reverted manually (chain.SetHead) or the deep reorg is
|
||||
// encountered. In such cases, no indexing should be scheduled.
|
||||
if beginID > lastID {
|
||||
log.Debug("State history is fully indexed", "last", lastID)
|
||||
return
|
||||
}
|
||||
log.Info("Start history indexing", "beginID", beginID, "lastID", lastID)
|
||||
|
||||
var (
|
||||
current = beginID
|
||||
start = time.Now()
|
||||
logged = time.Now()
|
||||
batch = newBatchIndexer(i.disk, false)
|
||||
)
|
||||
for current <= lastID {
|
||||
count := lastID - current + 1
|
||||
if count > historyReadBatch {
|
||||
count = historyReadBatch
|
||||
}
|
||||
histories, err := readHistories(i.freezer, current, count)
|
||||
if err != nil {
|
||||
// The history read might fall if the history is truncated from
|
||||
// head due to revert operation.
|
||||
log.Error("Failed to read history for indexing", "current", current, "count", count, "err", err)
|
||||
return
|
||||
}
|
||||
for _, h := range histories {
|
||||
if err := batch.process(h, current); err != nil {
|
||||
log.Error("Failed to index history", "err", err)
|
||||
return
|
||||
}
|
||||
current += 1
|
||||
|
||||
// Occasionally report the indexing progress
|
||||
if time.Since(logged) > time.Second*8 {
|
||||
logged = time.Now()
|
||||
|
||||
var (
|
||||
left = lastID - current + 1
|
||||
done = current - beginID
|
||||
speed = done/uint64(time.Since(start)/time.Millisecond+1) + 1 // +1s to avoid division by zero
|
||||
)
|
||||
// Override the ETA if larger than the largest until now
|
||||
eta := time.Duration(left/speed) * time.Millisecond
|
||||
log.Info("Indexing state history", "processed", done, "left", left, "eta", common.PrettyDuration(eta))
|
||||
}
|
||||
}
|
||||
// Check interruption signal and abort process if it's fired
|
||||
if interrupt != nil {
|
||||
if signal := interrupt.Load(); signal != 0 {
|
||||
if err := batch.finish(true); err != nil {
|
||||
log.Error("Failed to flush index", "err", err)
|
||||
}
|
||||
log.Info("State indexing interrupted")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := batch.finish(true); err != nil {
|
||||
log.Error("Failed to flush index", "err", err)
|
||||
}
|
||||
log.Info("Indexed state history", "from", beginID, "to", lastID, "elapsed", common.PrettyDuration(time.Since(start)))
|
||||
}
|
||||
|
||||
// historyIndexer manages the indexing and unindexing of state histories,
|
||||
// providing access to historical states.
|
||||
//
|
||||
// Upon initialization, historyIndexer starts a one-time background process
|
||||
// to complete the indexing of any remaining state histories. Once this
|
||||
// process is finished, all state histories are marked as fully indexed,
|
||||
// enabling handling of requests for historical states. Thereafter, any new
|
||||
// state histories must be indexed or unindexed synchronously, ensuring that
|
||||
// the history index is created or removed along with the corresponding
|
||||
// state history.
|
||||
type historyIndexer struct {
|
||||
initer *indexIniter
|
||||
disk ethdb.KeyValueStore
|
||||
freezer ethdb.AncientStore
|
||||
}
|
||||
|
||||
// checkVersion checks whether the index data in the database matches the version.
|
||||
func checkVersion(disk ethdb.KeyValueStore) {
|
||||
blob := rawdb.ReadStateHistoryIndexMetadata(disk)
|
||||
if len(blob) == 0 {
|
||||
return
|
||||
}
|
||||
var m indexMetadata
|
||||
err := rlp.DecodeBytes(blob, &m)
|
||||
if err == nil && m.Version == stateIndexVersion {
|
||||
return
|
||||
}
|
||||
// TODO(rjl493456442) would be better to group them into a batch.
|
||||
rawdb.DeleteStateHistoryIndexMetadata(disk)
|
||||
rawdb.DeleteStateHistoryIndex(disk)
|
||||
|
||||
version := "unknown"
|
||||
if err == nil {
|
||||
version = fmt.Sprintf("%d", m.Version)
|
||||
}
|
||||
log.Info("Cleaned up obsolete state history index", "version", version, "want", stateIndexVersion)
|
||||
}
|
||||
|
||||
// newHistoryIndexer constructs the history indexer and launches the background
|
||||
// initer to complete the indexing of any remaining state histories.
|
||||
func newHistoryIndexer(disk ethdb.KeyValueStore, freezer ethdb.AncientStore, lastHistoryID uint64) *historyIndexer {
|
||||
checkVersion(disk)
|
||||
return &historyIndexer{
|
||||
initer: newIndexIniter(disk, freezer, lastHistoryID),
|
||||
disk: disk,
|
||||
freezer: freezer,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *historyIndexer) close() {
|
||||
i.initer.close()
|
||||
}
|
||||
|
||||
// inited returns a flag indicating whether the existing state histories
|
||||
// have been fully indexed, in other words, whether they are available
|
||||
// for external access.
|
||||
func (i *historyIndexer) inited() bool {
|
||||
return i.initer.inited()
|
||||
}
|
||||
|
||||
// extend sends the notification that new state history with specified ID
|
||||
// has been written into the database and is ready for indexing.
|
||||
func (i *historyIndexer) extend(historyID uint64) error {
|
||||
signal := &interruptSignal{
|
||||
newLastID: historyID,
|
||||
result: make(chan error, 1),
|
||||
}
|
||||
select {
|
||||
case <-i.initer.closed:
|
||||
return errors.New("indexer is closed")
|
||||
case <-i.initer.done:
|
||||
return indexSingle(historyID, i.disk, i.freezer)
|
||||
case i.initer.interrupt <- signal:
|
||||
return <-signal.result
|
||||
}
|
||||
}
|
||||
|
||||
// shorten sends the notification that state history with specified ID
|
||||
// is about to be deleted from the database and should be unindexed.
|
||||
func (i *historyIndexer) shorten(historyID uint64) error {
|
||||
signal := &interruptSignal{
|
||||
newLastID: historyID - 1,
|
||||
result: make(chan error, 1),
|
||||
}
|
||||
select {
|
||||
case <-i.initer.closed:
|
||||
return errors.New("indexer is closed")
|
||||
case <-i.initer.done:
|
||||
return unindexSingle(historyID, i.disk, i.freezer)
|
||||
case i.initer.interrupt <- signal:
|
||||
return <-signal.result
|
||||
}
|
||||
}
|
||||
375
triedb/pathdb/history_reader.go
Normal file
375
triedb/pathdb/history_reader.go
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
// Copyright 2025 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 <http://www.gnu.org/licenses/
|
||||
|
||||
package pathdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
)
|
||||
|
||||
// stateIdent represents the identifier of a state element, which can be
|
||||
// either an account or a storage slot.
|
||||
type stateIdent struct {
|
||||
account bool
|
||||
|
||||
// The hash of the account address. This is used instead of the raw account
|
||||
// address is to align the traversal order with the Merkle-Patricia-Trie.
|
||||
addressHash common.Hash
|
||||
|
||||
// The hash of the storage slot key. This is used instead of the raw slot key
|
||||
// because, in legacy state histories (prior to the Cancun fork), the slot
|
||||
// identifier is the hash of the key, and the original key (preimage) cannot
|
||||
// be recovered. To maintain backward compatibility, the key hash is used.
|
||||
//
|
||||
// Meanwhile, using the storage key hash also preserve the traversal order
|
||||
// with Merkle-Patricia-Trie.
|
||||
//
|
||||
// This field is null if the identifier refers to account data.
|
||||
storageHash common.Hash
|
||||
}
|
||||
|
||||
// String returns the string format state identifier.
|
||||
func (ident stateIdent) String() string {
|
||||
if ident.account {
|
||||
return ident.addressHash.Hex()
|
||||
}
|
||||
return ident.addressHash.Hex() + ident.storageHash.Hex()
|
||||
}
|
||||
|
||||
// newAccountIdent constructs a state identifier for an account.
|
||||
func newAccountIdent(addressHash common.Hash) stateIdent {
|
||||
return stateIdent{
|
||||
account: true,
|
||||
addressHash: addressHash,
|
||||
}
|
||||
}
|
||||
|
||||
// newStorageIdent constructs a state identifier for a storage slot.
|
||||
// The address denotes the address of the associated account;
|
||||
// the storageHash denotes the hash of the raw storage slot key;
|
||||
func newStorageIdent(addressHash common.Hash, storageHash common.Hash) stateIdent {
|
||||
return stateIdent{
|
||||
addressHash: addressHash,
|
||||
storageHash: storageHash,
|
||||
}
|
||||
}
|
||||
|
||||
// stateIdentQuery is the extension of stateIdent by adding the raw storage key.
|
||||
type stateIdentQuery struct {
|
||||
stateIdent
|
||||
|
||||
address common.Address
|
||||
storageKey common.Hash
|
||||
}
|
||||
|
||||
// newAccountIdentQuery constructs a state identifier for an account.
|
||||
func newAccountIdentQuery(address common.Address, addressHash common.Hash) stateIdentQuery {
|
||||
return stateIdentQuery{
|
||||
stateIdent: stateIdent{
|
||||
account: true,
|
||||
addressHash: addressHash,
|
||||
},
|
||||
address: address,
|
||||
}
|
||||
}
|
||||
|
||||
// newStorageIdentQuery constructs a state identifier for a storage slot.
|
||||
// the address denotes the address of the associated account;
|
||||
// the addressHash denotes the address hash of the associated account;
|
||||
// the storageKey denotes the raw storage slot key;
|
||||
// the storageHash denotes the hash of the raw storage slot key;
|
||||
func newStorageIdentQuery(address common.Address, addressHash common.Hash, storageKey common.Hash, storageHash common.Hash) stateIdentQuery {
|
||||
return stateIdentQuery{
|
||||
stateIdent: stateIdent{
|
||||
addressHash: addressHash,
|
||||
storageHash: storageHash,
|
||||
},
|
||||
address: address,
|
||||
storageKey: storageKey,
|
||||
}
|
||||
}
|
||||
|
||||
// indexReaderWithLimitTag is a wrapper around indexReader that includes an
|
||||
// additional index position. This position represents the ID of the last
|
||||
// indexed state history at the time the reader was created, implying that
|
||||
// indexes beyond this position are unavailable.
|
||||
type indexReaderWithLimitTag struct {
|
||||
reader *indexReader
|
||||
limit uint64
|
||||
db ethdb.KeyValueReader
|
||||
}
|
||||
|
||||
// newIndexReaderWithLimitTag constructs a index reader with indexing position.
|
||||
func newIndexReaderWithLimitTag(db ethdb.KeyValueReader, state stateIdent) (*indexReaderWithLimitTag, error) {
|
||||
// Read the last indexed ID before the index reader construction
|
||||
metadata := loadIndexMetadata(db)
|
||||
if metadata == nil {
|
||||
return nil, errors.New("state history hasn't been indexed yet")
|
||||
}
|
||||
r, err := newIndexReader(db, state)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &indexReaderWithLimitTag{
|
||||
reader: r,
|
||||
limit: metadata.Last,
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// readGreaterThan locates the first element that is greater than the specified
|
||||
// id. If no such element is found, MaxUint64 is returned.
|
||||
//
|
||||
// Note: It is possible that additional histories have been indexed since the
|
||||
// reader was created. The reader should be refreshed as needed to load the
|
||||
// latest indexed data from disk.
|
||||
func (r *indexReaderWithLimitTag) readGreaterThan(id uint64, lastID uint64) (uint64, error) {
|
||||
// Mark the index reader as stale if the tracked indexing position moves
|
||||
// backward. This can occur if the pathdb is reverted and certain state
|
||||
// histories are unindexed. For simplicity, the reader is marked as stale
|
||||
// instead of being refreshed, as this scenario is highly unlikely.
|
||||
if r.limit > lastID {
|
||||
return 0, fmt.Errorf("index reader is stale, limit: %d, last-state-id: %d", r.limit, lastID)
|
||||
}
|
||||
// Try to find the element which is greater than the specified target
|
||||
res, err := r.reader.readGreaterThan(id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// Short circuit if the element is found within the current index
|
||||
if res != math.MaxUint64 {
|
||||
return res, nil
|
||||
}
|
||||
// The element was not found, and no additional histories have been indexed.
|
||||
// Return a not-found result.
|
||||
if r.limit == lastID {
|
||||
return res, nil
|
||||
}
|
||||
// Refresh the index reader and attempt again. If the latest indexed position
|
||||
// is even below the ID of the disk layer, it indicates that state histories
|
||||
// are being removed. In this case, it would theoretically be better to block
|
||||
// the state rollback operation synchronously until all readers are released.
|
||||
// Given that it's very unlikely to occur and users try to perform historical
|
||||
// state queries while reverting the states at the same time. Simply returning
|
||||
// an error should be sufficient for now.
|
||||
metadata := loadIndexMetadata(r.db)
|
||||
if metadata == nil || metadata.Last < lastID {
|
||||
return 0, errors.New("state history hasn't been indexed yet")
|
||||
}
|
||||
if err := r.reader.refresh(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
r.limit = metadata.Last
|
||||
|
||||
return r.reader.readGreaterThan(id)
|
||||
}
|
||||
|
||||
// historyReader is the structure to access historic state data.
|
||||
type historyReader struct {
|
||||
disk ethdb.KeyValueReader
|
||||
freezer ethdb.AncientReader
|
||||
readers map[string]*indexReaderWithLimitTag
|
||||
}
|
||||
|
||||
// newHistoryReader constructs the history reader with the supplied db.
|
||||
func newHistoryReader(disk ethdb.KeyValueReader, freezer ethdb.AncientReader) *historyReader {
|
||||
return &historyReader{
|
||||
disk: disk,
|
||||
freezer: freezer,
|
||||
readers: make(map[string]*indexReaderWithLimitTag),
|
||||
}
|
||||
}
|
||||
|
||||
// readAccountMetadata resolves the account metadata within the specified
|
||||
// state history.
|
||||
func (r *historyReader) readAccountMetadata(address common.Address, historyID uint64) ([]byte, error) {
|
||||
blob := rawdb.ReadStateAccountIndex(r.freezer, historyID)
|
||||
if len(blob) == 0 {
|
||||
return nil, fmt.Errorf("account index is truncated, historyID: %d", historyID)
|
||||
}
|
||||
if len(blob)%accountIndexSize != 0 {
|
||||
return nil, fmt.Errorf("account index is corrupted, historyID: %d, size: %d", historyID, len(blob))
|
||||
}
|
||||
n := len(blob) / accountIndexSize
|
||||
|
||||
pos := sort.Search(n, func(i int) bool {
|
||||
h := blob[accountIndexSize*i : accountIndexSize*i+common.HashLength]
|
||||
return bytes.Compare(h, address.Bytes()) >= 0
|
||||
})
|
||||
if pos == n {
|
||||
return nil, fmt.Errorf("account %#x is not found", address)
|
||||
}
|
||||
offset := accountIndexSize * pos
|
||||
if address != common.BytesToAddress(blob[offset:offset+common.AddressLength]) {
|
||||
return nil, fmt.Errorf("account %#x is not found", address)
|
||||
}
|
||||
return blob[offset : accountIndexSize*(pos+1)], nil
|
||||
}
|
||||
|
||||
// readStorageMetadata resolves the storage slot metadata within the specified
|
||||
// state history.
|
||||
func (r *historyReader) readStorageMetadata(storageKey common.Hash, storageHash common.Hash, historyID uint64, slotOffset, slotNumber int) ([]byte, error) {
|
||||
// TODO(rj493456442) optimize it with partial read
|
||||
blob := rawdb.ReadStateStorageIndex(r.freezer, historyID)
|
||||
if len(blob) == 0 {
|
||||
return nil, fmt.Errorf("storage index is truncated, historyID: %d", historyID)
|
||||
}
|
||||
if len(blob)%slotIndexSize != 0 {
|
||||
return nil, fmt.Errorf("storage indices is corrupted, historyID: %d, size: %d", historyID, len(blob))
|
||||
}
|
||||
if slotIndexSize*(slotOffset+slotNumber) > len(blob) {
|
||||
return nil, fmt.Errorf("storage indices is truncated, historyID: %d, size: %d, offset: %d, length: %d", historyID, len(blob), slotOffset, slotNumber)
|
||||
}
|
||||
subSlice := blob[slotIndexSize*slotOffset : slotIndexSize*(slotOffset+slotNumber)]
|
||||
|
||||
// TODO(rj493456442) get rid of the metadata resolution
|
||||
var (
|
||||
m meta
|
||||
target common.Hash
|
||||
)
|
||||
blob = rawdb.ReadStateHistoryMeta(r.freezer, historyID)
|
||||
if err := m.decode(blob); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if m.version == stateHistoryV0 {
|
||||
target = storageHash
|
||||
} else {
|
||||
target = storageKey
|
||||
}
|
||||
pos := sort.Search(slotNumber, func(i int) bool {
|
||||
slotID := subSlice[slotIndexSize*i : slotIndexSize*i+common.HashLength]
|
||||
return bytes.Compare(slotID, target.Bytes()) >= 0
|
||||
})
|
||||
if pos == slotNumber {
|
||||
return nil, fmt.Errorf("storage metadata is not found, slot key: %#x, historyID: %d", storageKey, historyID)
|
||||
}
|
||||
offset := slotIndexSize * pos
|
||||
if target != common.BytesToHash(subSlice[offset:offset+common.HashLength]) {
|
||||
return nil, fmt.Errorf("storage metadata is not found, slot key: %#x, historyID: %d", storageKey, historyID)
|
||||
}
|
||||
return subSlice[offset : slotIndexSize*(pos+1)], nil
|
||||
}
|
||||
|
||||
// readAccount retrieves the account data from the specified state history.
|
||||
func (r *historyReader) readAccount(address common.Address, historyID uint64) ([]byte, error) {
|
||||
metadata, err := r.readAccountMetadata(address, historyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
length := int(metadata[common.AddressLength]) // one byte for account data length
|
||||
offset := int(binary.BigEndian.Uint32(metadata[common.AddressLength+1 : common.AddressLength+5])) // four bytes for the account data offset
|
||||
|
||||
// TODO(rj493456442) optimize it with partial read
|
||||
data := rawdb.ReadStateAccountHistory(r.freezer, historyID)
|
||||
if len(data) < length+offset {
|
||||
return nil, fmt.Errorf("account data is truncated, address: %#x, historyID: %d, size: %d, offset: %d, len: %d", address, historyID, len(data), offset, length)
|
||||
}
|
||||
return data[offset : offset+length], nil
|
||||
}
|
||||
|
||||
// readStorage retrieves the storage slot data from the specified state history.
|
||||
func (r *historyReader) readStorage(address common.Address, storageKey common.Hash, storageHash common.Hash, historyID uint64) ([]byte, error) {
|
||||
metadata, err := r.readAccountMetadata(address, historyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// slotIndexOffset:
|
||||
// The offset of storage indices associated with the specified account.
|
||||
// slotIndexNumber:
|
||||
// The number of storage indices associated with the specified account.
|
||||
slotIndexOffset := int(binary.BigEndian.Uint32(metadata[common.AddressLength+5 : common.AddressLength+9]))
|
||||
slotIndexNumber := int(binary.BigEndian.Uint32(metadata[common.AddressLength+9 : common.AddressLength+13]))
|
||||
|
||||
slotMetadata, err := r.readStorageMetadata(storageKey, storageHash, historyID, slotIndexOffset, slotIndexNumber)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
length := int(slotMetadata[common.HashLength]) // one byte for slot data length
|
||||
offset := int(binary.BigEndian.Uint32(slotMetadata[common.HashLength+1 : common.HashLength+5])) // four bytes for slot data offset
|
||||
|
||||
// TODO(rj493456442) optimize it with partial read
|
||||
data := rawdb.ReadStateStorageHistory(r.freezer, historyID)
|
||||
if len(data) < offset+length {
|
||||
return nil, fmt.Errorf("storage data is truncated, address: %#x, key: %#x, historyID: %d, size: %d, offset: %d, len: %d", address, storageKey, historyID, len(data), offset, length)
|
||||
}
|
||||
return data[offset : offset+length], nil
|
||||
}
|
||||
|
||||
// read retrieves the state element data associated with the stateID.
|
||||
// stateID: represents the ID of the state of the specified version;
|
||||
// lastID: represents the ID of the latest/newest state history;
|
||||
// latestValue: represents the state value at the current disk layer with ID == lastID;
|
||||
func (r *historyReader) read(state stateIdentQuery, stateID uint64, lastID uint64, latestValue []byte) ([]byte, error) {
|
||||
tail, err := r.freezer.Tail()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// stateID == tail is allowed, as the first history object preserved
|
||||
// is tail+1
|
||||
if stateID < tail {
|
||||
return nil, errors.New("historical state has been pruned")
|
||||
}
|
||||
|
||||
// To serve the request, all state histories from stateID+1 to lastID
|
||||
// must be indexed. It's not supposed to happen unless system is very
|
||||
// wrong.
|
||||
metadata := loadIndexMetadata(r.disk)
|
||||
if metadata == nil || metadata.Last < lastID {
|
||||
indexed := "null"
|
||||
if metadata != nil {
|
||||
indexed = fmt.Sprintf("%d", metadata.Last)
|
||||
}
|
||||
return nil, fmt.Errorf("state history is not fully indexed, requested: %d, indexed: %s", stateID, indexed)
|
||||
}
|
||||
|
||||
// Construct the index reader to locate the corresponding history for
|
||||
// state retrieval
|
||||
ir, ok := r.readers[state.String()]
|
||||
if !ok {
|
||||
ir, err = newIndexReaderWithLimitTag(r.disk, state.stateIdent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.readers[state.String()] = ir
|
||||
}
|
||||
historyID, err := ir.readGreaterThan(stateID, lastID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The state was not found in the state histories, as it has not been modified
|
||||
// since stateID. Use the data from the associated disk layer instead.
|
||||
if historyID == math.MaxUint64 {
|
||||
return latestValue, nil
|
||||
}
|
||||
// Resolve data from the specified state history object. Notably, since the history
|
||||
// reader operates completely asynchronously with the indexer/unindexer, it's possible
|
||||
// that the associated state histories are no longer available due to a rollback.
|
||||
// Such truncation should be captured by the state resolver below, rather than returning
|
||||
// invalid data.
|
||||
if state.account {
|
||||
return r.readAccount(state.address, historyID)
|
||||
}
|
||||
return r.readStorage(state.address, state.storageKey, state.storageHash, historyID)
|
||||
}
|
||||
159
triedb/pathdb/history_reader_test.go
Normal file
159
triedb/pathdb/history_reader_test.go
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
// Copyright 2025 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 <http://www.gnu.org/licenses/
|
||||
|
||||
package pathdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
)
|
||||
|
||||
func waitIndexing(db *Database) {
|
||||
for {
|
||||
metadata := loadIndexMetadata(db.diskdb)
|
||||
if metadata != nil && metadata.Last >= db.tree.bottom().stateID() {
|
||||
return
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func checkHistoricState(env *tester, root common.Hash, hr *historyReader) error {
|
||||
// Short circuit if the historical state is no longer available
|
||||
if rawdb.ReadStateID(env.db.diskdb, root) == nil {
|
||||
return nil
|
||||
}
|
||||
var (
|
||||
dl = env.db.tree.bottom()
|
||||
stateID = rawdb.ReadStateID(env.db.diskdb, root)
|
||||
accounts = env.snapAccounts[root]
|
||||
storages = env.snapStorages[root]
|
||||
)
|
||||
for addrHash, accountData := range accounts {
|
||||
latest, _ := dl.account(addrHash, 0)
|
||||
blob, err := hr.read(newAccountIdentQuery(env.accountPreimage(addrHash), addrHash), *stateID, dl.stateID(), latest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Equal(accountData, blob) {
|
||||
return fmt.Errorf("wrong account data, expected %x, got %x", accountData, blob)
|
||||
}
|
||||
}
|
||||
for i := 0; i < len(env.roots); i++ {
|
||||
if env.roots[i] == root {
|
||||
break
|
||||
}
|
||||
// Find all accounts deleted in the past, ensure the associated data is null
|
||||
for addrHash := range env.snapAccounts[env.roots[i]] {
|
||||
if _, ok := accounts[addrHash]; !ok {
|
||||
latest, _ := dl.account(addrHash, 0)
|
||||
blob, err := hr.read(newAccountIdentQuery(env.accountPreimage(addrHash), addrHash), *stateID, dl.stateID(), latest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(blob) != 0 {
|
||||
return fmt.Errorf("wrong account data, expected null, got %x", blob)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for addrHash, slots := range storages {
|
||||
for slotHash, slotData := range slots {
|
||||
latest, _ := dl.storage(addrHash, slotHash, 0)
|
||||
blob, err := hr.read(newStorageIdentQuery(env.accountPreimage(addrHash), addrHash, env.hashPreimage(slotHash), slotHash), *stateID, dl.stateID(), latest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Equal(slotData, blob) {
|
||||
return fmt.Errorf("wrong storage data, expected %x, got %x", slotData, blob)
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := 0; i < len(env.roots); i++ {
|
||||
if env.roots[i] == root {
|
||||
break
|
||||
}
|
||||
// Find all storage slots deleted in the past, ensure the associated data is null
|
||||
for addrHash, slots := range env.snapStorages[env.roots[i]] {
|
||||
for slotHash := range slots {
|
||||
_, ok := storages[addrHash]
|
||||
if ok {
|
||||
_, ok = storages[addrHash][slotHash]
|
||||
}
|
||||
if !ok {
|
||||
latest, _ := dl.storage(addrHash, slotHash, 0)
|
||||
blob, err := hr.read(newStorageIdentQuery(env.accountPreimage(addrHash), addrHash, env.hashPreimage(slotHash), slotHash), *stateID, dl.stateID(), latest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(blob) != 0 {
|
||||
return fmt.Errorf("wrong storage data, expected null, got %x", blob)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestHistoryReader(t *testing.T) {
|
||||
testHistoryReader(t, 0) // with all histories reserved
|
||||
testHistoryReader(t, 10) // with latest 10 histories reserved
|
||||
}
|
||||
|
||||
func testHistoryReader(t *testing.T, historyLimit uint64) {
|
||||
maxDiffLayers = 4
|
||||
defer func() {
|
||||
maxDiffLayers = 128
|
||||
}()
|
||||
//log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelDebug, true)))
|
||||
|
||||
env := newTester(t, historyLimit, false, 64, true)
|
||||
defer env.release()
|
||||
waitIndexing(env.db)
|
||||
|
||||
var (
|
||||
roots = env.roots
|
||||
dRoot = env.db.tree.bottom().rootHash()
|
||||
hr = newHistoryReader(env.db.diskdb, env.db.freezer)
|
||||
)
|
||||
for _, root := range roots {
|
||||
if root == dRoot {
|
||||
break
|
||||
}
|
||||
if err := checkHistoricState(env, root, hr); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Pile up more histories on top, ensuring the historic reader is not affected
|
||||
env.extend(4)
|
||||
waitIndexing(env.db)
|
||||
|
||||
for _, root := range roots {
|
||||
if root == dRoot {
|
||||
break
|
||||
}
|
||||
if err := checkHistoricState(env, root, hr); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,10 @@ type binaryIterator struct {
|
|||
// accounts in a slow, but easily verifiable way. Note this function is used
|
||||
// for initialization, use `newBinaryAccountIterator` as the API.
|
||||
func (dl *diskLayer) initBinaryAccountIterator(seek common.Hash) *binaryIterator {
|
||||
// Block until the frozen buffer flushing is completed.
|
||||
if err := dl.waitFlush(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// The state set in the disk layer is mutable, hold the lock before obtaining
|
||||
// the account list to prevent concurrent map iteration and write.
|
||||
dl.lock.RLock()
|
||||
|
|
@ -113,6 +117,10 @@ func (dl *diffLayer) initBinaryAccountIterator(seek common.Hash) *binaryIterator
|
|||
// storage slots in a slow, but easily verifiable way. Note this function is used
|
||||
// for initialization, use `newBinaryStorageIterator` as the API.
|
||||
func (dl *diskLayer) initBinaryStorageIterator(account common.Hash, seek common.Hash) *binaryIterator {
|
||||
// Block until the frozen buffer flushing is completed.
|
||||
if err := dl.waitFlush(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// The state set in the disk layer is mutable, hold the lock before obtaining
|
||||
// the storage list to prevent concurrent map iteration and write.
|
||||
dl.lock.RLock()
|
||||
|
|
|
|||
|
|
@ -76,6 +76,11 @@ func newFastIterator(db *Database, root common.Hash, account common.Hash, seek c
|
|||
if accountIterator {
|
||||
switch dl := current.(type) {
|
||||
case *diskLayer:
|
||||
// Ensure no active background buffer flush is in progress, otherwise,
|
||||
// part of the state data may become invisible.
|
||||
if err := dl.waitFlush(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The state set in the disk layer is mutable, hold the lock before obtaining
|
||||
// the account list to prevent concurrent map iteration and write.
|
||||
dl.lock.RLock()
|
||||
|
|
@ -113,6 +118,11 @@ func newFastIterator(db *Database, root common.Hash, account common.Hash, seek c
|
|||
} else {
|
||||
switch dl := current.(type) {
|
||||
case *diskLayer:
|
||||
// Ensure no active background buffer flush is in progress, otherwise,
|
||||
// part of the state data may become invisible.
|
||||
if err := dl.waitFlush(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The state set in the disk layer is mutable, hold the lock before obtaining
|
||||
// the storage list to prevent concurrent map iteration and write.
|
||||
dl.lock.RLock()
|
||||
|
|
|
|||
|
|
@ -254,10 +254,9 @@ func TestFastIteratorBasics(t *testing.T) {
|
|||
// TestAccountIteratorTraversal tests some simple multi-layer iteration.
|
||||
func TestAccountIteratorTraversal(t *testing.T) {
|
||||
config := &Config{
|
||||
WriteBufferSize: 0,
|
||||
NoAsyncGeneration: true,
|
||||
}
|
||||
db := New(rawdb.NewMemoryDatabase(), config, false)
|
||||
db.waitGeneration()
|
||||
|
||||
// Stack three diff layers on top with various overlaps
|
||||
db.Update(common.HexToHash("0x02"), types.EmptyRootHash, 0, trienode.NewMergedNodeSet(),
|
||||
|
|
@ -298,10 +297,9 @@ func TestAccountIteratorTraversal(t *testing.T) {
|
|||
|
||||
func TestStorageIteratorTraversal(t *testing.T) {
|
||||
config := &Config{
|
||||
WriteBufferSize: 0,
|
||||
NoAsyncGeneration: true,
|
||||
}
|
||||
db := New(rawdb.NewMemoryDatabase(), config, false)
|
||||
db.waitGeneration()
|
||||
|
||||
// Stack three diff layers on top with various overlaps
|
||||
db.Update(common.HexToHash("0x02"), types.EmptyRootHash, 0, trienode.NewMergedNodeSet(),
|
||||
|
|
@ -311,14 +309,14 @@ func TestStorageIteratorTraversal(t *testing.T) {
|
|||
NewStateSetWithOrigin(randomAccountSet("0xaa"), randomStorageSet([]string{"0xaa"}, [][]string{{"0x04", "0x05", "0x06"}}, nil), nil, nil, false))
|
||||
|
||||
db.Update(common.HexToHash("0x04"), common.HexToHash("0x03"), 0, trienode.NewMergedNodeSet(),
|
||||
NewStateSetWithOrigin(randomAccountSet("0xaa"), randomStorageSet([]string{"0xaa"}, [][]string{{"0x01", "0x02", "0x03"}}, nil), nil, nil, false))
|
||||
NewStateSetWithOrigin(randomAccountSet("0xaa"), randomStorageSet([]string{"0xaa"}, [][]string{{"0x01", "0x02"}}, nil), nil, nil, false))
|
||||
|
||||
// Verify the single and multi-layer iterators
|
||||
head := db.tree.get(common.HexToHash("0x04"))
|
||||
|
||||
// singleLayer: 0x1, 0x2, 0x3
|
||||
diffIter := newDiffStorageIterator(common.HexToHash("0xaa"), common.Hash{}, head.(*diffLayer).states.stateSet.storageList(common.HexToHash("0xaa")), nil)
|
||||
verifyIterator(t, 3, diffIter, verifyNothing)
|
||||
verifyIterator(t, 2, diffIter, verifyNothing)
|
||||
|
||||
// binaryIterator: 0x1, 0x2, 0x3, 0x4, 0x5, 0x6
|
||||
verifyIterator(t, 6, head.(*diffLayer).newBinaryStorageIterator(common.HexToHash("0xaa"), common.Hash{}), verifyStorage)
|
||||
|
|
@ -342,10 +340,9 @@ func TestStorageIteratorTraversal(t *testing.T) {
|
|||
// also expect the correct values to show up.
|
||||
func TestAccountIteratorTraversalValues(t *testing.T) {
|
||||
config := &Config{
|
||||
WriteBufferSize: 0,
|
||||
NoAsyncGeneration: true,
|
||||
}
|
||||
db := New(rawdb.NewMemoryDatabase(), config, false)
|
||||
db.waitGeneration()
|
||||
|
||||
// Create a batch of account sets to seed subsequent layers with
|
||||
var (
|
||||
|
|
@ -458,10 +455,9 @@ func TestAccountIteratorTraversalValues(t *testing.T) {
|
|||
|
||||
func TestStorageIteratorTraversalValues(t *testing.T) {
|
||||
config := &Config{
|
||||
WriteBufferSize: 0,
|
||||
NoAsyncGeneration: true,
|
||||
}
|
||||
db := New(rawdb.NewMemoryDatabase(), config, false)
|
||||
db.waitGeneration()
|
||||
|
||||
wrapStorage := func(storage map[common.Hash][]byte) map[common.Hash]map[common.Hash][]byte {
|
||||
return map[common.Hash]map[common.Hash][]byte{
|
||||
|
|
@ -591,10 +587,9 @@ func TestAccountIteratorLargeTraversal(t *testing.T) {
|
|||
}
|
||||
// Build up a large stack of snapshots
|
||||
config := &Config{
|
||||
WriteBufferSize: 0,
|
||||
NoAsyncGeneration: true,
|
||||
}
|
||||
db := New(rawdb.NewMemoryDatabase(), config, false)
|
||||
db.waitGeneration()
|
||||
|
||||
for i := 1; i < 128; i++ {
|
||||
parent := types.EmptyRootHash
|
||||
|
|
@ -631,9 +626,9 @@ func TestAccountIteratorLargeTraversal(t *testing.T) {
|
|||
func TestAccountIteratorFlattening(t *testing.T) {
|
||||
config := &Config{
|
||||
WriteBufferSize: 10 * 1024,
|
||||
NoAsyncGeneration: true,
|
||||
}
|
||||
db := New(rawdb.NewMemoryDatabase(), config, false)
|
||||
db.waitGeneration()
|
||||
|
||||
// Create a stack of diffs on top
|
||||
db.Update(common.HexToHash("0x02"), types.EmptyRootHash, 1, trienode.NewMergedNodeSet(),
|
||||
|
|
@ -662,11 +657,24 @@ func TestAccountIteratorFlattening(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAccountIteratorSeek(t *testing.T) {
|
||||
t.Run("fast", func(t *testing.T) {
|
||||
testAccountIteratorSeek(t, func(db *Database, root, seek common.Hash) AccountIterator {
|
||||
it, _ := db.AccountIterator(root, seek)
|
||||
return it
|
||||
})
|
||||
})
|
||||
t.Run("binary", func(t *testing.T) {
|
||||
testAccountIteratorSeek(t, func(db *Database, root, seek common.Hash) AccountIterator {
|
||||
return db.tree.get(root).(*diffLayer).newBinaryAccountIterator(seek)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func testAccountIteratorSeek(t *testing.T, newIterator func(db *Database, root, seek common.Hash) AccountIterator) {
|
||||
config := &Config{
|
||||
WriteBufferSize: 0,
|
||||
NoAsyncGeneration: true,
|
||||
}
|
||||
db := New(rawdb.NewMemoryDatabase(), config, false)
|
||||
db.waitGeneration()
|
||||
|
||||
db.Update(common.HexToHash("0x02"), types.EmptyRootHash, 1, trienode.NewMergedNodeSet(),
|
||||
NewStateSetWithOrigin(randomAccountSet("0xaa", "0xee", "0xff", "0xf0"), nil, nil, nil, false))
|
||||
|
|
@ -682,39 +690,39 @@ func TestAccountIteratorSeek(t *testing.T) {
|
|||
// 03: aa, bb, dd, ee, f0 (, f0), ff
|
||||
// 04: aa, bb, cc, dd, ee, f0 (, f0), ff (, ff)
|
||||
// Construct various iterators and ensure their traversal is correct
|
||||
it, _ := db.AccountIterator(common.HexToHash("0x02"), common.HexToHash("0xdd"))
|
||||
it := newIterator(db, common.HexToHash("0x02"), common.HexToHash("0xdd"))
|
||||
defer it.Release()
|
||||
verifyIterator(t, 3, it, verifyAccount) // expected: ee, f0, ff
|
||||
|
||||
it, _ = db.AccountIterator(common.HexToHash("0x02"), common.HexToHash("0xaa"))
|
||||
it = newIterator(db, common.HexToHash("0x02"), common.HexToHash("0xaa"))
|
||||
defer it.Release()
|
||||
verifyIterator(t, 4, it, verifyAccount) // expected: aa, ee, f0, ff
|
||||
|
||||
it, _ = db.AccountIterator(common.HexToHash("0x02"), common.HexToHash("0xff"))
|
||||
it = newIterator(db, common.HexToHash("0x02"), common.HexToHash("0xff"))
|
||||
defer it.Release()
|
||||
verifyIterator(t, 1, it, verifyAccount) // expected: ff
|
||||
|
||||
it, _ = db.AccountIterator(common.HexToHash("0x02"), common.HexToHash("0xff1"))
|
||||
it = newIterator(db, common.HexToHash("0x02"), common.HexToHash("0xff1"))
|
||||
defer it.Release()
|
||||
verifyIterator(t, 0, it, verifyAccount) // expected: nothing
|
||||
|
||||
it, _ = db.AccountIterator(common.HexToHash("0x04"), common.HexToHash("0xbb"))
|
||||
it = newIterator(db, common.HexToHash("0x04"), common.HexToHash("0xbb"))
|
||||
defer it.Release()
|
||||
verifyIterator(t, 6, it, verifyAccount) // expected: bb, cc, dd, ee, f0, ff
|
||||
|
||||
it, _ = db.AccountIterator(common.HexToHash("0x04"), common.HexToHash("0xef"))
|
||||
it = newIterator(db, common.HexToHash("0x04"), common.HexToHash("0xef"))
|
||||
defer it.Release()
|
||||
verifyIterator(t, 2, it, verifyAccount) // expected: f0, ff
|
||||
|
||||
it, _ = db.AccountIterator(common.HexToHash("0x04"), common.HexToHash("0xf0"))
|
||||
it = newIterator(db, common.HexToHash("0x04"), common.HexToHash("0xf0"))
|
||||
defer it.Release()
|
||||
verifyIterator(t, 2, it, verifyAccount) // expected: f0, ff
|
||||
|
||||
it, _ = db.AccountIterator(common.HexToHash("0x04"), common.HexToHash("0xff"))
|
||||
it = newIterator(db, common.HexToHash("0x04"), common.HexToHash("0xff"))
|
||||
defer it.Release()
|
||||
verifyIterator(t, 1, it, verifyAccount) // expected: ff
|
||||
|
||||
it, _ = db.AccountIterator(common.HexToHash("0x04"), common.HexToHash("0xff1"))
|
||||
it = newIterator(db, common.HexToHash("0x04"), common.HexToHash("0xff1"))
|
||||
defer it.Release()
|
||||
verifyIterator(t, 0, it, verifyAccount) // expected: nothing
|
||||
}
|
||||
|
|
@ -735,10 +743,9 @@ func TestStorageIteratorSeek(t *testing.T) {
|
|||
|
||||
func testStorageIteratorSeek(t *testing.T, newIterator func(db *Database, root, account, seek common.Hash) StorageIterator) {
|
||||
config := &Config{
|
||||
WriteBufferSize: 0,
|
||||
NoAsyncGeneration: true,
|
||||
}
|
||||
db := New(rawdb.NewMemoryDatabase(), config, false)
|
||||
db.waitGeneration()
|
||||
|
||||
// Stack three diff layers on top with various overlaps
|
||||
db.Update(common.HexToHash("0x02"), types.EmptyRootHash, 1, trienode.NewMergedNodeSet(),
|
||||
|
|
@ -807,10 +814,9 @@ func TestAccountIteratorDeletions(t *testing.T) {
|
|||
|
||||
func testAccountIteratorDeletions(t *testing.T, newIterator func(db *Database, root, seek common.Hash) AccountIterator) {
|
||||
config := &Config{
|
||||
WriteBufferSize: 0,
|
||||
NoAsyncGeneration: true,
|
||||
}
|
||||
db := New(rawdb.NewMemoryDatabase(), config, false)
|
||||
db.waitGeneration()
|
||||
|
||||
// Stack three diff layers on top with various overlaps
|
||||
db.Update(common.HexToHash("0x02"), types.EmptyRootHash, 1, trienode.NewMergedNodeSet(),
|
||||
|
|
@ -847,10 +853,9 @@ func testAccountIteratorDeletions(t *testing.T, newIterator func(db *Database, r
|
|||
|
||||
func TestStorageIteratorDeletions(t *testing.T) {
|
||||
config := &Config{
|
||||
WriteBufferSize: 0,
|
||||
NoAsyncGeneration: true,
|
||||
}
|
||||
db := New(rawdb.NewMemoryDatabase(), config, false)
|
||||
db.waitGeneration()
|
||||
|
||||
// Stack three diff layers on top with various overlaps
|
||||
db.Update(common.HexToHash("0x02"), types.EmptyRootHash, 1, trienode.NewMergedNodeSet(),
|
||||
|
|
@ -916,9 +921,9 @@ func TestStaleIterator(t *testing.T) {
|
|||
func testStaleIterator(t *testing.T, newIter func(db *Database, hash common.Hash) Iterator) {
|
||||
config := &Config{
|
||||
WriteBufferSize: 16 * 1024 * 1024,
|
||||
NoAsyncGeneration: true,
|
||||
}
|
||||
db := New(rawdb.NewMemoryDatabase(), config, false)
|
||||
db.waitGeneration()
|
||||
|
||||
// [02 (disk), 03]
|
||||
db.Update(common.HexToHash("0x02"), types.EmptyRootHash, 1, trienode.NewMergedNodeSet(),
|
||||
|
|
@ -970,10 +975,9 @@ func BenchmarkAccountIteratorTraversal(b *testing.B) {
|
|||
return accounts
|
||||
}
|
||||
config := &Config{
|
||||
WriteBufferSize: 0,
|
||||
NoAsyncGeneration: true,
|
||||
}
|
||||
db := New(rawdb.NewMemoryDatabase(), config, false)
|
||||
db.waitGeneration()
|
||||
|
||||
for i := 1; i <= 100; i++ {
|
||||
parent := types.EmptyRootHash
|
||||
|
|
@ -1065,10 +1069,9 @@ func BenchmarkAccountIteratorLargeBaselayer(b *testing.B) {
|
|||
return accounts
|
||||
}
|
||||
config := &Config{
|
||||
WriteBufferSize: 0,
|
||||
NoAsyncGeneration: true,
|
||||
}
|
||||
db := New(rawdb.NewMemoryDatabase(), config, false)
|
||||
db.waitGeneration()
|
||||
|
||||
db.Update(common.HexToHash("0x02"), types.EmptyRootHash, 1, trienode.NewMergedNodeSet(), NewStateSetWithOrigin(makeAccounts(2000), nil, nil, nil, false))
|
||||
for i := 2; i <= 100; i++ {
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ func (db *Database) loadLayers() layer {
|
|||
log.Info("Failed to load journal, discard it", "err", err)
|
||||
}
|
||||
// Return single layer with persistent state.
|
||||
return newDiskLayer(root, rawdb.ReadPersistentStateID(db.diskdb), db, nil, nil, newBuffer(db.config.WriteBufferSize, nil, nil, 0))
|
||||
return newDiskLayer(root, rawdb.ReadPersistentStateID(db.diskdb), db, nil, nil, newBuffer(db.config.WriteBufferSize, nil, nil, 0), nil)
|
||||
}
|
||||
|
||||
// loadDiskLayer reads the binary blob from the layer journal, reconstructing
|
||||
|
|
@ -192,7 +192,7 @@ func (db *Database) loadDiskLayer(r *rlp.Stream) (layer, error) {
|
|||
if err := states.decode(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newDiskLayer(root, id, db, nil, nil, newBuffer(db.config.WriteBufferSize, &nodes, &states, id-stored)), nil
|
||||
return newDiskLayer(root, id, db, nil, nil, newBuffer(db.config.WriteBufferSize, &nodes, &states, id-stored), nil), nil
|
||||
}
|
||||
|
||||
// loadDiffLayer reads the next sections of a layer journal, reconstructing a new
|
||||
|
|
@ -301,9 +301,10 @@ func (db *Database) Journal(root common.Hash) error {
|
|||
} else { // disk layer only on noop runs (likely) or deep reorgs (unlikely)
|
||||
log.Info("Persisting dirty state to disk", "root", root, "layers", disk.buffer.layers)
|
||||
}
|
||||
// Terminate the background state generation if it's active
|
||||
if disk.generator != nil {
|
||||
disk.generator.stop()
|
||||
// Block until the background flushing is finished and terminate
|
||||
// the potential active state generator.
|
||||
if err := disk.terminate(); err != nil {
|
||||
return err
|
||||
}
|
||||
start := time.Now()
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import (
|
|||
|
||||
func newTestLayerTree() *layerTree {
|
||||
db := New(rawdb.NewMemoryDatabase(), nil, false)
|
||||
l := newDiskLayer(common.Hash{0x1}, 0, db, nil, nil, newBuffer(0, nil, nil, 0))
|
||||
l := newDiskLayer(common.Hash{0x1}, 0, db, nil, nil, newBuffer(0, nil, nil, 0), nil)
|
||||
t := newLayerTree(l)
|
||||
return t
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,8 +73,14 @@ var (
|
|||
historyDataBytesMeter = metrics.NewRegisteredMeter("pathdb/history/bytes/data", nil)
|
||||
historyIndexBytesMeter = metrics.NewRegisteredMeter("pathdb/history/bytes/index", nil)
|
||||
|
||||
indexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/index/time", nil)
|
||||
unindexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/unindex/time", nil)
|
||||
|
||||
lookupAddLayerTimer = metrics.NewRegisteredResettingTimer("pathdb/lookup/add/time", nil)
|
||||
lookupRemoveLayerTimer = metrics.NewRegisteredResettingTimer("pathdb/lookup/remove/time", nil)
|
||||
|
||||
historicalAccountReadTimer = metrics.NewRegisteredResettingTimer("pathdb/history/account/reads", nil)
|
||||
historicalStorageReadTimer = metrics.NewRegisteredResettingTimer("pathdb/history/storage/reads", nil)
|
||||
)
|
||||
|
||||
// Metrics in generation
|
||||
|
|
|
|||
|
|
@ -19,10 +19,13 @@ package pathdb
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/ethereum/go-ethereum/triedb/database"
|
||||
|
|
@ -192,3 +195,122 @@ func (db *Database) StateReader(root common.Hash) (database.StateReader, error)
|
|||
layer: layer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HistoricalStateReader is a wrapper over history reader, providing access to
|
||||
// historical state.
|
||||
type HistoricalStateReader struct {
|
||||
db *Database
|
||||
reader *historyReader
|
||||
id uint64
|
||||
}
|
||||
|
||||
// HistoricReader constructs a reader for accessing the requested historic state.
|
||||
func (db *Database) HistoricReader(root common.Hash) (*HistoricalStateReader, error) {
|
||||
// Bail out if the state history hasn't been fully indexed
|
||||
if db.indexer == nil || !db.indexer.inited() {
|
||||
return nil, errors.New("state histories haven't been fully indexed yet")
|
||||
}
|
||||
if db.freezer == nil {
|
||||
return nil, errors.New("state histories are not available")
|
||||
}
|
||||
// States at the current disk layer or above are directly accessible via
|
||||
// db.StateReader.
|
||||
//
|
||||
// States older than the current disk layer (including the disk layer
|
||||
// itself) are available through historic state access.
|
||||
//
|
||||
// Note: the requested state may refer to a stale historic state that has
|
||||
// already been pruned. This function does not validate availability, as
|
||||
// underlying states may be pruned dynamically. Validity is checked during
|
||||
// each actual state retrieval.
|
||||
id := rawdb.ReadStateID(db.diskdb, root)
|
||||
if id == nil {
|
||||
return nil, fmt.Errorf("state %#x is not available", root)
|
||||
}
|
||||
return &HistoricalStateReader{
|
||||
id: *id,
|
||||
db: db,
|
||||
reader: newHistoryReader(db.diskdb, db.freezer),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AccountRLP directly retrieves the account RLP associated with a particular
|
||||
// address in the slim data format. An error will be returned if the read
|
||||
// operation exits abnormally. Specifically, if the layer is already stale.
|
||||
//
|
||||
// Note:
|
||||
// - the returned account is not a copy, please don't modify it.
|
||||
// - no error will be returned if the requested account is not found in database.
|
||||
func (r *HistoricalStateReader) AccountRLP(address common.Address) ([]byte, error) {
|
||||
defer func(start time.Time) {
|
||||
historicalAccountReadTimer.UpdateSince(start)
|
||||
}(time.Now())
|
||||
|
||||
// TODO(rjl493456442): Theoretically, the obtained disk layer could become stale
|
||||
// within a very short time window.
|
||||
//
|
||||
// While reading the account data while holding `db.tree.lock` can resolve
|
||||
// this issue, but it will introduce a heavy contention over the lock.
|
||||
//
|
||||
// Let's optimistically assume the situation is very unlikely to happen,
|
||||
// and try to define a low granularity lock if the current approach doesn't
|
||||
// work later.
|
||||
dl := r.db.tree.bottom()
|
||||
hash := crypto.Keccak256Hash(address.Bytes())
|
||||
latest, err := dl.account(hash, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.reader.read(newAccountIdentQuery(address, hash), r.id, dl.stateID(), latest)
|
||||
}
|
||||
|
||||
// Account directly retrieves the account associated with a particular address in
|
||||
// the slim data format. An error will be returned if the read operation exits
|
||||
// abnormally. Specifically, if the layer is already stale.
|
||||
//
|
||||
// No error will be returned if the requested account is not found in database
|
||||
func (r *HistoricalStateReader) Account(address common.Address) (*types.SlimAccount, error) {
|
||||
blob, err := r.AccountRLP(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(blob) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
account := new(types.SlimAccount)
|
||||
if err := rlp.DecodeBytes(blob, account); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
// Storage directly retrieves the storage data associated with a particular key,
|
||||
// within a particular account. An error will be returned if the read operation
|
||||
// exits abnormally. Specifically, if the layer is already stale.
|
||||
//
|
||||
// Note:
|
||||
// - the returned storage data is not a copy, please don't modify it.
|
||||
// - no error will be returned if the requested slot is not found in database.
|
||||
func (r *HistoricalStateReader) Storage(address common.Address, key common.Hash) ([]byte, error) {
|
||||
defer func(start time.Time) {
|
||||
historicalStorageReadTimer.UpdateSince(start)
|
||||
}(time.Now())
|
||||
|
||||
// TODO(rjl493456442): Theoretically, the obtained disk layer could become stale
|
||||
// within a very short time window.
|
||||
//
|
||||
// While reading the account data while holding `db.tree.lock` can resolve
|
||||
// this issue, but it will introduce a heavy contention over the lock.
|
||||
//
|
||||
// Let's optimistically assume the situation is very unlikely to happen,
|
||||
// and try to define a low granularity lock if the current approach doesn't
|
||||
// work later.
|
||||
dl := r.db.tree.bottom()
|
||||
addrHash := crypto.Keccak256Hash(address.Bytes())
|
||||
keyHash := crypto.Keccak256Hash(key.Bytes())
|
||||
latest, err := dl.storage(addrHash, keyHash, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.reader.read(newStorageIdentQuery(address, addrHash, key, keyHash), r.id, dl.stateID(), latest)
|
||||
}
|
||||
|
|
|
|||
355
triedb/pathdb/verifier.go
Normal file
355
triedb/pathdb/verifier.go
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package pathdb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
)
|
||||
|
||||
// trieKV represents a trie key-value pair
|
||||
type trieKV struct {
|
||||
key common.Hash
|
||||
value []byte
|
||||
}
|
||||
|
||||
type (
|
||||
// trieHasherFn is the interface of trie hasher which can be implemented
|
||||
// by different trie algorithm.
|
||||
trieHasherFn func(in chan trieKV, out chan common.Hash)
|
||||
|
||||
// leafCallbackFn is the callback invoked at the leaves of the trie,
|
||||
// returns the subtrie root with the specified subtrie identifier.
|
||||
leafCallbackFn func(accountHash, codeHash common.Hash, stat *generateStats) (common.Hash, error)
|
||||
)
|
||||
|
||||
// VerifyState traverses the flat states specified by the given state root and
|
||||
// ensures they are matched with each other.
|
||||
func (db *Database) VerifyState(root common.Hash) error {
|
||||
acctIt, err := db.AccountIterator(root, common.Hash{})
|
||||
if err != nil {
|
||||
return err // The required snapshot might not exist.
|
||||
}
|
||||
defer acctIt.Release()
|
||||
|
||||
got, err := generateTrieRoot(acctIt, common.Hash{}, stackTrieHasher, func(accountHash, codeHash common.Hash, stat *generateStats) (common.Hash, error) {
|
||||
// Migrate the code first, commit the contract code into the tmp db.
|
||||
if codeHash != types.EmptyCodeHash {
|
||||
code := rawdb.ReadCode(db.diskdb, codeHash)
|
||||
if len(code) == 0 {
|
||||
return common.Hash{}, errors.New("failed to read contract code")
|
||||
}
|
||||
}
|
||||
// Then migrate all storage trie nodes into the tmp db.
|
||||
storageIt, err := db.StorageIterator(root, accountHash, common.Hash{})
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
defer storageIt.Release()
|
||||
|
||||
hash, err := generateTrieRoot(storageIt, accountHash, stackTrieHasher, nil, stat, false)
|
||||
if err != nil {
|
||||
return common.Hash{}, err
|
||||
}
|
||||
return hash, nil
|
||||
}, newGenerateStats(), true)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if got != root {
|
||||
return fmt.Errorf("state root hash mismatch: got %x, want %x", got, root)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateStats is a collection of statistics gathered by the trie generator
|
||||
// for logging purposes.
|
||||
type generateStats struct {
|
||||
head common.Hash
|
||||
start time.Time
|
||||
|
||||
accounts uint64 // Number of accounts done (including those being crawled)
|
||||
slots uint64 // Number of storage slots done (including those being crawled)
|
||||
|
||||
slotsStart map[common.Hash]time.Time // Start time for account slot crawling
|
||||
slotsHead map[common.Hash]common.Hash // Slot head for accounts being crawled
|
||||
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// newGenerateStats creates a new generator stats.
|
||||
func newGenerateStats() *generateStats {
|
||||
return &generateStats{
|
||||
slotsStart: make(map[common.Hash]time.Time),
|
||||
slotsHead: make(map[common.Hash]common.Hash),
|
||||
start: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// progressAccounts updates the generator stats for the account range.
|
||||
func (stat *generateStats) progressAccounts(account common.Hash, done uint64) {
|
||||
stat.lock.Lock()
|
||||
defer stat.lock.Unlock()
|
||||
|
||||
stat.accounts += done
|
||||
stat.head = account
|
||||
}
|
||||
|
||||
// finishAccounts updates the generator stats for the finished account range.
|
||||
func (stat *generateStats) finishAccounts(done uint64) {
|
||||
stat.lock.Lock()
|
||||
defer stat.lock.Unlock()
|
||||
|
||||
stat.accounts += done
|
||||
}
|
||||
|
||||
// progressContract updates the generator stats for a specific in-progress contract.
|
||||
func (stat *generateStats) progressContract(account common.Hash, slot common.Hash, done uint64) {
|
||||
stat.lock.Lock()
|
||||
defer stat.lock.Unlock()
|
||||
|
||||
stat.slots += done
|
||||
stat.slotsHead[account] = slot
|
||||
if _, ok := stat.slotsStart[account]; !ok {
|
||||
stat.slotsStart[account] = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// finishContract updates the generator stats for a specific just-finished contract.
|
||||
func (stat *generateStats) finishContract(account common.Hash, done uint64) {
|
||||
stat.lock.Lock()
|
||||
defer stat.lock.Unlock()
|
||||
|
||||
stat.slots += done
|
||||
delete(stat.slotsHead, account)
|
||||
delete(stat.slotsStart, account)
|
||||
}
|
||||
|
||||
// report prints the cumulative progress statistic smartly.
|
||||
func (stat *generateStats) report() {
|
||||
stat.lock.RLock()
|
||||
defer stat.lock.RUnlock()
|
||||
|
||||
ctx := []interface{}{
|
||||
"accounts", stat.accounts,
|
||||
"slots", stat.slots,
|
||||
"elapsed", common.PrettyDuration(time.Since(stat.start)),
|
||||
}
|
||||
if stat.accounts > 0 {
|
||||
// If there's progress on the account trie, estimate the time to finish crawling it
|
||||
if done := binary.BigEndian.Uint64(stat.head[:8]) / stat.accounts; done > 0 {
|
||||
var (
|
||||
left = (math.MaxUint64 - binary.BigEndian.Uint64(stat.head[:8])) / stat.accounts
|
||||
speed = done/uint64(time.Since(stat.start)/time.Millisecond+1) + 1 // +1s to avoid division by zero
|
||||
eta = time.Duration(left/speed) * time.Millisecond
|
||||
)
|
||||
// If there are large contract crawls in progress, estimate their finish time
|
||||
for acc, head := range stat.slotsHead {
|
||||
start := stat.slotsStart[acc]
|
||||
if done := binary.BigEndian.Uint64(head[:8]); done > 0 {
|
||||
var (
|
||||
left = math.MaxUint64 - binary.BigEndian.Uint64(head[:8])
|
||||
speed = done/uint64(time.Since(start)/time.Millisecond+1) + 1 // +1s to avoid division by zero
|
||||
)
|
||||
// Override the ETA if larger than the largest until now
|
||||
if slotETA := time.Duration(left/speed) * time.Millisecond; eta < slotETA {
|
||||
eta = slotETA
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx = append(ctx, []interface{}{
|
||||
"eta", common.PrettyDuration(eta),
|
||||
}...)
|
||||
}
|
||||
}
|
||||
log.Info("Iterating state snapshot", ctx...)
|
||||
}
|
||||
|
||||
// reportDone prints the last log when the whole generation is finished.
|
||||
func (stat *generateStats) reportDone() {
|
||||
stat.lock.RLock()
|
||||
defer stat.lock.RUnlock()
|
||||
|
||||
var ctx []interface{}
|
||||
ctx = append(ctx, []interface{}{"accounts", stat.accounts}...)
|
||||
if stat.slots != 0 {
|
||||
ctx = append(ctx, []interface{}{"slots", stat.slots}...)
|
||||
}
|
||||
ctx = append(ctx, []interface{}{"elapsed", common.PrettyDuration(time.Since(stat.start))}...)
|
||||
log.Info("Iterated snapshot", ctx...)
|
||||
}
|
||||
|
||||
// runReport periodically prints the progress information.
|
||||
func runReport(stats *generateStats, stop chan bool) {
|
||||
timer := time.NewTimer(0)
|
||||
defer timer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
stats.report()
|
||||
timer.Reset(time.Second * 8)
|
||||
case success := <-stop:
|
||||
if success {
|
||||
stats.reportDone()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generateTrieRoot generates the trie hash based on the snapshot iterator.
|
||||
// It can be used for generating account trie, storage trie or even the
|
||||
// whole state which connects the accounts and the corresponding storages.
|
||||
func generateTrieRoot(it Iterator, account common.Hash, generatorFn trieHasherFn, leafCallback leafCallbackFn, stats *generateStats, report bool) (common.Hash, error) {
|
||||
var (
|
||||
in = make(chan trieKV) // chan to pass leaves
|
||||
out = make(chan common.Hash, 1) // chan to collect result
|
||||
stoplog = make(chan bool, 1) // 1-size buffer, works when logging is not enabled
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
// Spin up a go-routine for trie hash re-generation
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
generatorFn(in, out)
|
||||
}()
|
||||
// Spin up a go-routine for progress logging
|
||||
if report && stats != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
runReport(stats, stoplog)
|
||||
}()
|
||||
}
|
||||
// Create a semaphore to assign tasks and collect results through. We'll pre-
|
||||
// fill it with nils, thus using the same channel for both limiting concurrent
|
||||
// processing and gathering results.
|
||||
threads := runtime.NumCPU()
|
||||
results := make(chan error, threads)
|
||||
for i := 0; i < threads; i++ {
|
||||
results <- nil // fill the semaphore
|
||||
}
|
||||
// stop is a helper function to shutdown the background threads
|
||||
// and return the re-generated trie hash.
|
||||
stop := func(fail error) (common.Hash, error) {
|
||||
close(in)
|
||||
result := <-out
|
||||
for i := 0; i < threads; i++ {
|
||||
if err := <-results; err != nil && fail == nil {
|
||||
fail = err
|
||||
}
|
||||
}
|
||||
stoplog <- fail == nil
|
||||
|
||||
wg.Wait()
|
||||
return result, fail
|
||||
}
|
||||
var (
|
||||
logged = time.Now()
|
||||
processed = uint64(0)
|
||||
leaf trieKV
|
||||
)
|
||||
// Start to feed leaves
|
||||
for it.Next() {
|
||||
if account == (common.Hash{}) {
|
||||
var (
|
||||
err error
|
||||
fullData []byte
|
||||
)
|
||||
if leafCallback == nil {
|
||||
fullData, err = types.FullAccountRLP(it.(AccountIterator).Account())
|
||||
if err != nil {
|
||||
return stop(err)
|
||||
}
|
||||
} else {
|
||||
// Wait until the semaphore allows us to continue, aborting if
|
||||
// a sub-task failed
|
||||
if err := <-results; err != nil {
|
||||
results <- nil // stop will drain the results, add a noop back for this error we just consumed
|
||||
return stop(err)
|
||||
}
|
||||
// Fetch the next account and process it concurrently
|
||||
account, err := types.FullAccount(it.(AccountIterator).Account())
|
||||
if err != nil {
|
||||
return stop(err)
|
||||
}
|
||||
go func(hash common.Hash) {
|
||||
subroot, err := leafCallback(hash, common.BytesToHash(account.CodeHash), stats)
|
||||
if err != nil {
|
||||
results <- err
|
||||
return
|
||||
}
|
||||
if account.Root != subroot {
|
||||
results <- fmt.Errorf("invalid subroot(path %x), want %x, have %x", hash, account.Root, subroot)
|
||||
return
|
||||
}
|
||||
results <- nil
|
||||
}(it.Hash())
|
||||
fullData, err = rlp.EncodeToBytes(account)
|
||||
if err != nil {
|
||||
return stop(err)
|
||||
}
|
||||
}
|
||||
leaf = trieKV{it.Hash(), fullData}
|
||||
} else {
|
||||
leaf = trieKV{it.Hash(), common.CopyBytes(it.(StorageIterator).Slot())}
|
||||
}
|
||||
in <- leaf
|
||||
|
||||
// Accumulate the generation statistic if it's required.
|
||||
processed++
|
||||
if time.Since(logged) > 3*time.Second && stats != nil {
|
||||
if account == (common.Hash{}) {
|
||||
stats.progressAccounts(it.Hash(), processed)
|
||||
} else {
|
||||
stats.progressContract(account, it.Hash(), processed)
|
||||
}
|
||||
logged, processed = time.Now(), 0
|
||||
}
|
||||
}
|
||||
// Commit the last part statistic.
|
||||
if processed > 0 && stats != nil {
|
||||
if account == (common.Hash{}) {
|
||||
stats.finishAccounts(processed)
|
||||
} else {
|
||||
stats.finishContract(account, processed)
|
||||
}
|
||||
}
|
||||
return stop(nil)
|
||||
}
|
||||
|
||||
func stackTrieHasher(in chan trieKV, out chan common.Hash) {
|
||||
t := trie.NewStackTrie(nil)
|
||||
for leaf := range in {
|
||||
t.Update(leaf.key[:], leaf.value)
|
||||
}
|
||||
out <- t.Hash()
|
||||
}
|
||||
Loading…
Reference in a new issue