From 4fc93431c84f1d6966e3be988328973993d957c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Tue, 6 Aug 2019 13:40:28 +0300 Subject: [PATCH 1/5] state-snapshots-v1 --- README.md | 10 +- accounts/abi/bind/backends/simulated.go | 6 +- cmd/evm/runner.go | 4 +- cmd/geth/chaincmd.go | 2 +- core/blockchain.go | 60 ++-- core/chain_makers.go | 2 +- core/genesis.go | 4 +- core/rawdb/accessors_snapshot.go | 93 ++++++ core/rawdb/database.go | 8 + core/rawdb/schema.go | 18 +- core/state/snapshot/account.go | 54 ++++ core/state/snapshot/difflayer.go | 376 ++++++++++++++++++++++++ core/state/snapshot/disklayer.go | 83 ++++++ core/state/snapshot/generate.go | 197 +++++++++++++ core/state/snapshot/generate_test.go | 111 +++++++ core/state/snapshot/snapshot.go | 225 ++++++++++++++ core/state/snapshot/snapshot_test.go | 17 ++ core/state/snapshot/sort.go | 62 ++++ core/state/state_object.go | 33 ++- core/state/statedb.go | 93 +++++- core/vm/runtime/runtime.go | 4 +- core/vm/runtime/runtime_test.go | 4 +- eth/api_test.go | 6 +- eth/api_tracer.go | 6 +- eth/handler_test.go | 2 +- light/odr_test.go | 4 +- light/trie.go | 2 +- tests/state_test_util.go | 4 +- trie/iterator.go | 2 + 29 files changed, 1418 insertions(+), 74 deletions(-) create mode 100644 core/rawdb/accessors_snapshot.go create mode 100644 core/state/snapshot/account.go create mode 100644 core/state/snapshot/difflayer.go create mode 100644 core/state/snapshot/disklayer.go create mode 100644 core/state/snapshot/generate.go create mode 100644 core/state/snapshot/generate_test.go create mode 100644 core/state/snapshot/snapshot.go create mode 100644 core/state/snapshot/snapshot_test.go create mode 100644 core/state/snapshot/sort.go diff --git a/README.md b/README.md index fd25941543b9..29f473269f69 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Specifying the `--testnet` flag, however, will reconfigure your `geth` instance this. * Instead of connecting the main Ethereum network, the client will connect to the test network, which uses different P2P bootnodes, different network IDs and genesis states. - + *Note: Although there are some internal protective measures to prevent transactions from crossing over between the main network and test network, you should make sure to always use separate accounts for play-money and real-money. Unless you manually move @@ -333,6 +333,14 @@ Please see the [Developers' Guide](https://github.com/ethereum/go-ethereum/wiki/ for more details on configuring your environment, managing project dependencies, and testing procedures. +## Releases + +Instead of batching together features for a new release, or back-porting fixes into an old one, Geth employs a rolling release. + +We strive to push a new - backwards compatible - minor version every other week, irrelevant of what's been merged. This ensures that both fixes and features are shipped fast (good for users); and that there's always only one canonical version to maintain (good for devs). + +Every now and again we make drastic or breaking changes to the code, which results in a deflection from the regular release schedule until the new version is deemed stable enough. Such releases are tagged with a new major version number to + ## License The go-ethereum library (i.e. all code outside of the `cmd` directory) is licensed under the diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go index e30572e0a111..3676c076df8f 100644 --- a/accounts/abi/bind/backends/simulated.go +++ b/accounts/abi/bind/backends/simulated.go @@ -119,7 +119,7 @@ func (b *SimulatedBackend) rollback() { statedb, _ := b.blockchain.State() b.pendingBlock = blocks[0] - b.pendingState, _ = state.New(b.pendingBlock.Root(), statedb.Database()) + b.pendingState, _ = state.New(b.pendingBlock.Root(), statedb.Database(), nil) } // CodeAt returns the code associated with a certain account in the blockchain. @@ -347,7 +347,7 @@ func (b *SimulatedBackend) SendTransaction(ctx context.Context, tx *types.Transa statedb, _ := b.blockchain.State() b.pendingBlock = blocks[0] - b.pendingState, _ = state.New(b.pendingBlock.Root(), statedb.Database()) + b.pendingState, _ = state.New(b.pendingBlock.Root(), statedb.Database(), nil) return nil } @@ -432,7 +432,7 @@ func (b *SimulatedBackend) AdjustTime(adjustment time.Duration) error { statedb, _ := b.blockchain.State() b.pendingBlock = blocks[0] - b.pendingState, _ = state.New(b.pendingBlock.Root(), statedb.Database()) + b.pendingState, _ = state.New(b.pendingBlock.Root(), statedb.Database(), nil) return nil } diff --git a/cmd/evm/runner.go b/cmd/evm/runner.go index 318aa222a33a..9a9fee2db8c7 100644 --- a/cmd/evm/runner.go +++ b/cmd/evm/runner.go @@ -100,10 +100,10 @@ func runCmd(ctx *cli.Context) error { genesisConfig = gen db := rawdb.NewMemoryDatabase() genesis := gen.ToBlock(db) - statedb, _ = state.New(genesis.Root(), state.NewDatabase(db)) + statedb, _ = state.New(genesis.Root(), state.NewDatabase(db), nil) chainConfig = gen.Config } else { - statedb, _ = state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase())) + statedb, _ = state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) genesisConfig = new(core.Genesis) } if ctx.GlobalString(SenderFlag.Name) != "" { diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 49e6a0594933..103af55e4090 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -521,7 +521,7 @@ func dump(ctx *cli.Context) error { fmt.Println("{}") utils.Fatalf("block not found") } else { - state, err := state.New(block.Root(), state.NewDatabase(chainDb)) + state, err := state.New(block.Root(), state.NewDatabase(chainDb), nil) if err != nil { utils.Fatalf("could not create new state: %v", err) } diff --git a/core/blockchain.go b/core/blockchain.go index 59be3558950a..f39b288724eb 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/ethdb" @@ -50,15 +51,17 @@ var ( headHeaderGauge = metrics.NewRegisteredGauge("chain/head/header", nil) headFastBlockGauge = metrics.NewRegisteredGauge("chain/head/receipt", nil) - accountReadTimer = metrics.NewRegisteredTimer("chain/account/reads", nil) - accountHashTimer = metrics.NewRegisteredTimer("chain/account/hashes", nil) - accountUpdateTimer = metrics.NewRegisteredTimer("chain/account/updates", nil) - accountCommitTimer = metrics.NewRegisteredTimer("chain/account/commits", nil) + accountSnapReadTimer = metrics.NewRegisteredTimer("chain/account/snapreads", nil) + accountTrieReadTimer = metrics.NewRegisteredTimer("chain/account/triereads", nil) + accountHashTimer = metrics.NewRegisteredTimer("chain/account/hashes", nil) + accountUpdateTimer = metrics.NewRegisteredTimer("chain/account/updates", nil) + accountCommitTimer = metrics.NewRegisteredTimer("chain/account/commits", nil) - storageReadTimer = metrics.NewRegisteredTimer("chain/storage/reads", nil) - storageHashTimer = metrics.NewRegisteredTimer("chain/storage/hashes", nil) - storageUpdateTimer = metrics.NewRegisteredTimer("chain/storage/updates", nil) - storageCommitTimer = metrics.NewRegisteredTimer("chain/storage/commits", nil) + storageSnapReadTimer = metrics.NewRegisteredTimer("chain/storage/snapreads", nil) + storageTrieReadTimer = metrics.NewRegisteredTimer("chain/storage/triereads", nil) + storageHashTimer = metrics.NewRegisteredTimer("chain/storage/hashes", nil) + storageUpdateTimer = metrics.NewRegisteredTimer("chain/storage/updates", nil) + storageCommitTimer = metrics.NewRegisteredTimer("chain/storage/commits", nil) blockInsertTimer = metrics.NewRegisteredTimer("chain/inserts", nil) blockValidationTimer = metrics.NewRegisteredTimer("chain/validation", nil) @@ -131,9 +134,10 @@ type BlockChain struct { chainConfig *params.ChainConfig // Chain & network configuration cacheConfig *CacheConfig // Cache configuration for pruning - db ethdb.Database // Low level persistent database to store final content in - triegc *prque.Prque // Priority queue mapping block numbers to tries to gc - gcproc time.Duration // Accumulates canonical block processing for trie dumping + db ethdb.Database // Low level persistent database to store final content in + snaps *snapshot.SnapshotTree // Snapshot tree for fast trie leaf access + triegc *prque.Prque // Priority queue mapping block numbers to tries to gc + gcproc time.Duration // Accumulates canonical block processing for trie dumping hc *HeaderChain rmLogsFeed event.Feed @@ -280,6 +284,13 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par } } } + // Load any existing snapshot, regenerating it if loading failed + head := bc.CurrentBlock() + if bc.snaps, err = snapshot.New(bc.db, "snapshot.rlp", head.NumberU64(), head.Root()); err != nil { + return nil, err + } + fmt.Println(bc.snaps) + // Take ownership of this particular state go bc.update() return bc, nil @@ -326,7 +337,7 @@ func (bc *BlockChain) loadLastState() error { return bc.Reset() } // Make sure the state associated with the block is available - if _, err := state.New(currentBlock.Root(), bc.stateCache); err != nil { + if _, err := state.New(currentBlock.Root(), bc.stateCache, bc.snaps); err != nil { // Dangling block without a state associated, init from scratch log.Warn("Head state missing, repairing chain", "number", currentBlock.Number(), "hash", currentBlock.Hash()) if err := bc.repair(¤tBlock); err != nil { @@ -388,7 +399,7 @@ func (bc *BlockChain) SetHead(head uint64) error { if newHeadBlock == nil { newHeadBlock = bc.genesisBlock } else { - if _, err := state.New(newHeadBlock.Root(), bc.stateCache); err != nil { + if _, err := state.New(newHeadBlock.Root(), bc.stateCache, bc.snaps); err != nil { // Rewound state missing, rolled back to before pivot, reset to genesis newHeadBlock = bc.genesisBlock } @@ -500,7 +511,7 @@ func (bc *BlockChain) State() (*state.StateDB, error) { // StateAt returns a new mutable state based on a particular point in time. func (bc *BlockChain) StateAt(root common.Hash) (*state.StateDB, error) { - return state.New(root, bc.stateCache) + return state.New(root, bc.stateCache, bc.snaps) } // StateCache returns the caching database underpinning the blockchain instance. @@ -551,7 +562,7 @@ func (bc *BlockChain) ResetWithGenesisBlock(genesis *types.Block) error { func (bc *BlockChain) repair(head **types.Block) error { for { // Abort if we've rewound to a head block that does have associated state - if _, err := state.New((*head).Root(), bc.stateCache); err == nil { + if _, err := state.New((*head).Root(), bc.stateCache, bc.snaps); err == nil { log.Info("Rewound blockchain to past state", "number", (*head).Number(), "hash", (*head).Hash()) return nil } @@ -1047,7 +1058,6 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [ } // Don't collect too much in-memory, write it out every 100K blocks if len(deleted) > 100000 { - // Sync the ancient store explicitly to ensure all data has been flushed to disk. if err := bc.db.Sync(); err != nil { return 0, err @@ -1590,7 +1600,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, [] if parent == nil { parent = bc.GetHeader(block.ParentHash(), block.NumberU64()-1) } - statedb, err := state.New(parent.Root, bc.stateCache) + statedb, err := state.New(parent.Root, bc.stateCache, bc.snaps) if err != nil { return it.index, events, coalescedLogs, err } @@ -1601,7 +1611,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, [] if !bc.cacheConfig.TrieCleanNoPrefetch { if followup, err := it.peek(); followup != nil && err == nil { go func(start time.Time) { - throwaway, _ := state.New(parent.Root, bc.stateCache) + throwaway, _ := state.New(parent.Root, bc.stateCache, bc.snaps) bc.prefetcher.Prefetch(followup, throwaway, bc.vmConfig, &followupInterrupt) blockPrefetchExecuteTimer.Update(time.Since(start)) @@ -1620,14 +1630,16 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, [] return it.index, events, coalescedLogs, err } // Update the metrics touched during block processing - accountReadTimer.Update(statedb.AccountReads) // Account reads are complete, we can mark them - storageReadTimer.Update(statedb.StorageReads) // Storage reads are complete, we can mark them - accountUpdateTimer.Update(statedb.AccountUpdates) // Account updates are complete, we can mark them - storageUpdateTimer.Update(statedb.StorageUpdates) // Storage updates are complete, we can mark them + accountSnapReadTimer.Update(statedb.AccountSnapReads) // Account reads are complete, we can mark them + accountTrieReadTimer.Update(statedb.AccountTrieReads) // Account reads are complete, we can mark them + storageSnapReadTimer.Update(statedb.StorageSnapReads) // Storage reads are complete, we can mark them + storageTrieReadTimer.Update(statedb.StorageTrieReads) // Storage reads are complete, we can mark them + accountUpdateTimer.Update(statedb.AccountUpdates) // Account updates are complete, we can mark them + storageUpdateTimer.Update(statedb.StorageUpdates) // Storage updates are complete, we can mark them triehash := statedb.AccountHashes + statedb.StorageHashes // Save to not double count in validation - trieproc := statedb.AccountReads + statedb.AccountUpdates - trieproc += statedb.StorageReads + statedb.StorageUpdates + trieproc := statedb.AccountSnapReads + statedb.AccountTrieReads + statedb.AccountUpdates + trieproc += statedb.StorageSnapReads + statedb.StorageTrieReads + statedb.StorageUpdates blockExecutionTimer.Update(time.Since(substart) - trieproc - triehash) diff --git a/core/chain_makers.go b/core/chain_makers.go index 17f4042116fa..e74ab290c307 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -228,7 +228,7 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse return nil, nil } for i := 0; i < n; i++ { - statedb, err := state.New(parent.Root(), state.NewDatabase(db)) + statedb, err := state.New(parent.Root(), state.NewDatabase(db), nil) if err != nil { panic(err) } diff --git a/core/genesis.go b/core/genesis.go index 87bab2520159..c49ec57d1e8a 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -173,7 +173,7 @@ func SetupGenesisBlock(db ethdb.Database, genesis *Genesis) (*params.ChainConfig // We have the genesis block in database(perhaps in ancient database) // but the corresponding state is missing. header := rawdb.ReadHeader(db, stored, 0) - if _, err := state.New(header.Root, state.NewDatabaseWithCache(db, 0)); err != nil { + if _, err := state.New(header.Root, state.NewDatabaseWithCache(db, 0), nil); err != nil { if genesis == nil { genesis = DefaultGenesisBlock() } @@ -245,7 +245,7 @@ func (g *Genesis) ToBlock(db ethdb.Database) *types.Block { if db == nil { db = rawdb.NewMemoryDatabase() } - statedb, _ := state.New(common.Hash{}, state.NewDatabase(db)) + statedb, _ := state.New(common.Hash{}, state.NewDatabase(db), nil) for addr, account := range g.Alloc { statedb.AddBalance(addr, account.Balance) statedb.SetCode(addr, account.Code) diff --git a/core/rawdb/accessors_snapshot.go b/core/rawdb/accessors_snapshot.go new file mode 100644 index 000000000000..f0f3d8ed0206 --- /dev/null +++ b/core/rawdb/accessors_snapshot.go @@ -0,0 +1,93 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rawdb + +import ( + "encoding/binary" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" +) + +// ReadSnapshotBlock retrieves the number and root of the block whose state is +// contained in the persisted snapshot. +func ReadSnapshotBlock(db ethdb.KeyValueReader) (uint64, common.Hash) { + data, _ := db.Get(snapshotBlockKey) + if len(data) != 8+common.HashLength { + return 0, common.Hash{} + } + return binary.BigEndian.Uint64(data[:8]), common.BytesToHash(data[8:]) +} + +// WriteSnapshotBlock stores the number and root of the block whose state is +// contained in the persisted snapshot. +func WriteSnapshotBlock(db ethdb.KeyValueWriter, number uint64, root common.Hash) { + if err := db.Put(snapshotBlockKey, append(encodeBlockNumber(number), root.Bytes()...)); err != nil { + log.Crit("Failed to store snapsnot block's number and root", "err", err) + } +} + +// DeleteSnapshotBlock deletes the number and hash of the block whose state is +// contained in the persisted snapshot. Since snapshots are not immutable, this +// method can be used during updates, so a crash or failure will mark the entire +// snapshot invalid. +func DeleteSnapshotBlock(db ethdb.KeyValueWriter) { + if err := db.Delete(snapshotBlockKey); err != nil { + log.Crit("Failed to remove snapsnot block's number and hash", "err", err) + } +} + +// ReadAccountSnapshot retrieves the snapshot entry of an account trie leaf. +func ReadAccountSnapshot(db ethdb.KeyValueReader, hash common.Hash) []byte { + data, _ := db.Get(accountSnapshotKey(hash)) + return data +} + +// WriteAccountSnapshot stores the snapshot entry of an account trie leaf. +func WriteAccountSnapshot(db ethdb.KeyValueWriter, hash common.Hash, entry []byte) { + if err := db.Put(accountSnapshotKey(hash), entry); err != nil { + log.Crit("Failed to store account snapshot", "err", err) + } +} + +// DeleteAccountSnapshot removes the snapshot entry of an account trie leaf. +func DeleteAccountSnapshot(db ethdb.KeyValueWriter, hash common.Hash) { + if err := db.Delete(accountSnapshotKey(hash)); err != nil { + log.Crit("Failed to delete account snapshot", "err", err) + } +} + +// ReadStorageSnapshot retrieves the snapshot entry of an storage trie leaf. +func ReadStorageSnapshot(db ethdb.KeyValueReader, accountHash, storageHash common.Hash) []byte { + data, _ := db.Get(storageSnapshotKey(accountHash, storageHash)) + return data +} + +// WriteStorageSnapshot stores the snapshot entry of an storage trie leaf. +func WriteStorageSnapshot(db ethdb.KeyValueWriter, accountHash, storageHash common.Hash, entry []byte) { + if err := db.Put(storageSnapshotKey(accountHash, storageHash), entry); err != nil { + log.Crit("Failed to store storage snapshot", "err", err) + } +} + +// DeleteStorageSnapshot removes the snapshot entry of an storage trie leaf. +func DeleteStorageSnapshot(db ethdb.KeyValueWriter, accountHash, storageHash common.Hash) { + if err := db.Delete(storageSnapshotKey(accountHash, storageHash)); err != nil { + log.Crit("Failed to delete storage snapshot", "err", err) + } +} diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 353b7dce623d..3910f4b58a11 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -240,6 +240,8 @@ func InspectDatabase(db ethdb.Database) error { hashNumPairing common.StorageSize trieSize common.StorageSize txlookupSize common.StorageSize + accountSnapSize common.StorageSize + storageSnapSize common.StorageSize preimageSize common.StorageSize bloomBitsSize common.StorageSize cliqueSnapsSize common.StorageSize @@ -281,6 +283,10 @@ func InspectDatabase(db ethdb.Database) error { receiptSize += size case bytes.HasPrefix(key, txLookupPrefix) && len(key) == (len(txLookupPrefix)+common.HashLength): txlookupSize += size + case bytes.HasPrefix(key, StateSnapshotPrefix) && len(key) == (len(StateSnapshotPrefix)+common.HashLength): + accountSnapSize += size + case bytes.HasPrefix(key, StateSnapshotPrefix) && len(key) == (len(StateSnapshotPrefix)+2*common.HashLength): + storageSnapSize += size case bytes.HasPrefix(key, preimagePrefix) && len(key) == (len(preimagePrefix)+common.HashLength): preimageSize += size case bytes.HasPrefix(key, bloomBitsPrefix) && len(key) == (len(bloomBitsPrefix)+10+common.HashLength): @@ -332,6 +338,8 @@ func InspectDatabase(db ethdb.Database) error { {"Key-Value store", "Bloombit index", bloomBitsSize.String()}, {"Key-Value store", "Trie nodes", trieSize.String()}, {"Key-Value store", "Trie preimages", preimageSize.String()}, + {"Key-Value store", "Account snapshot", accountSnapSize.String()}, + {"Key-Value store", "Storage snapshot", storageSnapSize.String()}, {"Key-Value store", "Clique snapshots", cliqueSnapsSize.String()}, {"Key-Value store", "Singleton metadata", metadata.String()}, {"Ancient store", "Headers", ancientHeaders.String()}, diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index a44a2c99f94f..2e0e12a755f9 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -41,6 +41,9 @@ var ( // fastTrieProgressKey tracks the number of trie entries imported during fast sync. fastTrieProgressKey = []byte("TrieSync") + // snapshotBlockKey tracks the number and hash of the last snapshot. + snapshotBlockKey = []byte("SnapshotBlock") + // Data item prefixes (use single byte to avoid mixing data types, avoid `i`, used for indexes). headerPrefix = []byte("h") // headerPrefix + num (uint64 big endian) + hash -> header headerTDSuffix = []byte("t") // headerPrefix + num (uint64 big endian) + hash + headerTDSuffix -> td @@ -50,8 +53,9 @@ var ( blockBodyPrefix = []byte("b") // blockBodyPrefix + num (uint64 big endian) + hash -> block body blockReceiptsPrefix = []byte("r") // blockReceiptsPrefix + num (uint64 big endian) + hash -> block receipts - txLookupPrefix = []byte("l") // txLookupPrefix + hash -> transaction/receipt lookup metadata - bloomBitsPrefix = []byte("B") // bloomBitsPrefix + bit (uint16 big endian) + section (uint64 big endian) + hash -> bloom bits + txLookupPrefix = []byte("l") // txLookupPrefix + hash -> transaction/receipt lookup metadata + bloomBitsPrefix = []byte("B") // bloomBitsPrefix + bit (uint16 big endian) + section (uint64 big endian) + hash -> bloom bits + StateSnapshotPrefix = []byte("s") // StateSnapshotPrefix + account hash [+ storage hash] -> account/storage trie value preimagePrefix = []byte("secure-key-") // preimagePrefix + hash -> preimage configPrefix = []byte("ethereum-config-") // config prefix for the db @@ -145,6 +149,16 @@ func txLookupKey(hash common.Hash) []byte { return append(txLookupPrefix, hash.Bytes()...) } +// accountSnapshotKey = StateSnapshotPrefix + hash +func accountSnapshotKey(hash common.Hash) []byte { + return append(StateSnapshotPrefix, hash.Bytes()...) +} + +// storageSnapshotKey = StateSnapshotPrefix + account hash + storage hash +func storageSnapshotKey(accountHash, storageHash common.Hash) []byte { + return append(append(StateSnapshotPrefix, accountHash.Bytes()...), storageHash.Bytes()...) +} + // bloomBitsKey = bloomBitsPrefix + bit (uint16 big endian) + section (uint64 big endian) + hash func bloomBitsKey(bit uint, section uint64, hash common.Hash) []byte { key := append(append(bloomBitsPrefix, make([]byte, 10)...), hash.Bytes()...) diff --git a/core/state/snapshot/account.go b/core/state/snapshot/account.go new file mode 100644 index 000000000000..1068dc2a010a --- /dev/null +++ b/core/state/snapshot/account.go @@ -0,0 +1,54 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package snapshot + +import ( + "bytes" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" +) + +// Account is a slim version of a state.Account, where the root and code hash +// are replaced with a nil byte slice for empty accounts. +type Account struct { + Nonce uint64 + Balance *big.Int + Root []byte + CodeHash []byte +} + +// AccountRLP converts a state.Account content into a slim snapshot version RLP +// encoded. +func AccountRLP(nonce uint64, balance *big.Int, root common.Hash, codehash []byte) []byte { + slim := Account{ + Nonce: nonce, + Balance: balance, + } + if root != emptyRoot { + slim.Root = root[:] + } + if !bytes.Equal(codehash, emptyCode[:]) { + slim.CodeHash = codehash + } + data, err := rlp.EncodeToBytes(slim) + if err != nil { + panic(err) + } + return data +} diff --git a/core/state/snapshot/difflayer.go b/core/state/snapshot/difflayer.go new file mode 100644 index 000000000000..e93d9fbf70a9 --- /dev/null +++ b/core/state/snapshot/difflayer.go @@ -0,0 +1,376 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package snapshot + +import ( + "fmt" + "io" + "os" + "sort" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rlp" +) + +// diffLayer represents a collection of modifications made to a state snapshot +// after running a block on top. It contains one sorted list for the account trie +// and one-one list for each storage tries. +// +// The goal of a diff layer is to act as a journal, tracking recent modifications +// made to the state, that have not yet graduated into a semi-immutable state. +type diffLayer struct { + parent snapshot // Parent snapshot modified by this one, never nil + memory uint64 // Approximate guess as to how much memory we use + + number uint64 // Block number to which this snapshot diff belongs to + root common.Hash // Root hash to which this snapshot diff belongs to + + accountOrder []common.Hash // Sorted accounts for iterated retrieval + accountData map[common.Hash][]byte // Keyed accounts for direct retrival (nil means deleted) + storageOrder map[common.Hash][]common.Hash // Sorted storage slots for iterated retrievals. one per account + storageData map[common.Hash]map[common.Hash][]byte // Keyed storage slots for direct retrival. one per account (nil means deleted) + + lock sync.RWMutex +} + +// newDiffLayer creates a new diff on top of an existing snapshot, whether that's a low +// level persistent database or a hierarchical diff already. +func newDiffLayer(parent snapshot, root common.Hash, accounts map[common.Hash][]byte, storage map[common.Hash]map[common.Hash][]byte) *diffLayer { + // Create the new layer with some pre-allocated data segments + parentNumber, _ := parent.Info() + + dl := &diffLayer{ + parent: parent, + number: parentNumber + 1, + root: root, + accountData: accounts, + storageData: storage, + } + // Fill the account hashes and sort them for the iterator + dl.accountOrder = make([]common.Hash, 0, len(accounts)) + for hash, data := range accounts { + dl.accountOrder = append(dl.accountOrder, hash) + dl.memory += uint64(len(data)) + } + sort.Sort(hashes(dl.accountOrder)) + dl.memory += uint64(len(dl.accountOrder) * common.HashLength) + + // Fill the storage hashes and sort them for the iterator + dl.storageOrder = make(map[common.Hash][]common.Hash, len(storage)) + for accountHash, slots := range storage { + // If the slots are nil, sanity check that it's a deleted account + if slots == nil { + // Ensure that the account was just marked as deleted + if account, ok := accounts[accountHash]; account != nil || !ok { + panic(fmt.Sprintf("storage in %#x nil, but account conflicts (%#x, exists: %v)", accountHash, account, ok)) + } + // Everything ok, store the deletion mark and continue + dl.storageOrder[accountHash] = nil + continue + } + // Storage slots are not nil so entire contract was not deleted, ensure the + // account was just updated. + if account, ok := accounts[accountHash]; account == nil || !ok { + log.Error(fmt.Sprintf("storage in %#x exists, but account nil (exists: %v)", accountHash, ok)) + //panic(fmt.Sprintf("storage in %#x exists, but account nil (exists: %v)", accountHash, ok)) + } + // Fill the storage hashes for this account and sort them for the iterator + storageOrder := make([]common.Hash, 0, len(slots)) + for storageHash, data := range slots { + storageOrder = append(storageOrder, storageHash) + dl.memory += uint64(len(data)) + } + sort.Sort(hashes(storageOrder)) + dl.storageOrder[accountHash] = storageOrder + dl.memory += uint64(len(storageOrder) * common.HashLength) + } + dl.memory += uint64(len(dl.storageOrder) * common.HashLength) + + return dl +} + +// loadDiffLayer reads the next sections of a snapshot journal, reconstructing a new +// diff and verifying that it can be linked to the requested parent. +func loadDiffLayer(parent snapshot, r io.Reader) (snapshot, error) { + // Read the next diff journal entry + var ( + number uint64 + root common.Hash + accounts = make(map[common.Hash][]byte) + storage = make(map[common.Hash]map[common.Hash][]byte) + ) + if err := rlp.Decode(r, &number); err != nil { + // The first read may fail with EOF, marking the end of the journal + if err == io.EOF { + return parent, nil + } + return nil, err + } + if err := rlp.Decode(r, &root); err != nil { + return nil, err + } + if err := rlp.Decode(r, &accounts); err != nil { + return nil, err + } + if err := rlp.Decode(r, &storage); err != nil { + return nil, err + } + // Validate the block number to avoid state corruption + if parentNumber, _ := parent.Info(); number != parentNumber+1 { + return nil, fmt.Errorf("snapshot chain broken: block #%dl after #%dl", number, parentNumber) + } + return loadDiffLayer(newDiffLayer(parent, root, accounts, storage), r) +} + +// Info returns the block number and root hash for which this snapshot was made. +func (dl *diffLayer) Info() (uint64, common.Hash) { + return dl.number, dl.root +} + +// Account directly retrieves the account associated with a particular hash in +// the snapshot slim data format. +func (dl *diffLayer) Account(hash common.Hash) *Account { + data := dl.AccountRLP(hash) + if len(data) == 0 { // nil if deleted, []byte{} if never existed + return nil + } + account := new(Account) + if err := rlp.DecodeBytes(data, account); err != nil { + panic(err) + } + return account +} + +// AccountRLP directly retrieves the account RLP associated with a particular +// hash in the snapshot slim data format. +func (dl *diffLayer) AccountRLP(hash common.Hash) []byte { + dl.lock.RLock() + defer dl.lock.RUnlock() + + // If the account is known locally, return it. Note, a nil account means it was + // deleted, and is a different notion than an unknown account! + if data, ok := dl.accountData[hash]; ok { + return data + } + // Account unknown to this diff, resolve from parent + return dl.parent.AccountRLP(hash) +} + +// Storage directly retrieves the storage data associated with a particular hash, +// within a particular account. If the slot is unknown to this diff, it's parent +// is consulted. +func (dl *diffLayer) Storage(accountHash, storageHash common.Hash) []byte { + dl.lock.RLock() + defer dl.lock.RUnlock() + + // If the account is known locally, try to resolve the slot locally. Note, a nil + // account means it was deleted, and is a different notion than an unknown account! + if storage, ok := dl.storageData[accountHash]; ok { + if storage == nil { + return nil + } + if data, ok := storage[storageHash]; ok { + return data + } + } + // Account - or slot within - unknown to this diff, resolve from parent + return dl.parent.Storage(accountHash, storageHash) +} + +// Update creates a new layer on top of the existing snapshot diff tree with +// the specified data items. +func (dl *diffLayer) Update(blockRoot common.Hash, accounts map[common.Hash][]byte, storage map[common.Hash]map[common.Hash][]byte) *diffLayer { + return newDiffLayer(dl, blockRoot, accounts, storage) +} + +// Cap traverses downwards the diff tree until the number of allowed layers are +// crossed. All diffs beyond the permitted number are flattened downwards. If +// the layer limit is reached, memory cap is also enforced (but not before). The +// block numbers for the disk layer and first diff layer are returned for GC. +func (dl *diffLayer) Cap(layers int, memory uint64) (uint64, uint64) { + dl.lock.Lock() + defer dl.lock.Unlock() + + // Dive until we run out of layers or reach the persistent database + if layers > 2 { + // If we still have diff layers below, recurse + if parent, ok := dl.parent.(*diffLayer); ok { + return parent.Cap(layers-1, memory) + } + // Diff stack too shallow, return block numbers without modifications + return dl.parent.(*diskLayer).number, dl.number + } + // We're out of layers, flatten anything below, stopping if it's the disk or if + // the memory limit is not yet exceeded. + switch parent := dl.parent.(type) { + case *diskLayer: + return parent.number, dl.number + case *diffLayer: + dl.parent = parent.flatten() + if dl.parent.(*diffLayer).memory < memory { + diskNumber, _ := parent.parent.Info() + return diskNumber, parent.number + } + default: + panic(fmt.Sprintf("unknown data layer: %T", parent)) + } + // If the bottommost layer is larger than our memory cap, persist to disk + var ( + parent = dl.parent.(*diffLayer) + base = parent.parent.(*diskLayer) + batch = base.db.NewBatch() + ) + // Start by temporarilly deleting the current snapshot block marker. This + // ensures that in the case of a crash, the entire snapshot is invalidated. + rawdb.DeleteSnapshotBlock(batch) + + // Push all the accounts into the database + for hash, data := range parent.accountData { + rawdb.WriteAccountSnapshot(batch, hash, data) + if batch.ValueSize() > ethdb.IdealBatchSize { + if err := batch.Write(); err != nil { + log.Crit("Failed to write account snapshot", "err", err) + } + batch.Reset() + } + } + // Push all the storage slots into the database + for accountHash, storage := range parent.storageData { + for storageHash, data := range storage { + rawdb.WriteStorageSnapshot(batch, accountHash, storageHash, data) + } + if batch.ValueSize() > ethdb.IdealBatchSize { + if err := batch.Write(); err != nil { + log.Crit("Failed to write storage snapshot", "err", err) + } + batch.Reset() + } + } + // Update the snapshot block marker and write any remainder data + base.number, base.root = parent.number, parent.root + + rawdb.WriteSnapshotBlock(batch, base.number, base.root) + if err := batch.Write(); err != nil { + log.Crit("Failed to write leftover snapshot", "err", err) + } + dl.parent = base + + return base.number, dl.number +} + +// flatten pushes all data from this point downwards, flattening everything into +// a single diff at the bottom. Since usually the lowermost diff is the largest, +// the flattening bulds up from there in reverse. +func (dl *diffLayer) flatten() snapshot { + // If the parent is not diff, we're the first in line, return unmodified + parent, ok := dl.parent.(*diffLayer) + if !ok { + return dl + } + // Parent is a diff, flatten it first (note, apart from weird corned cases, + // flatten will realistically only ever merge 1 layer, so there's no need to + // be smarter about grouping flattens together). + parent = parent.flatten().(*diffLayer) + + // Overwrite all the updated accounts blindly, merge the sorted list + for hash, data := range dl.accountData { + parent.accountData[hash] = data + } + parent.accountOrder = merge(parent.accountOrder, dl.accountOrder) + + // Overwrite all the updates storage slots (individually) + for accountHash, storage := range dl.storageData { + // If storage didn't exist (or was deleted) in the parent; or if the storage + // was freshly deleted in the child, overwrite blindly + if parent.storageData[accountHash] == nil || storage == nil { + parent.storageOrder[accountHash] = dl.storageOrder[accountHash] + parent.storageData[accountHash] = storage + continue + } + // Storage exists in both parent and child, merge the slots + comboData := parent.storageData[accountHash] + for storageHash, data := range storage { + comboData[storageHash] = data + } + parent.storageOrder[accountHash] = merge(parent.storageOrder[accountHash], dl.storageOrder[accountHash]) + parent.storageData[accountHash] = comboData + } + // Return the combo parent + parent.number = dl.number + parent.root = dl.root + parent.memory += dl.memory + return parent +} + +// Journal commits an entire diff hierarchy to disk into a single journal file. +// This is meant to be used during shutdown to persist the snapshot without +// flattening everything down (bad for reorgs). +func (dl *diffLayer) Journal() error { + dl.lock.RLock() + defer dl.lock.RUnlock() + + writer, err := dl.journal() + if err != nil { + return err + } + writer.Close() + return nil +} + +// journal is the internal version of Journal that also returns the journal file +// so subsequent layers know where to write to. +func (dl *diffLayer) journal() (io.WriteCloser, error) { + // If we've reached the bottom, open the journal + var writer io.WriteCloser + if parent, ok := dl.parent.(*diskLayer); ok { + file, err := os.Create(parent.journal) + if err != nil { + return nil, err + } + writer = file + } + // If we haven't reached the bottom yet, journal the parent first + if writer == nil { + file, err := dl.parent.(*diffLayer).journal() + if err != nil { + return nil, err + } + writer = file + } + // Everything below was journalled, persist this layer too + if err := rlp.Encode(writer, dl.number); err != nil { + writer.Close() + return nil, err + } + if err := rlp.Encode(writer, dl.root); err != nil { + writer.Close() + return nil, err + } + if err := rlp.Encode(writer, dl.accountData); err != nil { + writer.Close() + return nil, err + } + if err := rlp.Encode(writer, dl.storageData); err != nil { + writer.Close() + return nil, err + } + return writer, nil +} diff --git a/core/state/snapshot/disklayer.go b/core/state/snapshot/disklayer.go new file mode 100644 index 000000000000..d6a624df19f4 --- /dev/null +++ b/core/state/snapshot/disklayer.go @@ -0,0 +1,83 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package snapshot + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/rlp" +) + +// diskLayer is a low level persistent snapshot built on top of a key-value store. +type diskLayer struct { + journal string // Path of the snapshot journal to use on shutdown + db ethdb.KeyValueStore // Key-value store containing the base snapshot + + number uint64 // Block number of the base snapshot + root common.Hash // Root hash of the base snapshot +} + +// Info returns the block number and root hash for which this snapshot was made. +func (dl *diskLayer) Info() (uint64, common.Hash) { + return dl.number, dl.root +} + +// Account directly retrieves the account associated with a particular hash in +// the snapshot slim data format. +func (dl *diskLayer) Account(hash common.Hash) *Account { + data := dl.AccountRLP(hash) + if data == nil { + return nil + } + account := new(Account) + if err := rlp.DecodeBytes(data, account); err != nil { + panic(err) + } + return account +} + +// AccountRLP directly retrieves the account RLP associated with a particular +// hash in the snapshot slim data format. +func (dl *diskLayer) AccountRLP(hash common.Hash) []byte { + return rawdb.ReadAccountSnapshot(dl.db, hash) +} + +// Storage directly retrieves the storage data associated with a particular hash, +// within a particular account. +func (dl *diskLayer) Storage(accountHash, storageHash common.Hash) []byte { + return rawdb.ReadStorageSnapshot(dl.db, accountHash, storageHash) +} + +// Update creates a new layer on top of the existing snapshot diff tree with +// the specified data items. Note, the maps are retained by the method to avoid +// copying everything. +func (dl *diskLayer) Update(blockHash common.Hash, accounts map[common.Hash][]byte, storage map[common.Hash]map[common.Hash][]byte) *diffLayer { + return newDiffLayer(dl, blockHash, accounts, storage) +} + +// Cap traverses downwards the diff tree until the number of allowed layers are +// crossed. All diffs beyond the permitted number are flattened downwards. +func (dl *diskLayer) Cap(layers int, memory uint64) (uint64, uint64) { + return dl.number, dl.number +} + +// Journal commits an entire diff hierarchy to disk into a single journal file. +func (dl *diskLayer) Journal() error { + // There's no journalling a disk layer + return nil +} diff --git a/core/state/snapshot/generate.go b/core/state/snapshot/generate.go new file mode 100644 index 000000000000..ef7c818c9938 --- /dev/null +++ b/core/state/snapshot/generate.go @@ -0,0 +1,197 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package snapshot + +import ( + "bytes" + "fmt" + "math/big" + "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" + "github.com/ethereum/go-ethereum/trie" +) + +var ( + // emptyRoot is the known root hash of an empty trie. + emptyRoot = common.HexToHash("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") + + // emptyCode is the known hash of the empty EVM bytecode. + emptyCode = crypto.Keccak256Hash(nil) +) + +// wipeSnapshot iterates over the entire key-value database and deletes all the +// data associated with the snapshot (accounts, storage, metadata). After all is +// done, the snapshot range of the database is compacted to free up unused data +// blocks. +func wipeSnapshot(db ethdb.KeyValueStore) error { + // Batch deletions together to avoid holding an iterator for too long + var ( + batch = db.NewBatch() + items int + ) + // Iterate over the snapshot key-range and delete all of them + log.Info("Deleting previous snapshot leftovers") + start, logged := time.Now(), time.Now() + + it := db.NewIteratorWithStart(rawdb.StateSnapshotPrefix) + for it.Next() { + // Skip any keys with the correct prefix but wrong lenth (trie nodes) + key := it.Key() + if !bytes.HasPrefix(key, rawdb.StateSnapshotPrefix) { + break + } + if len(key) != len(rawdb.StateSnapshotPrefix)+common.HashLength && len(key) != len(rawdb.StateSnapshotPrefix)+2*common.HashLength { + continue + } + // Delete the key and periodically recreate the batch and iterator + batch.Delete(key) + items++ + + if items%10000 == 0 { + // Batch too large (or iterator too long lived, flush and recreate) + it.Release() + if err := batch.Write(); err != nil { + return err + } + batch.Reset() + it = db.NewIteratorWithStart(key) + + if time.Since(logged) > 8*time.Second { + log.Info("Deleting previous snapshot leftovers", "wiped", items, "elapsed", time.Since(start)) + logged = time.Now() + } + } + } + it.Release() + + rawdb.DeleteSnapshotBlock(batch) + if err := batch.Write(); err != nil { + return err + } + log.Info("Deleted previous snapshot leftovers", "wiped", items, "elapsed", time.Since(start)) + + // Compact the snapshot section of the database to get rid of unused space + log.Info("Compacting snapshot area in database") + start = time.Now() + + end := common.CopyBytes(rawdb.StateSnapshotPrefix) + end[len(end)-1]++ + + if err := db.Compact(rawdb.StateSnapshotPrefix, end); err != nil { + return err + } + log.Info("Compacted snapshot area in database", "elapsed", time.Since(start)) + + return nil +} + +// generateSnapshot regenerates a brand new snapshot based on an existing state database and head block. +func generateSnapshot(db ethdb.KeyValueStore, journal string, headNumber uint64, headRoot common.Hash) (snapshot, error) { + // Wipe any previously existing snapshot from the database + if err := wipeSnapshot(db); err != nil { + return nil, err + } + // Iterate the entire storage trie and re-generate the state snapshot + var ( + accountCount int + storageCount int + storageNodes int + accountSize common.StorageSize + storageSize common.StorageSize + ) + batch := db.NewBatch() + triedb := trie.NewDatabase(db) + + accTrie, err := trie.NewSecure(headRoot, triedb) + if err != nil { + return nil, err + } + accIt := trie.NewIterator(accTrie.NodeIterator(nil)) + for accIt.Next() { + var ( + curStorageCount int + curStorageNodes int + curAccountSize common.StorageSize + curStorageSize common.StorageSize + ) + var acc struct { + Nonce uint64 + Balance *big.Int + Root common.Hash + CodeHash []byte + } + if err := rlp.DecodeBytes(accIt.Value, &acc); err != nil { + return nil, err + } + data := AccountRLP(acc.Nonce, acc.Balance, acc.Root, acc.CodeHash) + curAccountSize += common.StorageSize(1 + common.HashLength + len(data)) + + rawdb.WriteAccountSnapshot(batch, common.BytesToHash(accIt.Key), data) + if batch.ValueSize() > ethdb.IdealBatchSize { + batch.Write() + batch.Reset() + } + if acc.Root != emptyRoot { + storeTrie, err := trie.NewSecure(acc.Root, triedb) + if err != nil { + return nil, err + } + storeIt := trie.NewIterator(storeTrie.NodeIterator(nil)) + for storeIt.Next() { + curStorageSize += common.StorageSize(1 + 2*common.HashLength + len(storeIt.Value)) + curStorageCount++ + + rawdb.WriteStorageSnapshot(batch, common.BytesToHash(accIt.Key), common.BytesToHash(storeIt.Key), storeIt.Value) + if batch.ValueSize() > ethdb.IdealBatchSize { + batch.Write() + batch.Reset() + } + } + curStorageNodes = storeIt.Nodes + } + accountCount++ + storageCount += curStorageCount + accountSize += curAccountSize + storageSize += curStorageSize + storageNodes += curStorageNodes + + fmt.Printf("%#x: %9s + %9s (%6d slots, %6d nodes), total %9s (%d accs, %d nodes) + %9s (%d slots, %d nodes)\n", accIt.Key, curAccountSize.TerminalString(), curStorageSize.TerminalString(), curStorageCount, curStorageNodes, accountSize.TerminalString(), accountCount, accIt.Nodes, storageSize.TerminalString(), storageCount, storageNodes) + } + // Update the snapshot block marker and write any remainder data + rawdb.WriteSnapshotBlock(batch, headNumber, headRoot) + batch.Write() + batch.Reset() + + // Compact the snapshot section of the database to get rid of unused space + log.Info("Compacting snapshot in chain database") + if err := db.Compact([]byte{'s'}, []byte{'s' + 1}); err != nil { + return nil, err + } + // New snapshot generated, construct a brand new base layer + return &diskLayer{ + journal: journal, + db: db, + number: headNumber, + root: headRoot, + }, nil +} diff --git a/core/state/snapshot/generate_test.go b/core/state/snapshot/generate_test.go new file mode 100644 index 000000000000..1206445c5860 --- /dev/null +++ b/core/state/snapshot/generate_test.go @@ -0,0 +1,111 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package snapshot + +import ( + "math/rand" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb/memorydb" +) + +// randomHash generates a random blob of data and returns it as a hash. +func randomHash() common.Hash { + var hash common.Hash + if n, err := rand.Read(hash[:]); n != common.HashLength || err != nil { + panic(err) + } + return hash +} + +// Tests that given a database with random data content, all parts of a snapshot +// can be crrectly wiped without touching anything else. +func TestWipe(t *testing.T) { + // Create a database with some random snapshot data + db := memorydb.New() + + for i := 0; i < 128; i++ { + account := randomHash() + rawdb.WriteAccountSnapshot(db, account, randomHash().Bytes()) + for j := 0; j < 1024; j++ { + rawdb.WriteStorageSnapshot(db, account, randomHash(), randomHash().Bytes()) + } + } + rawdb.WriteSnapshotBlock(db, 123, randomHash()) + + // Add some random non-snapshot data too to make wiping harder + for i := 0; i < 65536; i++ { + // Generate a key that's the wrong length for a state snapshot item + var keysize int + for keysize == 0 || keysize == 32 || keysize == 64 { + keysize = 8 + rand.Intn(64) // +8 to ensure we will "never" randomize duplicates + } + // Randomize the suffix, dedup and inject it under the snapshot namespace + keysuffix := make([]byte, keysize) + rand.Read(keysuffix) + db.Put(append(rawdb.StateSnapshotPrefix, keysuffix...), randomHash().Bytes()) + } + // Sanity check that all the keys are present + var items int + + it := db.NewIteratorWithPrefix(rawdb.StateSnapshotPrefix) + defer it.Release() + + for it.Next() { + key := it.Key() + if len(key) == len(rawdb.StateSnapshotPrefix)+32 || len(key) == len(rawdb.StateSnapshotPrefix)+64 { + items++ + } + } + if items != 128+128*1024 { + t.Fatalf("snapshot size mismatch: have %d, want %d", items, 128+128*1024) + } + if number, hash := rawdb.ReadSnapshotBlock(db); number != 123 || hash == (common.Hash{}) { + t.Errorf("snapshot block marker mismatch: have #%d [%#x], want #%d []", number, hash, 123) + } + // Wipe all snapshot entries from the database + if err := wipeSnapshot(db); err != nil { + t.Fatalf("failed to wipe snapshot: %v", err) + } + // Iterate over the database end ensure no snapshot information remains + it = db.NewIteratorWithPrefix(rawdb.StateSnapshotPrefix) + defer it.Release() + + for it.Next() { + key := it.Key() + if len(key) == len(rawdb.StateSnapshotPrefix)+32 || len(key) == len(rawdb.StateSnapshotPrefix)+64 { + t.Errorf("snapshot entry remained after wipe: %x", key) + } + } + if number, hash := rawdb.ReadSnapshotBlock(db); number != 0 || hash != (common.Hash{}) { + t.Errorf("snapshot block marker remained after wipe: #%d [%#x]", number, hash) + } + // Iterate over the database and ensure miscellaneous items are present + items = 0 + + it = db.NewIterator() + defer it.Release() + + for it.Next() { + items++ + } + if items != 65536 { + t.Fatalf("misc item count mismatch: have %d, want %d", items, 65536) + } +} diff --git a/core/state/snapshot/snapshot.go b/core/state/snapshot/snapshot.go new file mode 100644 index 000000000000..a3f5d8037bf0 --- /dev/null +++ b/core/state/snapshot/snapshot.go @@ -0,0 +1,225 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Package snapshot implements a journalled, dynamic state dump. +package snapshot + +import ( + "errors" + "fmt" + "os" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" +) + +// Snapshot represents the functionality supported by a snapshot storage layer. +type Snapshot interface { + // Info returns the block number and root hash for which this snapshot was made. + Info() (uint64, common.Hash) + + // Account directly retrieves the account associated with a particular hash in + // the snapshot slim data format. + Account(hash common.Hash) *Account + + // AccountRLP directly retrieves the account RLP associated with a particular + // hash in the snapshot slim data format. + AccountRLP(hash common.Hash) []byte + + // Storage directly retrieves the storage data associated with a particular hash, + // within a particular account. + Storage(accountHash, storageHash common.Hash) []byte +} + +// snapshot is the internal version of the snapshot data layer that supports some +// additional methods compared to the public API. +type snapshot interface { + Snapshot + + // Update creates a new layer on top of the existing snapshot diff tree with + // the specified data items. Note, the maps are retained by the method to avoid + // copying everything. + Update(blockRoot common.Hash, accounts map[common.Hash][]byte, storage map[common.Hash]map[common.Hash][]byte) *diffLayer + + // Cap traverses downwards the diff tree until the number of allowed layers are + // crossed. All diffs beyond the permitted number are flattened downwards. The + // block numbers for the disk layer and first diff layer are returned for GC. + Cap(layers int, memory uint64) (uint64, uint64) + + // Journal commits an entire diff hierarchy to disk into a single journal file. + // This is meant to be used during shutdown to persist the snapshot without + // flattening everything down (bad for reorgs). + Journal() error +} + +// SnapshotTree is an Ethereum state snapshot tree. It consists of one persistent +// base layer backed by a key-value store, on top of which arbitrarilly many in- +// memory diff layers are topped. The memory diffs can form a tree with branching, +// but the disk layer is singleton and common to all. If a reorg goes deeper than +// the disk layer, everything needs to be deleted. +// +// The goal of a state snapshot is twofold: to allow direct access to account and +// storage data to avoid expensive multi-level trie lookups; and to allow sorted, +// cheap iteration of the account/storage tries for sync aid. +type SnapshotTree struct { + layers map[common.Hash]snapshot // Collection of all known layers + lock sync.RWMutex +} + +// New attempts to load an already existing snapshot from a persistent key-value +// store (with a number of memory layers from a journal), ensuring that the head +// of the snapshot matches the expected one. +// +// If the snapshot is missing or inconsistent, the entirety is deleted and will +// be reconstructed from scratch based on the tries in the key-value store. +func New(db ethdb.KeyValueStore, journal string, headNumber uint64, headRoot common.Hash) (*SnapshotTree, error) { + // Attempt to load a previously persisted snapshot + head, err := loadSnapshot(db, journal, headNumber, headRoot) + if err != nil { + log.Warn("Failed to load snapshot, regenerating", "err", err) + if head, err = generateSnapshot(db, journal, headNumber, headRoot); err != nil { + return nil, err + } + } + // Existing snapshot loaded or one regenerated, seed all the layers + snap := &SnapshotTree{ + layers: make(map[common.Hash]snapshot), + } + for head != nil { + _, root := head.Info() + snap.layers[root] = head + + switch self := head.(type) { + case *diffLayer: + head = self.parent + case *diskLayer: + head = nil + default: + panic(fmt.Sprintf("unknown data layer: %T", self)) + } + } + return snap, nil +} + +// Snapshot retrieves a snapshot belonging to the given block root, or nil if no +// snapshot is maintained for that block. +func (st *SnapshotTree) Snapshot(blockRoot common.Hash) Snapshot { + st.lock.RLock() + defer st.lock.RUnlock() + + return st.layers[blockRoot] +} + +// Update adds a new snapshot into the tree, if that can be linked to an existing +// old parent. It is disallowed to insert a disk layer (the origin of all). +func (st *SnapshotTree) Update(blockRoot common.Hash, parentRoot common.Hash, accounts map[common.Hash][]byte, storage map[common.Hash]map[common.Hash][]byte) error { + // Generate a new snapshot on top of the parent + parent := st.Snapshot(parentRoot).(snapshot) + if parent == nil { + return fmt.Errorf("parent [%#x] snapshot missing", parentRoot) + } + snap := parent.Update(blockRoot, accounts, storage) + + // Save the new snapshot for later + st.lock.Lock() + defer st.lock.Unlock() + + st.layers[snap.root] = snap + return nil +} + +// Cap traverses downwards the snapshot tree from a head block hash until the +// number of allowed layers are crossed. All layers beyond the permitted number +// are flattened downwards. +func (st *SnapshotTree) Cap(blockRoot common.Hash, layers int, memory uint64) error { + // Retrieve the head snapshot to cap from + snap := st.Snapshot(blockRoot).(snapshot) + if snap == nil { + return fmt.Errorf("snapshot [%#x] missing", blockRoot) + } + // Run the internal capping and discard all stale layers + st.lock.Lock() + defer st.lock.Unlock() + + diskNumber, diffNumber := snap.Cap(layers, memory) + for hash, snap := range st.layers { + if number, _ := snap.Info(); number != diskNumber && number < diffNumber { + delete(st.layers, hash) + } + } + return nil +} + +// Journal commits an entire diff hierarchy to disk into a single journal file. +// This is meant to be used during shutdown to persist the snapshot without +// flattening everything down (bad for reorgs). +func (st *SnapshotTree) Journal(blockRoot common.Hash) error { + // Retrieve the head snapshot to journal from + snap := st.Snapshot(blockRoot).(snapshot) + if snap == nil { + return fmt.Errorf("snapshot [%#x] missing", blockRoot) + } + // Run the journaling + st.lock.Lock() + defer st.lock.Unlock() + + return snap.Journal() +} + +// loadSnapshot loads a pre-existing state snapshot backed by a key-value store. +func loadSnapshot(db ethdb.KeyValueStore, journal string, headNumber uint64, headRoot common.Hash) (snapshot, error) { + // Retrieve the block number and hash of the snapshot, failing if no snapshot + // is present in the database (or crashed mid-update). + number, root := rawdb.ReadSnapshotBlock(db) + if root == (common.Hash{}) { + return nil, errors.New("missing or corrupted snapshot") + } + base := &diskLayer{ + journal: journal, + db: db, + number: number, + root: root, + } + // Load all the snapshot diffs from the journal, failing if their chain is broken + // or does not lead from the disk snapshot to the specified head. + if _, err := os.Stat(journal); os.IsNotExist(err) { + // Journal doesn't exist, don't worry if it's not supposed to + if number != headNumber || root != headRoot { + return nil, fmt.Errorf("snapshot journal missing, head does't match snapshot: #%d [%#x] vs. #%d [%#x]", + headNumber, headRoot, number, root) + } + return base, nil + } + file, err := os.Open(journal) + if err != nil { + return nil, err + } + snapshot, err := loadDiffLayer(base, file) + if err != nil { + return nil, err + } + // Entire snapshot journal loaded, sanity check the head and return + // Journal doesn't exist, don't worry if it's not supposed to + number, root = snapshot.Info() + if number != headNumber || root != headRoot { + return nil, fmt.Errorf("head does't match snapshot: #%d [%#x] vs. #%d [%#x]", + headNumber, headRoot, number, root) + } + return snapshot, nil +} diff --git a/core/state/snapshot/snapshot_test.go b/core/state/snapshot/snapshot_test.go new file mode 100644 index 000000000000..903bd4a6f630 --- /dev/null +++ b/core/state/snapshot/snapshot_test.go @@ -0,0 +1,17 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package snapshot diff --git a/core/state/snapshot/sort.go b/core/state/snapshot/sort.go new file mode 100644 index 000000000000..04729c60b273 --- /dev/null +++ b/core/state/snapshot/sort.go @@ -0,0 +1,62 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package snapshot + +import ( + "bytes" + + "github.com/ethereum/go-ethereum/common" +) + +// hashes is a helper to implement sort.Interface. +type hashes []common.Hash + +// Len is the number of elements in the collection. +func (hs hashes) Len() int { return len(hs) } + +// Less reports whether the element with index i should sort before the element +// with index j. +func (hs hashes) Less(i, j int) bool { return bytes.Compare(hs[i][:], hs[j][:]) < 0 } + +// Swap swaps the elements with indexes i and j. +func (hs hashes) Swap(i, j int) { hs[i], hs[j] = hs[j], hs[i] } + +// merge combines two sorted lists of hashes into a combo sorted one. +func merge(a, b []common.Hash) []common.Hash { + result := make([]common.Hash, len(a)+len(b)) + + i := 0 + for len(a) > 0 && len(b) > 0 { + if bytes.Compare(a[0][:], b[0][:]) < 0 { + result[i] = a[0] + a = a[1:] + } else { + result[i] = b[0] + b = b[1:] + } + i++ + } + for j := 0; j < len(a); j++ { + result[i] = a[j] + i++ + } + for j := 0; j < len(b); j++ { + result[i] = b[j] + i++ + } + return result +} diff --git a/core/state/state_object.go b/core/state/state_object.go index 45ae95a2a92c..167bc5bcbf7e 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -188,16 +188,27 @@ func (s *stateObject) GetCommittedState(db Database, key common.Hash) common.Has if cached { return value } + // If no live objects are available, attempt to use snapshots + var ( + enc []byte + err error + ) + /* if s.db.snap != nil { + if metrics.EnabledExpensive { + defer func(start time.Time) { s.db.StorageSnapReads += time.Since(start) }(time.Now()) + } + enc = s.db.snap.Storage(s.addrHash, crypto.Keccak256Hash(key[:])) + } else {*/ // Track the amount of time wasted on reading the storage trie if metrics.EnabledExpensive { - defer func(start time.Time) { s.db.StorageReads += time.Since(start) }(time.Now()) + defer func(start time.Time) { s.db.StorageTrieReads += time.Since(start) }(time.Now()) } // Otherwise load the value from the database - enc, err := s.getTrie(db).TryGet(key[:]) - if err != nil { + if enc, err = s.getTrie(db).TryGet(key[:]); err != nil { s.setError(err) return common.Hash{} } + //} if len(enc) > 0 { _, content, _, err := rlp.Split(enc) if err != nil { @@ -269,13 +280,21 @@ func (s *stateObject) updateTrie(db Database) Trie { } s.originStorage[key] = value + var v []byte if (value == common.Hash{}) { s.setError(tr.TryDelete(key[:])) - continue + } else { + // Encoding []byte cannot fail, ok to ignore the error. + v, _ = rlp.EncodeToBytes(bytes.TrimLeft(value[:], "\x00")) + s.setError(tr.TryUpdate(key[:], v)) + } + // If state snapshotting is active, cache the data til commit + if s.db.snap != nil { + if s.db.snapStorage[s.addrHash] == nil { + s.db.snapStorage[s.addrHash] = make(map[common.Hash][]byte) + } + s.db.snapStorage[s.addrHash][crypto.Keccak256Hash(key[:])] = v // v will be nil if value is 0x00 } - // Encoding []byte cannot fail, ok to ignore the error. - v, _ := rlp.EncodeToBytes(bytes.TrimLeft(value[:], "\x00")) - s.setError(tr.TryUpdate(key[:], v)) } return tr } diff --git a/core/state/statedb.go b/core/state/statedb.go index b07f08fd2121..4613f6836fee 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -25,6 +25,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" @@ -66,6 +67,11 @@ type StateDB struct { db Database trie Trie + snaps *snapshot.SnapshotTree + snap snapshot.Snapshot + snapAccounts map[common.Hash][]byte + snapStorage map[common.Hash]map[common.Hash][]byte + // This map holds 'live' objects, which will get modified while processing a state transition. stateObjects map[common.Address]*stateObject stateObjectsDirty map[common.Address]struct{} @@ -94,31 +100,41 @@ type StateDB struct { nextRevisionId int // Measurements gathered during execution for debugging purposes - AccountReads time.Duration - AccountHashes time.Duration - AccountUpdates time.Duration - AccountCommits time.Duration - StorageReads time.Duration - StorageHashes time.Duration - StorageUpdates time.Duration - StorageCommits time.Duration + AccountSnapReads time.Duration + AccountTrieReads time.Duration + AccountHashes time.Duration + AccountUpdates time.Duration + AccountCommits time.Duration + StorageSnapReads time.Duration + StorageTrieReads time.Duration + StorageHashes time.Duration + StorageUpdates time.Duration + StorageCommits time.Duration } // Create a new state from a given trie. -func New(root common.Hash, db Database) (*StateDB, error) { +func New(root common.Hash, db Database, snaps *snapshot.SnapshotTree) (*StateDB, error) { tr, err := db.OpenTrie(root) if err != nil { return nil, err } - return &StateDB{ + sdb := &StateDB{ db: db, trie: tr, + snaps: snaps, stateObjects: make(map[common.Address]*stateObject), stateObjectsDirty: make(map[common.Address]struct{}), logs: make(map[common.Hash][]*types.Log), preimages: make(map[common.Hash][]byte), journal: newJournal(), - }, nil + } + if sdb.snaps != nil { + if sdb.snap = sdb.snaps.Snapshot(root); sdb.snap != nil { + sdb.snapAccounts = make(map[common.Hash][]byte) + sdb.snapStorage = make(map[common.Hash]map[common.Hash][]byte) + } + } + return sdb, nil } // setError remembers the first non-nil error it is called with. @@ -149,6 +165,14 @@ func (self *StateDB) Reset(root common.Hash) error { self.logSize = 0 self.preimages = make(map[common.Hash][]byte) self.clearJournalAndRefund() + + if self.snaps != nil { + self.snapAccounts, self.snapStorage = nil, nil + if self.snap = self.snaps.Snapshot(root); self.snap != nil { + self.snapAccounts = make(map[common.Hash][]byte) + self.snapStorage = make(map[common.Hash]map[common.Hash][]byte) + } + } return nil } @@ -434,6 +458,11 @@ func (s *StateDB) updateStateObject(stateObject *stateObject) { panic(fmt.Errorf("can't encode object at %x: %v", addr[:], err)) } s.setError(s.trie.TryUpdate(addr[:], data)) + + // If state snapshotting is active, cache the data til commit + if s.snap != nil { + s.snapAccounts[stateObject.addrHash] = snapshot.AccountRLP(stateObject.data.Nonce, stateObject.data.Balance, stateObject.data.Root, stateObject.data.CodeHash) + } } // deleteStateObject removes the given object from the state trie. @@ -447,6 +476,11 @@ func (s *StateDB) deleteStateObject(stateObject *stateObject) { addr := stateObject.Address() s.setError(s.trie.TryDelete(addr[:])) + + // If state snapshotting is active, cache the data til commit + if s.snap != nil { + s.snapAccounts[stateObject.addrHash] = nil // Yes, nil means deleted + } } // Retrieve a state object given by the address. Returns nil if not found. @@ -458,21 +492,39 @@ func (s *StateDB) getStateObject(addr common.Address) (stateObject *stateObject) } return obj } - // Track the amount of time wasted on loading the object from the database + // If no live objects are available, attempt to use snapshots + var data Account + /*if s.snap != nil { + if metrics.EnabledExpensive { + defer func(start time.Time) { s.AccountSnapReads += time.Since(start) }(time.Now()) + } + acc := s.snap.Account(crypto.Keccak256Hash(addr[:])) + if acc == nil { + return nil + } + data.Nonce, data.Balance, data.CodeHash = acc.Nonce, acc.Balance, acc.CodeHash + if len(data.CodeHash) == 0 { + data.CodeHash = emptyCodeHash + } + data.Root = common.BytesToHash(acc.Root) + if data.Root == (common.Hash{}) { + data.Root = emptyRoot + } + } else {*/ + // Snapshot unavailable, fall back to the trie if metrics.EnabledExpensive { - defer func(start time.Time) { s.AccountReads += time.Since(start) }(time.Now()) + defer func(start time.Time) { s.AccountTrieReads += time.Since(start) }(time.Now()) } - // Load the object from the database enc, err := s.trie.TryGet(addr[:]) if len(enc) == 0 { s.setError(err) return nil } - var data Account if err := rlp.DecodeBytes(enc, &data); err != nil { log.Error("Failed to decode state object", "addr", addr, "err", err) return nil } + //} // Insert into the live set obj := newObject(s, addr, data) s.setStateObject(obj) @@ -733,5 +785,16 @@ func (s *StateDB) Commit(deleteEmptyObjects bool) (root common.Hash, err error) } return nil }) + // If snapshotting is enabled, update the snapshot tree with this new version + if s.snap != nil { + _, parentRoot := s.snap.Info() + if err := s.snaps.Update(root, parentRoot, s.snapAccounts, s.snapStorage); err != nil { + log.Warn("Failed to update snapshot tree", "from", parentRoot, "to", root, "err", err) + } + if err := s.snaps.Cap(root, 16, 4*1024*1024); err != nil { + log.Warn("Failed to cap snapshot tree", "root", root, "layers", 16, "memory", 4*1024*1024, "err", err) + } + s.snap, s.snapAccounts, s.snapStorage = nil, nil, nil + } return root, err } diff --git a/core/vm/runtime/runtime.go b/core/vm/runtime/runtime.go index db1f6f38222b..165195a67a1c 100644 --- a/core/vm/runtime/runtime.go +++ b/core/vm/runtime/runtime.go @@ -99,7 +99,7 @@ func Execute(code, input []byte, cfg *Config) ([]byte, *state.StateDB, error) { setDefaults(cfg) if cfg.State == nil { - cfg.State, _ = state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase())) + cfg.State, _ = state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) } var ( address = common.BytesToAddress([]byte("contract")) @@ -129,7 +129,7 @@ func Create(input []byte, cfg *Config) ([]byte, common.Address, uint64, error) { setDefaults(cfg) if cfg.State == nil { - cfg.State, _ = state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase())) + cfg.State, _ = state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) } var ( vmenv = NewEnv(cfg) diff --git a/core/vm/runtime/runtime_test.go b/core/vm/runtime/runtime_test.go index 15f545ddcafb..11de5ce187a1 100644 --- a/core/vm/runtime/runtime_test.go +++ b/core/vm/runtime/runtime_test.go @@ -95,7 +95,7 @@ func TestExecute(t *testing.T) { } func TestCall(t *testing.T) { - state, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase())) + state, _ := state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) address := common.HexToAddress("0x0a") state.SetCode(address, []byte{ byte(vm.PUSH1), 10, @@ -151,7 +151,7 @@ func BenchmarkCall(b *testing.B) { } func benchmarkEVM_Create(bench *testing.B, code string) { var ( - statedb, _ = state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase())) + statedb, _ = state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) sender = common.BytesToAddress([]byte("sender")) receiver = common.BytesToAddress([]byte("receiver")) ) diff --git a/eth/api_test.go b/eth/api_test.go index 1e7c489c3295..ab846db3ea0e 100644 --- a/eth/api_test.go +++ b/eth/api_test.go @@ -64,7 +64,7 @@ func (h resultHash) Less(i, j int) bool { return bytes.Compare(h[i].Bytes(), h[j func TestAccountRange(t *testing.T) { var ( statedb = state.NewDatabase(rawdb.NewMemoryDatabase()) - state, _ = state.New(common.Hash{}, statedb) + state, _ = state.New(common.Hash{}, statedb, nil) addrs = [AccountRangeMaxResults * 2]common.Address{} m = map[common.Address]bool{} ) @@ -162,7 +162,7 @@ func TestAccountRange(t *testing.T) { func TestEmptyAccountRange(t *testing.T) { var ( statedb = state.NewDatabase(rawdb.NewMemoryDatabase()) - state, _ = state.New(common.Hash{}, statedb) + state, _ = state.New(common.Hash{}, statedb, nil) ) state.Commit(true) @@ -188,7 +188,7 @@ func TestEmptyAccountRange(t *testing.T) { func TestStorageRangeAt(t *testing.T) { // Create a state where account 0x010000... has a few storage entries. var ( - state, _ = state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase())) + state, _ = state.New(common.Hash{}, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) addr = common.Address{0x01} keys = []common.Hash{ // hashes of Keys of storage common.HexToHash("340dd630ad21bf010b4e676dbfa9ba9a02175262d1fa356232cfde6cb5b47ef2"), diff --git a/eth/api_tracer.go b/eth/api_tracer.go index ce211cbd99ef..560f460445eb 100644 --- a/eth/api_tracer.go +++ b/eth/api_tracer.go @@ -155,7 +155,7 @@ func (api *PrivateDebugAPI) traceChain(ctx context.Context, start, end *types.Bl return nil, fmt.Errorf("parent block #%d not found", number-1) } } - statedb, err := state.New(start.Root(), database) + statedb, err := state.New(start.Root(), database, nil) if err != nil { // If the starting state is missing, allow some number of blocks to be reexecuted reexec := defaultTraceReexec @@ -168,7 +168,7 @@ func (api *PrivateDebugAPI) traceChain(ctx context.Context, start, end *types.Bl if start == nil { break } - if statedb, err = state.New(start.Root(), database); err == nil { + if statedb, err = state.New(start.Root(), database, nil); err == nil { break } } @@ -648,7 +648,7 @@ func (api *PrivateDebugAPI) computeStateDB(block *types.Block, reexec uint64) (* if block == nil { break } - if statedb, err = state.New(block.Root(), database); err == nil { + if statedb, err = state.New(block.Root(), database, nil); err == nil { break } } diff --git a/eth/handler_test.go b/eth/handler_test.go index 0f1672fd4498..b6963d207bcf 100644 --- a/eth/handler_test.go +++ b/eth/handler_test.go @@ -348,7 +348,7 @@ func testGetNodeData(t *testing.T, protocol int) { } accounts := []common.Address{testBank, acc1Addr, acc2Addr} for i := uint64(0); i <= pm.blockchain.CurrentBlock().NumberU64(); i++ { - trie, _ := state.New(pm.blockchain.GetBlockByNumber(i).Root(), state.NewDatabase(statedb)) + trie, _ := state.New(pm.blockchain.GetBlockByNumber(i).Root(), state.NewDatabase(statedb), nil) for j, acc := range accounts { state, _ := pm.blockchain.State() diff --git a/light/odr_test.go b/light/odr_test.go index debd5544c312..9149c02fc212 100644 --- a/light/odr_test.go +++ b/light/odr_test.go @@ -149,7 +149,7 @@ func odrAccounts(ctx context.Context, db ethdb.Database, bc *core.BlockChain, lc st = NewState(ctx, header, lc.Odr()) } else { header := bc.GetHeaderByHash(bhash) - st, _ = state.New(header.Root, state.NewDatabase(db)) + st, _ = state.New(header.Root, state.NewDatabase(db), nil) } var res []byte @@ -189,7 +189,7 @@ func odrContractCall(ctx context.Context, db ethdb.Database, bc *core.BlockChain } else { chain = bc header = bc.GetHeaderByHash(bhash) - st, _ = state.New(header.Root, state.NewDatabase(db)) + st, _ = state.New(header.Root, state.NewDatabase(db), nil) } // Perform read-only call. diff --git a/light/trie.go b/light/trie.go index e512bf6f9562..0d69e74e21f2 100644 --- a/light/trie.go +++ b/light/trie.go @@ -30,7 +30,7 @@ import ( ) func NewState(ctx context.Context, head *types.Header, odr OdrBackend) *state.StateDB { - state, _ := state.New(head.Root, NewStateDatabase(ctx, head, odr)) + state, _ := state.New(head.Root, NewStateDatabase(ctx, head, odr), nil) return state } diff --git a/tests/state_test_util.go b/tests/state_test_util.go index c6341e5248eb..1ca79b6751be 100644 --- a/tests/state_test_util.go +++ b/tests/state_test_util.go @@ -196,7 +196,7 @@ func (t *StateTest) gasLimit(subtest StateSubtest) uint64 { func MakePreState(db ethdb.Database, accounts core.GenesisAlloc) *state.StateDB { sdb := state.NewDatabase(db) - statedb, _ := state.New(common.Hash{}, sdb) + statedb, _ := state.New(common.Hash{}, sdb, nil) for addr, a := range accounts { statedb.SetCode(addr, a.Code) statedb.SetNonce(addr, a.Nonce) @@ -207,7 +207,7 @@ func MakePreState(db ethdb.Database, accounts core.GenesisAlloc) *state.StateDB } // Commit and re-open to start with a clean state. root, _ := statedb.Commit(false) - statedb, _ = state.New(root, sdb) + statedb, _ = state.New(root, sdb, nil) return statedb } diff --git a/trie/iterator.go b/trie/iterator.go index da93b2fadb3b..259eec20b38f 100644 --- a/trie/iterator.go +++ b/trie/iterator.go @@ -29,6 +29,7 @@ import ( type Iterator struct { nodeIt NodeIterator + Nodes int // Number of nodes iterated over Key []byte // Current data key on which the iterator is positioned on Value []byte // Current data value on which the iterator is positioned on Err error @@ -44,6 +45,7 @@ func NewIterator(it NodeIterator) *Iterator { // Next moves the iterator forward one key-value entry. func (it *Iterator) Next() bool { for it.nodeIt.Next(true) { + it.Nodes++ if it.nodeIt.Leaf() { it.Key = it.nodeIt.LeafKey() it.Value = it.nodeIt.LeafBlob() From a86220dfc367a43425faa0d0d3b0ba6a36aafe05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Thu, 8 Aug 2019 10:08:20 +0300 Subject: [PATCH 2/5] core/state: don't sort snapshot ordering --- core/blockchain.go | 47 ++++++++++++------------ core/state/snapshot/difflayer.go | 55 ++++++++++++++++------------ core/state/snapshot/snapshot.go | 4 +-- core/state/state_object.go | 24 ++++++------- core/state/statedb.go | 62 ++++++++++++++++++-------------- core/state_prefetcher.go | 2 ++ 6 files changed, 109 insertions(+), 85 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index f39b288724eb..ff630cfbddfa 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -51,17 +51,19 @@ var ( headHeaderGauge = metrics.NewRegisteredGauge("chain/head/header", nil) headFastBlockGauge = metrics.NewRegisteredGauge("chain/head/receipt", nil) - accountSnapReadTimer = metrics.NewRegisteredTimer("chain/account/snapreads", nil) - accountTrieReadTimer = metrics.NewRegisteredTimer("chain/account/triereads", nil) - accountHashTimer = metrics.NewRegisteredTimer("chain/account/hashes", nil) - accountUpdateTimer = metrics.NewRegisteredTimer("chain/account/updates", nil) - accountCommitTimer = metrics.NewRegisteredTimer("chain/account/commits", nil) - - storageSnapReadTimer = metrics.NewRegisteredTimer("chain/storage/snapreads", nil) - storageTrieReadTimer = metrics.NewRegisteredTimer("chain/storage/triereads", nil) - storageHashTimer = metrics.NewRegisteredTimer("chain/storage/hashes", nil) - storageUpdateTimer = metrics.NewRegisteredTimer("chain/storage/updates", nil) - storageCommitTimer = metrics.NewRegisteredTimer("chain/storage/commits", nil) + accountReadTimer = metrics.NewRegisteredTimer("chain/account/reads", nil) + accountHashTimer = metrics.NewRegisteredTimer("chain/account/hashes", nil) + accountUpdateTimer = metrics.NewRegisteredTimer("chain/account/updates", nil) + accountCommitTimer = metrics.NewRegisteredTimer("chain/account/commits", nil) + + storageReadTimer = metrics.NewRegisteredTimer("chain/storage/reads", nil) + storageHashTimer = metrics.NewRegisteredTimer("chain/storage/hashes", nil) + storageUpdateTimer = metrics.NewRegisteredTimer("chain/storage/updates", nil) + storageCommitTimer = metrics.NewRegisteredTimer("chain/storage/commits", nil) + + snapshotAccountReadTimer = metrics.NewRegisteredTimer("chain/snapshot/accountreads", nil) + snapshotStorageReadTimer = metrics.NewRegisteredTimer("chain/snapshot/storagereads", nil) + snapshotCommitTimer = metrics.NewRegisteredTimer("chain/snapshot/commits", nil) blockInsertTimer = metrics.NewRegisteredTimer("chain/inserts", nil) blockValidationTimer = metrics.NewRegisteredTimer("chain/validation", nil) @@ -1630,16 +1632,16 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, [] return it.index, events, coalescedLogs, err } // Update the metrics touched during block processing - accountSnapReadTimer.Update(statedb.AccountSnapReads) // Account reads are complete, we can mark them - accountTrieReadTimer.Update(statedb.AccountTrieReads) // Account reads are complete, we can mark them - storageSnapReadTimer.Update(statedb.StorageSnapReads) // Storage reads are complete, we can mark them - storageTrieReadTimer.Update(statedb.StorageTrieReads) // Storage reads are complete, we can mark them - accountUpdateTimer.Update(statedb.AccountUpdates) // Account updates are complete, we can mark them - storageUpdateTimer.Update(statedb.StorageUpdates) // Storage updates are complete, we can mark them + accountReadTimer.Update(statedb.AccountReads) // Account reads are complete, we can mark them + storageReadTimer.Update(statedb.StorageReads) // Storage reads are complete, we can mark them + accountUpdateTimer.Update(statedb.AccountUpdates) // Account updates are complete, we can mark them + storageUpdateTimer.Update(statedb.StorageUpdates) // Storage updates are complete, we can mark them + snapshotAccountReadTimer.Update(statedb.SnapshotAccountReads) // Account reads are complete, we can mark them + snapshotStorageReadTimer.Update(statedb.SnapshotStorageReads) // Storage reads are complete, we can mark them triehash := statedb.AccountHashes + statedb.StorageHashes // Save to not double count in validation - trieproc := statedb.AccountSnapReads + statedb.AccountTrieReads + statedb.AccountUpdates - trieproc += statedb.StorageSnapReads + statedb.StorageTrieReads + statedb.StorageUpdates + trieproc := statedb.SnapshotAccountReads + statedb.AccountReads + statedb.AccountUpdates + trieproc += statedb.SnapshotStorageReads + statedb.StorageReads + statedb.StorageUpdates blockExecutionTimer.Update(time.Since(substart) - trieproc - triehash) @@ -1668,10 +1670,11 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, [] atomic.StoreUint32(&followupInterrupt, 1) // Update the metrics touched during block commit - accountCommitTimer.Update(statedb.AccountCommits) // Account commits are complete, we can mark them - storageCommitTimer.Update(statedb.StorageCommits) // Storage commits are complete, we can mark them + accountCommitTimer.Update(statedb.AccountCommits) // Account commits are complete, we can mark them + storageCommitTimer.Update(statedb.StorageCommits) // Storage commits are complete, we can mark them + snapshotCommitTimer.Update(statedb.SnapshotCommits) // Snapshot commits are complete, we can mark them - blockWriteTimer.Update(time.Since(substart) - statedb.AccountCommits - statedb.StorageCommits) + blockWriteTimer.Update(time.Since(substart) - statedb.AccountCommits - statedb.StorageCommits - statedb.SnapshotCommits) blockInsertTimer.UpdateSince(start) switch status { diff --git a/core/state/snapshot/difflayer.go b/core/state/snapshot/difflayer.go index e93d9fbf70a9..5ffc4047d9b5 100644 --- a/core/state/snapshot/difflayer.go +++ b/core/state/snapshot/difflayer.go @@ -43,10 +43,12 @@ type diffLayer struct { number uint64 // Block number to which this snapshot diff belongs to root common.Hash // Root hash to which this snapshot diff belongs to - accountOrder []common.Hash // Sorted accounts for iterated retrieval - accountData map[common.Hash][]byte // Keyed accounts for direct retrival (nil means deleted) - storageOrder map[common.Hash][]common.Hash // Sorted storage slots for iterated retrievals. one per account - storageData map[common.Hash]map[common.Hash][]byte // Keyed storage slots for direct retrival. one per account (nil means deleted) + accountList []common.Hash // List of account for iteration, might not be sorted yet (lazy) + accountSorted bool // Flag whether the account list has alreayd been sorted or not + accountData map[common.Hash][]byte // Keyed accounts for direct retrival (nil means deleted) + storageList map[common.Hash][]common.Hash // List of storage slots for iterated retrievals, one per account + storageSorted map[common.Hash]bool // Flag whether the storage slot list has alreayd been sorted or not + storageData map[common.Hash]map[common.Hash][]byte // Keyed storage slots for direct retrival. one per account (nil means deleted) lock sync.RWMutex } @@ -65,16 +67,21 @@ func newDiffLayer(parent snapshot, root common.Hash, accounts map[common.Hash][] storageData: storage, } // Fill the account hashes and sort them for the iterator - dl.accountOrder = make([]common.Hash, 0, len(accounts)) + accountList := make([]common.Hash, 0, len(accounts)) for hash, data := range accounts { - dl.accountOrder = append(dl.accountOrder, hash) + accountList = append(accountList, hash) dl.memory += uint64(len(data)) } - sort.Sort(hashes(dl.accountOrder)) - dl.memory += uint64(len(dl.accountOrder) * common.HashLength) + sort.Sort(hashes(accountList)) + dl.accountList = accountList + dl.accountSorted = true + + dl.memory += uint64(len(dl.accountList) * common.HashLength) // Fill the storage hashes and sort them for the iterator - dl.storageOrder = make(map[common.Hash][]common.Hash, len(storage)) + dl.storageList = make(map[common.Hash][]common.Hash, len(storage)) + dl.storageSorted = make(map[common.Hash]bool, len(storage)) + for accountHash, slots := range storage { // If the slots are nil, sanity check that it's a deleted account if slots == nil { @@ -83,7 +90,7 @@ func newDiffLayer(parent snapshot, root common.Hash, accounts map[common.Hash][] panic(fmt.Sprintf("storage in %#x nil, but account conflicts (%#x, exists: %v)", accountHash, account, ok)) } // Everything ok, store the deletion mark and continue - dl.storageOrder[accountHash] = nil + dl.storageList[accountHash] = nil continue } // Storage slots are not nil so entire contract was not deleted, ensure the @@ -93,16 +100,18 @@ func newDiffLayer(parent snapshot, root common.Hash, accounts map[common.Hash][] //panic(fmt.Sprintf("storage in %#x exists, but account nil (exists: %v)", accountHash, ok)) } // Fill the storage hashes for this account and sort them for the iterator - storageOrder := make([]common.Hash, 0, len(slots)) + storageList := make([]common.Hash, 0, len(slots)) for storageHash, data := range slots { - storageOrder = append(storageOrder, storageHash) + storageList = append(storageList, storageHash) dl.memory += uint64(len(data)) } - sort.Sort(hashes(storageOrder)) - dl.storageOrder[accountHash] = storageOrder - dl.memory += uint64(len(storageOrder) * common.HashLength) + sort.Sort(hashes(storageList)) + dl.storageList[accountHash] = storageList + dl.storageSorted[accountHash] = true + + dl.memory += uint64(len(storageList) * common.HashLength) } - dl.memory += uint64(len(dl.storageOrder) * common.HashLength) + dl.memory += uint64(len(dl.storageList) * common.HashLength) return dl } @@ -206,9 +215,6 @@ func (dl *diffLayer) Update(blockRoot common.Hash, accounts map[common.Hash][]by // the layer limit is reached, memory cap is also enforced (but not before). The // block numbers for the disk layer and first diff layer are returned for GC. func (dl *diffLayer) Cap(layers int, memory uint64) (uint64, uint64) { - dl.lock.Lock() - defer dl.lock.Unlock() - // Dive until we run out of layers or reach the persistent database if layers > 2 { // If we still have diff layers below, recurse @@ -224,6 +230,9 @@ func (dl *diffLayer) Cap(layers int, memory uint64) (uint64, uint64) { case *diskLayer: return parent.number, dl.number case *diffLayer: + dl.lock.Lock() + defer dl.lock.Unlock() + dl.parent = parent.flatten() if dl.parent.(*diffLayer).memory < memory { diskNumber, _ := parent.parent.Info() @@ -294,14 +303,15 @@ func (dl *diffLayer) flatten() snapshot { for hash, data := range dl.accountData { parent.accountData[hash] = data } - parent.accountOrder = merge(parent.accountOrder, dl.accountOrder) + parent.accountList = append(parent.accountList, dl.accountList...) // TODO(karalabe): dedup!! + parent.accountSorted = false // Overwrite all the updates storage slots (individually) for accountHash, storage := range dl.storageData { // If storage didn't exist (or was deleted) in the parent; or if the storage // was freshly deleted in the child, overwrite blindly if parent.storageData[accountHash] == nil || storage == nil { - parent.storageOrder[accountHash] = dl.storageOrder[accountHash] + parent.storageList[accountHash] = dl.storageList[accountHash] parent.storageData[accountHash] = storage continue } @@ -310,8 +320,9 @@ func (dl *diffLayer) flatten() snapshot { for storageHash, data := range storage { comboData[storageHash] = data } - parent.storageOrder[accountHash] = merge(parent.storageOrder[accountHash], dl.storageOrder[accountHash]) parent.storageData[accountHash] = comboData + parent.storageList[accountHash] = append(parent.storageList[accountHash], dl.storageList[accountHash]...) // TODO(karalabe): dedup!! + parent.storageSorted[accountHash] = false } // Return the combo parent parent.number = dl.number diff --git a/core/state/snapshot/snapshot.go b/core/state/snapshot/snapshot.go index a3f5d8037bf0..e7a029ffaabc 100644 --- a/core/state/snapshot/snapshot.go +++ b/core/state/snapshot/snapshot.go @@ -158,9 +158,9 @@ func (st *SnapshotTree) Cap(blockRoot common.Hash, layers int, memory uint64) er defer st.lock.Unlock() diskNumber, diffNumber := snap.Cap(layers, memory) - for hash, snap := range st.layers { + for root, snap := range st.layers { if number, _ := snap.Info(); number != diskNumber && number < diffNumber { - delete(st.layers, hash) + delete(st.layers, root) } } return nil diff --git a/core/state/state_object.go b/core/state/state_object.go index 167bc5bcbf7e..9706797c9589 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -193,22 +193,22 @@ func (s *stateObject) GetCommittedState(db Database, key common.Hash) common.Has enc []byte err error ) - /* if s.db.snap != nil { + if s.db.snap != nil { if metrics.EnabledExpensive { - defer func(start time.Time) { s.db.StorageSnapReads += time.Since(start) }(time.Now()) + defer func(start time.Time) { s.db.SnapshotStorageReads += time.Since(start) }(time.Now()) } enc = s.db.snap.Storage(s.addrHash, crypto.Keccak256Hash(key[:])) - } else {*/ - // Track the amount of time wasted on reading the storage trie - if metrics.EnabledExpensive { - defer func(start time.Time) { s.db.StorageTrieReads += time.Since(start) }(time.Now()) - } - // Otherwise load the value from the database - if enc, err = s.getTrie(db).TryGet(key[:]); err != nil { - s.setError(err) - return common.Hash{} + } else { + // Track the amount of time wasted on reading the storage trie + if metrics.EnabledExpensive { + defer func(start time.Time) { s.db.StorageReads += time.Since(start) }(time.Now()) + } + // Otherwise load the value from the database + if enc, err = s.getTrie(db).TryGet(key[:]); err != nil { + s.setError(err) + return common.Hash{} + } } - //} if len(enc) > 0 { _, content, _, err := rlp.Split(enc) if err != nil { diff --git a/core/state/statedb.go b/core/state/statedb.go index 4613f6836fee..e817c14dc7ef 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -100,16 +100,17 @@ type StateDB struct { nextRevisionId int // Measurements gathered during execution for debugging purposes - AccountSnapReads time.Duration - AccountTrieReads time.Duration - AccountHashes time.Duration - AccountUpdates time.Duration - AccountCommits time.Duration - StorageSnapReads time.Duration - StorageTrieReads time.Duration - StorageHashes time.Duration - StorageUpdates time.Duration - StorageCommits time.Duration + AccountReads time.Duration + AccountHashes time.Duration + AccountUpdates time.Duration + AccountCommits time.Duration + StorageReads time.Duration + StorageHashes time.Duration + StorageUpdates time.Duration + StorageCommits time.Duration + SnapshotAccountReads time.Duration + SnapshotStorageReads time.Duration + SnapshotCommits time.Duration } // Create a new state from a given trie. @@ -494,9 +495,9 @@ func (s *StateDB) getStateObject(addr common.Address) (stateObject *stateObject) } // If no live objects are available, attempt to use snapshots var data Account - /*if s.snap != nil { + if s.snap != nil { if metrics.EnabledExpensive { - defer func(start time.Time) { s.AccountSnapReads += time.Since(start) }(time.Now()) + defer func(start time.Time) { s.SnapshotAccountReads += time.Since(start) }(time.Now()) } acc := s.snap.Account(crypto.Keccak256Hash(addr[:])) if acc == nil { @@ -510,21 +511,21 @@ func (s *StateDB) getStateObject(addr common.Address) (stateObject *stateObject) if data.Root == (common.Hash{}) { data.Root = emptyRoot } - } else {*/ - // Snapshot unavailable, fall back to the trie - if metrics.EnabledExpensive { - defer func(start time.Time) { s.AccountTrieReads += time.Since(start) }(time.Now()) - } - enc, err := s.trie.TryGet(addr[:]) - if len(enc) == 0 { - s.setError(err) - return nil - } - if err := rlp.DecodeBytes(enc, &data); err != nil { - log.Error("Failed to decode state object", "addr", addr, "err", err) - return nil + } else { + // Snapshot unavailable, fall back to the trie + if metrics.EnabledExpensive { + defer func(start time.Time) { s.AccountReads += time.Since(start) }(time.Now()) + } + enc, err := s.trie.TryGet(addr[:]) + if len(enc) == 0 { + s.setError(err) + return nil + } + if err := rlp.DecodeBytes(enc, &data); err != nil { + log.Error("Failed to decode state object", "addr", addr, "err", err) + return nil + } } - //} // Insert into the live set obj := newObject(s, addr, data) s.setStateObject(obj) @@ -768,8 +769,9 @@ func (s *StateDB) Commit(deleteEmptyObjects bool) (root common.Hash, err error) delete(s.stateObjectsDirty, addr) } // Write the account trie changes, measuing the amount of wasted time + var start time.Time if metrics.EnabledExpensive { - defer func(start time.Time) { s.AccountCommits += time.Since(start) }(time.Now()) + start = time.Now() } root, err = s.trie.Commit(func(leaf []byte, parent common.Hash) error { var account Account @@ -785,8 +787,14 @@ func (s *StateDB) Commit(deleteEmptyObjects bool) (root common.Hash, err error) } return nil }) + if metrics.EnabledExpensive { + s.AccountCommits += time.Since(start) + } // If snapshotting is enabled, update the snapshot tree with this new version if s.snap != nil { + if metrics.EnabledExpensive { + defer func(start time.Time) { s.SnapshotCommits += time.Since(start) }(time.Now()) + } _, parentRoot := s.snap.Info() if err := s.snaps.Update(root, parentRoot, s.snapAccounts, s.snapStorage); err != nil { log.Warn("Failed to update snapshot tree", "from", parentRoot, "to", root, "err", err) diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go index cb85a05b578e..bb5db4ced1e0 100644 --- a/core/state_prefetcher.go +++ b/core/state_prefetcher.go @@ -65,6 +65,8 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c return // Ugh, something went horribly wrong, bail out } } + // All transactions processed, finalize the block to force loading written-only trie paths + statedb.Finalise(true) // TODO(karalabe): should we run this on interrupt too? } // precacheTransaction attempts to apply a transaction to the given state database From aef093b73f1660acff6e1735165f51c7a7088df5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Fri, 9 Aug 2019 12:30:57 +0300 Subject: [PATCH 3/5] core/state: be smarter as to where we read state from --- core/state/journal.go | 10 ++++----- core/state/state_object.go | 46 +++++++++++++++++++++++++++++++------- core/state/statedb.go | 38 ++++++++++++++++--------------- core/state_prefetcher.go | 2 -- 4 files changed, 63 insertions(+), 33 deletions(-) diff --git a/core/state/journal.go b/core/state/journal.go index a03ca57dbc64..075b3a7a578b 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -151,7 +151,7 @@ func (ch resetObjectChange) dirtied() *common.Address { } func (ch suicideChange) revert(s *StateDB) { - obj := s.getStateObject(*ch.account) + obj := s.getStateObject(*ch.account, true) if obj != nil { obj.suicided = ch.prev obj.setBalance(ch.prevbalance) @@ -172,7 +172,7 @@ func (ch touchChange) dirtied() *common.Address { } func (ch balanceChange) revert(s *StateDB) { - s.getStateObject(*ch.account).setBalance(ch.prev) + s.getStateObject(*ch.account, true).setBalance(ch.prev) } func (ch balanceChange) dirtied() *common.Address { @@ -180,7 +180,7 @@ func (ch balanceChange) dirtied() *common.Address { } func (ch nonceChange) revert(s *StateDB) { - s.getStateObject(*ch.account).setNonce(ch.prev) + s.getStateObject(*ch.account, true).setNonce(ch.prev) } func (ch nonceChange) dirtied() *common.Address { @@ -188,7 +188,7 @@ func (ch nonceChange) dirtied() *common.Address { } func (ch codeChange) revert(s *StateDB) { - s.getStateObject(*ch.account).setCode(common.BytesToHash(ch.prevhash), ch.prevcode) + s.getStateObject(*ch.account, true).setCode(common.BytesToHash(ch.prevhash), ch.prevcode) } func (ch codeChange) dirtied() *common.Address { @@ -196,7 +196,7 @@ func (ch codeChange) dirtied() *common.Address { } func (ch storageChange) revert(s *StateDB) { - s.getStateObject(*ch.account).setState(ch.key, ch.prevalue) + s.getStateObject(*ch.account, true).setState(ch.key, ch.prevalue) } func (ch storageChange) dirtied() *common.Address { diff --git a/core/state/state_object.go b/core/state/state_object.go index 9706797c9589..dcc4bbcdbe05 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -162,27 +162,56 @@ func (s *stateObject) getTrie(db Database) Trie { return s.trie } -// GetState retrieves a value from the account storage trie. +// GetState retrieves a value from the account storage. This method will use the +// state snapshot so retrieve the value (opposed to the trie directly) since the +// read doesn't need to pull in log(n) trie nodes in addition from disk. func (s *stateObject) GetState(db Database, key common.Hash) common.Hash { - // If the fake storage is set, only lookup the state here(in the debugging mode) + // If the fake storage is set, only lookup the state here (debugging mode) if s.fakeStorage != nil { return s.fakeStorage[key] } + // No fake storage, retrieve a real object from the storage trie. We're cheating + // a bit here since we know that this method is only using during simple reads. + // As such, we possibly will not write this key, so might as well avoid touching + // trie nodes and pull it directly from the state snapshot. + return s.getState(db, key, true) +} + +// getState retrieves a value from the account's storage, but the caller gets to +// control whether to use the state snapshot or the state trie as the source. For +// simple reads, the snapshot should be used as it's faster. For writes however, +// using the trie will be a bit slower, but will pre-cache nodes needed during +// commit anyway. +func (s *stateObject) getState(db Database, key common.Hash, snapshot bool) common.Hash { // If we have a dirty value for this state entry, return it value, dirty := s.dirtyStorage[key] if dirty { return value } // Otherwise return the entry's original value - return s.GetCommittedState(db, key) + return s.getCommittedState(db, key, snapshot) } -// GetCommittedState retrieves a value from the committed account storage trie. +// GetCommittedState retrieves a value from the committed account storage. This +// method will use the slow trie (opposed to state snapshots) since the committed +// value is only ever used to avoid writes, so we can pre-load trie nodes. func (s *stateObject) GetCommittedState(db Database, key common.Hash) common.Hash { - // If the fake storage is set, only lookup the state here(in the debugging mode) + // If the fake storage is set, only lookup the state here (debugging mode) if s.fakeStorage != nil { return s.fakeStorage[key] } + // No fake storage, retrieve a real object from the storage trie. We're cheating + // a bit here since we know that this method is only using during net sstore gas + // metering. As such, we probably not only read, but also write this key in the + // same transaction, so might as well pre-cache trie nodes on the write path. + return s.getCommittedState(db, key, false) +} + +// getCommittedState retrieves a value from the account's storage, but the caller +// gets to control whether to use the state snapshot or the state trie. For simple +// reads, the snapshot should be used as it's faster. For pre-writes however, using +// the trie will be a bit slower, but will pre-cache nodes needed during commit. +func (s *stateObject) getCommittedState(db Database, key common.Hash, snapshot bool) common.Hash { // If we have the original value cached, return that value, cached := s.originStorage[key] if cached { @@ -193,7 +222,7 @@ func (s *stateObject) GetCommittedState(db Database, key common.Hash) common.Has enc []byte err error ) - if s.db.snap != nil { + if snapshot && s.db.snap != nil { if metrics.EnabledExpensive { defer func(start time.Time) { s.db.SnapshotStorageReads += time.Since(start) }(time.Now()) } @@ -227,8 +256,9 @@ func (s *stateObject) SetState(db Database, key, value common.Hash) { s.fakeStorage[key] = value return } - // If the new value is the same as old, don't set - prev := s.GetState(db, key) + // If the new value is the same as old, don't set (use the trie to cache any + // nodes if we decide to write) + prev := s.getState(db, key, false) if prev == value { return } diff --git a/core/state/statedb.go b/core/state/statedb.go index e817c14dc7ef..defe61d114f4 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -234,19 +234,19 @@ func (self *StateDB) SubRefund(gas uint64) { // Exist reports whether the given account address exists in the state. // Notably this also returns true for suicided accounts. func (self *StateDB) Exist(addr common.Address) bool { - return self.getStateObject(addr) != nil + return self.getStateObject(addr, true) != nil } // Empty returns whether the state object is either non-existent // or empty according to the EIP161 specification (balance = nonce = code = 0) func (self *StateDB) Empty(addr common.Address) bool { - so := self.getStateObject(addr) + so := self.getStateObject(addr, true) return so == nil || so.empty() } // Retrieve the balance from the given address or 0 if object not found func (self *StateDB) GetBalance(addr common.Address) *big.Int { - stateObject := self.getStateObject(addr) + stateObject := self.getStateObject(addr, true) if stateObject != nil { return stateObject.Balance() } @@ -254,7 +254,7 @@ func (self *StateDB) GetBalance(addr common.Address) *big.Int { } func (self *StateDB) GetNonce(addr common.Address) uint64 { - stateObject := self.getStateObject(addr) + stateObject := self.getStateObject(addr, true) if stateObject != nil { return stateObject.Nonce() } @@ -273,7 +273,7 @@ func (self *StateDB) BlockHash() common.Hash { } func (self *StateDB) GetCode(addr common.Address) []byte { - stateObject := self.getStateObject(addr) + stateObject := self.getStateObject(addr, true) if stateObject != nil { return stateObject.Code(self.db) } @@ -281,7 +281,7 @@ func (self *StateDB) GetCode(addr common.Address) []byte { } func (self *StateDB) GetCodeSize(addr common.Address) int { - stateObject := self.getStateObject(addr) + stateObject := self.getStateObject(addr, true) if stateObject == nil { return 0 } @@ -296,7 +296,7 @@ func (self *StateDB) GetCodeSize(addr common.Address) int { } func (self *StateDB) GetCodeHash(addr common.Address) common.Hash { - stateObject := self.getStateObject(addr) + stateObject := self.getStateObject(addr, true) if stateObject == nil { return common.Hash{} } @@ -305,7 +305,7 @@ func (self *StateDB) GetCodeHash(addr common.Address) common.Hash { // GetState retrieves a value from the given account's storage trie. func (self *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { - stateObject := self.getStateObject(addr) + stateObject := self.getStateObject(addr, true) if stateObject != nil { return stateObject.GetState(self.db, hash) } @@ -330,9 +330,11 @@ func (self *StateDB) GetStorageProof(a common.Address, key common.Hash) ([][]byt return [][]byte(proof), err } -// GetCommittedState retrieves a value from the given account's committed storage trie. +// GetCommittedState retrieves a value from the given account's committed storage. +// This method will use the slow trie (opposed to state snapshots) since the value +// committed is only ever used to avoid writes, so we can pre-load trie nodes. func (self *StateDB) GetCommittedState(addr common.Address, hash common.Hash) common.Hash { - stateObject := self.getStateObject(addr) + stateObject := self.getStateObject(addr, false) if stateObject != nil { return stateObject.GetCommittedState(self.db, hash) } @@ -347,7 +349,7 @@ func (self *StateDB) Database() Database { // StorageTrie returns the storage trie of an account. // The return value is a copy and is nil for non-existent accounts. func (self *StateDB) StorageTrie(addr common.Address) Trie { - stateObject := self.getStateObject(addr) + stateObject := self.getStateObject(addr, true) if stateObject == nil { return nil } @@ -356,7 +358,7 @@ func (self *StateDB) StorageTrie(addr common.Address) Trie { } func (self *StateDB) HasSuicided(addr common.Address) bool { - stateObject := self.getStateObject(addr) + stateObject := self.getStateObject(addr, true) if stateObject != nil { return stateObject.suicided } @@ -426,7 +428,7 @@ func (self *StateDB) SetStorage(addr common.Address, storage map[common.Hash]com // The account's state object is still available until the state is committed, // getStateObject will return a non-nil account after Suicide. func (self *StateDB) Suicide(addr common.Address) bool { - stateObject := self.getStateObject(addr) + stateObject := self.getStateObject(addr, false) if stateObject == nil { return false } @@ -485,7 +487,7 @@ func (s *StateDB) deleteStateObject(stateObject *stateObject) { } // Retrieve a state object given by the address. Returns nil if not found. -func (s *StateDB) getStateObject(addr common.Address) (stateObject *stateObject) { +func (s *StateDB) getStateObject(addr common.Address, snapshot bool) (stateObject *stateObject) { // Prefer live objects if obj := s.stateObjects[addr]; obj != nil { if obj.deleted { @@ -495,7 +497,7 @@ func (s *StateDB) getStateObject(addr common.Address) (stateObject *stateObject) } // If no live objects are available, attempt to use snapshots var data Account - if s.snap != nil { + if snapshot && s.snap != nil { if metrics.EnabledExpensive { defer func(start time.Time) { s.SnapshotAccountReads += time.Since(start) }(time.Now()) } @@ -538,7 +540,7 @@ func (self *StateDB) setStateObject(object *stateObject) { // Retrieve a state object or create a new state object if nil. func (self *StateDB) GetOrNewStateObject(addr common.Address) *stateObject { - stateObject := self.getStateObject(addr) + stateObject := self.getStateObject(addr, true) if stateObject == nil || stateObject.deleted { stateObject, _ = self.createObject(addr) } @@ -548,7 +550,7 @@ func (self *StateDB) GetOrNewStateObject(addr common.Address) *stateObject { // createObject creates a new state object. If there is an existing account with // the given address, it is overwritten and returned as the second return value. func (self *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) { - prev = self.getStateObject(addr) + prev = self.getStateObject(addr, false) newobj = newObject(self, addr, Account{}) newobj.setNonce(0) // sets the object to dirty if prev == nil { @@ -578,7 +580,7 @@ func (self *StateDB) CreateAccount(addr common.Address) { } func (db *StateDB) ForEachStorage(addr common.Address, cb func(key, value common.Hash) bool) error { - so := db.getStateObject(addr) + so := db.getStateObject(addr, true) if so == nil { return nil } diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go index bb5db4ced1e0..cb85a05b578e 100644 --- a/core/state_prefetcher.go +++ b/core/state_prefetcher.go @@ -65,8 +65,6 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c return // Ugh, something went horribly wrong, bail out } } - // All transactions processed, finalize the block to force loading written-only trie paths - statedb.Finalise(true) // TODO(karalabe): should we run this on interrupt too? } // precacheTransaction attempts to apply a transaction to the given state database From 9e96671bd1e1c34a5b69efed42f5a584c00952bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Fri, 9 Aug 2019 14:01:28 +0300 Subject: [PATCH 4/5] core/state/snapshot: hack in memory cache into teh disk layer --- core/state/snapshot/difflayer.go | 3 +++ core/state/snapshot/disklayer.go | 36 ++++++++++++++++++++++++++++++-- core/state/snapshot/generate.go | 9 ++++++++ core/state/snapshot/snapshot.go | 18 ++++++++++++++++ 4 files changed, 64 insertions(+), 2 deletions(-) diff --git a/core/state/snapshot/difflayer.go b/core/state/snapshot/difflayer.go index 5ffc4047d9b5..3cdcb6579a51 100644 --- a/core/state/snapshot/difflayer.go +++ b/core/state/snapshot/difflayer.go @@ -254,6 +254,8 @@ func (dl *diffLayer) Cap(layers int, memory uint64) (uint64, uint64) { // Push all the accounts into the database for hash, data := range parent.accountData { rawdb.WriteAccountSnapshot(batch, hash, data) + base.cache.Set(string(hash[:]), data) + if batch.ValueSize() > ethdb.IdealBatchSize { if err := batch.Write(); err != nil { log.Crit("Failed to write account snapshot", "err", err) @@ -265,6 +267,7 @@ func (dl *diffLayer) Cap(layers int, memory uint64) (uint64, uint64) { for accountHash, storage := range parent.storageData { for storageHash, data := range storage { rawdb.WriteStorageSnapshot(batch, accountHash, storageHash, data) + base.cache.Set(string(append(accountHash[:], storageHash[:]...)), data) } if batch.ValueSize() > ethdb.IdealBatchSize { if err := batch.Write(); err != nil { diff --git a/core/state/snapshot/disklayer.go b/core/state/snapshot/disklayer.go index d6a624df19f4..595675d7f2cb 100644 --- a/core/state/snapshot/disklayer.go +++ b/core/state/snapshot/disklayer.go @@ -17,6 +17,7 @@ package snapshot import ( + "github.com/allegro/bigcache" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/ethdb" @@ -27,6 +28,7 @@ import ( type diskLayer struct { journal string // Path of the snapshot journal to use on shutdown db ethdb.KeyValueStore // Key-value store containing the base snapshot + cache *bigcache.BigCache // Cache to avoid hitting the disk for direct access number uint64 // Block number of the base snapshot root common.Hash // Root hash of the base snapshot @@ -54,13 +56,43 @@ func (dl *diskLayer) Account(hash common.Hash) *Account { // AccountRLP directly retrieves the account RLP associated with a particular // hash in the snapshot slim data format. func (dl *diskLayer) AccountRLP(hash common.Hash) []byte { - return rawdb.ReadAccountSnapshot(dl.db, hash) + key := string(hash[:]) + + // Try to retrieve the account from the memory cache + if blob, err := dl.cache.Get(key); err == nil { + snapshotCleanHitMeter.Mark(1) + snapshotCleanReadMeter.Mark(int64(len(blob))) + return blob + } + // Cache doesn't contain account, pull from disk and cache for later + blob := rawdb.ReadAccountSnapshot(dl.db, hash) + dl.cache.Set(key, blob) + + snapshotCleanMissMeter.Mark(1) + snapshotCleanWriteMeter.Mark(int64(len(blob))) + + return blob } // Storage directly retrieves the storage data associated with a particular hash, // within a particular account. func (dl *diskLayer) Storage(accountHash, storageHash common.Hash) []byte { - return rawdb.ReadStorageSnapshot(dl.db, accountHash, storageHash) + key := string(append(accountHash[:], storageHash[:]...)) + + // Try to retrieve the storage slot from the memory cache + if blob, err := dl.cache.Get(key); err == nil { + snapshotCleanHitMeter.Mark(1) + snapshotCleanReadMeter.Mark(int64(len(blob))) + return blob + } + // Cache doesn't contain storage slot, pull from disk and cache for later + blob := rawdb.ReadStorageSnapshot(dl.db, accountHash, storageHash) + dl.cache.Set(key, blob) + + snapshotCleanMissMeter.Mark(1) + snapshotCleanWriteMeter.Mark(int64(len(blob))) + + return blob } // Update creates a new layer on top of the existing snapshot diff tree with diff --git a/core/state/snapshot/generate.go b/core/state/snapshot/generate.go index ef7c818c9938..ca9c4e18c94a 100644 --- a/core/state/snapshot/generate.go +++ b/core/state/snapshot/generate.go @@ -22,6 +22,7 @@ import ( "math/big" "time" + "github.com/allegro/bigcache" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/crypto" @@ -188,9 +189,17 @@ func generateSnapshot(db ethdb.KeyValueStore, journal string, headNumber uint64, return nil, err } // New snapshot generated, construct a brand new base layer + cache, _ := bigcache.NewBigCache(bigcache.Config{ // TODO(karalabe): dedup + Shards: 1024, + LifeWindow: time.Hour, + MaxEntriesInWindow: 512 * 1024, + MaxEntrySize: 512, + HardMaxCacheSize: 512, + }) return &diskLayer{ journal: journal, db: db, + cache: cache, number: headNumber, root: headRoot, }, nil diff --git a/core/state/snapshot/snapshot.go b/core/state/snapshot/snapshot.go index e7a029ffaabc..59a7a73056d4 100644 --- a/core/state/snapshot/snapshot.go +++ b/core/state/snapshot/snapshot.go @@ -22,11 +22,21 @@ import ( "fmt" "os" "sync" + "time" + "github.com/allegro/bigcache" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/metrics" +) + +var ( + snapshotCleanHitMeter = metrics.NewRegisteredMeter("state/snapshot/clean/hit", nil) + snapshotCleanMissMeter = metrics.NewRegisteredMeter("state/snapshot/clean/miss", nil) + snapshotCleanReadMeter = metrics.NewRegisteredMeter("state/snapshot/clean/read", nil) + snapshotCleanWriteMeter = metrics.NewRegisteredMeter("state/snapshot/clean/write", nil) ) // Snapshot represents the functionality supported by a snapshot storage layer. @@ -190,9 +200,17 @@ func loadSnapshot(db ethdb.KeyValueStore, journal string, headNumber uint64, hea if root == (common.Hash{}) { return nil, errors.New("missing or corrupted snapshot") } + cache, _ := bigcache.NewBigCache(bigcache.Config{ // TODO(karalabe): dedup + Shards: 1024, + LifeWindow: time.Hour, + MaxEntriesInWindow: 512 * 1024, + MaxEntrySize: 512, + HardMaxCacheSize: 512, + }) base := &diskLayer{ journal: journal, db: db, + cache: cache, number: number, root: root, } From 9265a267cdc6f4e545f3a4320c3072f4ec3ee29d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Mon, 12 Aug 2019 10:15:54 +0300 Subject: [PATCH 5/5] Revert "core/state: be smarter as to where we read state from" This reverts commit aef093b73f1660acff6e1735165f51c7a7088df5. --- core/state/journal.go | 10 ++++----- core/state/state_object.go | 46 +++++++------------------------------- core/state/statedb.go | 38 +++++++++++++++---------------- core/state_prefetcher.go | 2 ++ 4 files changed, 33 insertions(+), 63 deletions(-) diff --git a/core/state/journal.go b/core/state/journal.go index 075b3a7a578b..a03ca57dbc64 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -151,7 +151,7 @@ func (ch resetObjectChange) dirtied() *common.Address { } func (ch suicideChange) revert(s *StateDB) { - obj := s.getStateObject(*ch.account, true) + obj := s.getStateObject(*ch.account) if obj != nil { obj.suicided = ch.prev obj.setBalance(ch.prevbalance) @@ -172,7 +172,7 @@ func (ch touchChange) dirtied() *common.Address { } func (ch balanceChange) revert(s *StateDB) { - s.getStateObject(*ch.account, true).setBalance(ch.prev) + s.getStateObject(*ch.account).setBalance(ch.prev) } func (ch balanceChange) dirtied() *common.Address { @@ -180,7 +180,7 @@ func (ch balanceChange) dirtied() *common.Address { } func (ch nonceChange) revert(s *StateDB) { - s.getStateObject(*ch.account, true).setNonce(ch.prev) + s.getStateObject(*ch.account).setNonce(ch.prev) } func (ch nonceChange) dirtied() *common.Address { @@ -188,7 +188,7 @@ func (ch nonceChange) dirtied() *common.Address { } func (ch codeChange) revert(s *StateDB) { - s.getStateObject(*ch.account, true).setCode(common.BytesToHash(ch.prevhash), ch.prevcode) + s.getStateObject(*ch.account).setCode(common.BytesToHash(ch.prevhash), ch.prevcode) } func (ch codeChange) dirtied() *common.Address { @@ -196,7 +196,7 @@ func (ch codeChange) dirtied() *common.Address { } func (ch storageChange) revert(s *StateDB) { - s.getStateObject(*ch.account, true).setState(ch.key, ch.prevalue) + s.getStateObject(*ch.account).setState(ch.key, ch.prevalue) } func (ch storageChange) dirtied() *common.Address { diff --git a/core/state/state_object.go b/core/state/state_object.go index dcc4bbcdbe05..9706797c9589 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -162,56 +162,27 @@ func (s *stateObject) getTrie(db Database) Trie { return s.trie } -// GetState retrieves a value from the account storage. This method will use the -// state snapshot so retrieve the value (opposed to the trie directly) since the -// read doesn't need to pull in log(n) trie nodes in addition from disk. +// GetState retrieves a value from the account storage trie. func (s *stateObject) GetState(db Database, key common.Hash) common.Hash { - // If the fake storage is set, only lookup the state here (debugging mode) + // If the fake storage is set, only lookup the state here(in the debugging mode) if s.fakeStorage != nil { return s.fakeStorage[key] } - // No fake storage, retrieve a real object from the storage trie. We're cheating - // a bit here since we know that this method is only using during simple reads. - // As such, we possibly will not write this key, so might as well avoid touching - // trie nodes and pull it directly from the state snapshot. - return s.getState(db, key, true) -} - -// getState retrieves a value from the account's storage, but the caller gets to -// control whether to use the state snapshot or the state trie as the source. For -// simple reads, the snapshot should be used as it's faster. For writes however, -// using the trie will be a bit slower, but will pre-cache nodes needed during -// commit anyway. -func (s *stateObject) getState(db Database, key common.Hash, snapshot bool) common.Hash { // If we have a dirty value for this state entry, return it value, dirty := s.dirtyStorage[key] if dirty { return value } // Otherwise return the entry's original value - return s.getCommittedState(db, key, snapshot) + return s.GetCommittedState(db, key) } -// GetCommittedState retrieves a value from the committed account storage. This -// method will use the slow trie (opposed to state snapshots) since the committed -// value is only ever used to avoid writes, so we can pre-load trie nodes. +// GetCommittedState retrieves a value from the committed account storage trie. func (s *stateObject) GetCommittedState(db Database, key common.Hash) common.Hash { - // If the fake storage is set, only lookup the state here (debugging mode) + // If the fake storage is set, only lookup the state here(in the debugging mode) if s.fakeStorage != nil { return s.fakeStorage[key] } - // No fake storage, retrieve a real object from the storage trie. We're cheating - // a bit here since we know that this method is only using during net sstore gas - // metering. As such, we probably not only read, but also write this key in the - // same transaction, so might as well pre-cache trie nodes on the write path. - return s.getCommittedState(db, key, false) -} - -// getCommittedState retrieves a value from the account's storage, but the caller -// gets to control whether to use the state snapshot or the state trie. For simple -// reads, the snapshot should be used as it's faster. For pre-writes however, using -// the trie will be a bit slower, but will pre-cache nodes needed during commit. -func (s *stateObject) getCommittedState(db Database, key common.Hash, snapshot bool) common.Hash { // If we have the original value cached, return that value, cached := s.originStorage[key] if cached { @@ -222,7 +193,7 @@ func (s *stateObject) getCommittedState(db Database, key common.Hash, snapshot b enc []byte err error ) - if snapshot && s.db.snap != nil { + if s.db.snap != nil { if metrics.EnabledExpensive { defer func(start time.Time) { s.db.SnapshotStorageReads += time.Since(start) }(time.Now()) } @@ -256,9 +227,8 @@ func (s *stateObject) SetState(db Database, key, value common.Hash) { s.fakeStorage[key] = value return } - // If the new value is the same as old, don't set (use the trie to cache any - // nodes if we decide to write) - prev := s.getState(db, key, false) + // If the new value is the same as old, don't set + prev := s.GetState(db, key) if prev == value { return } diff --git a/core/state/statedb.go b/core/state/statedb.go index defe61d114f4..e817c14dc7ef 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -234,19 +234,19 @@ func (self *StateDB) SubRefund(gas uint64) { // Exist reports whether the given account address exists in the state. // Notably this also returns true for suicided accounts. func (self *StateDB) Exist(addr common.Address) bool { - return self.getStateObject(addr, true) != nil + return self.getStateObject(addr) != nil } // Empty returns whether the state object is either non-existent // or empty according to the EIP161 specification (balance = nonce = code = 0) func (self *StateDB) Empty(addr common.Address) bool { - so := self.getStateObject(addr, true) + so := self.getStateObject(addr) return so == nil || so.empty() } // Retrieve the balance from the given address or 0 if object not found func (self *StateDB) GetBalance(addr common.Address) *big.Int { - stateObject := self.getStateObject(addr, true) + stateObject := self.getStateObject(addr) if stateObject != nil { return stateObject.Balance() } @@ -254,7 +254,7 @@ func (self *StateDB) GetBalance(addr common.Address) *big.Int { } func (self *StateDB) GetNonce(addr common.Address) uint64 { - stateObject := self.getStateObject(addr, true) + stateObject := self.getStateObject(addr) if stateObject != nil { return stateObject.Nonce() } @@ -273,7 +273,7 @@ func (self *StateDB) BlockHash() common.Hash { } func (self *StateDB) GetCode(addr common.Address) []byte { - stateObject := self.getStateObject(addr, true) + stateObject := self.getStateObject(addr) if stateObject != nil { return stateObject.Code(self.db) } @@ -281,7 +281,7 @@ func (self *StateDB) GetCode(addr common.Address) []byte { } func (self *StateDB) GetCodeSize(addr common.Address) int { - stateObject := self.getStateObject(addr, true) + stateObject := self.getStateObject(addr) if stateObject == nil { return 0 } @@ -296,7 +296,7 @@ func (self *StateDB) GetCodeSize(addr common.Address) int { } func (self *StateDB) GetCodeHash(addr common.Address) common.Hash { - stateObject := self.getStateObject(addr, true) + stateObject := self.getStateObject(addr) if stateObject == nil { return common.Hash{} } @@ -305,7 +305,7 @@ func (self *StateDB) GetCodeHash(addr common.Address) common.Hash { // GetState retrieves a value from the given account's storage trie. func (self *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { - stateObject := self.getStateObject(addr, true) + stateObject := self.getStateObject(addr) if stateObject != nil { return stateObject.GetState(self.db, hash) } @@ -330,11 +330,9 @@ func (self *StateDB) GetStorageProof(a common.Address, key common.Hash) ([][]byt return [][]byte(proof), err } -// GetCommittedState retrieves a value from the given account's committed storage. -// This method will use the slow trie (opposed to state snapshots) since the value -// committed is only ever used to avoid writes, so we can pre-load trie nodes. +// GetCommittedState retrieves a value from the given account's committed storage trie. func (self *StateDB) GetCommittedState(addr common.Address, hash common.Hash) common.Hash { - stateObject := self.getStateObject(addr, false) + stateObject := self.getStateObject(addr) if stateObject != nil { return stateObject.GetCommittedState(self.db, hash) } @@ -349,7 +347,7 @@ func (self *StateDB) Database() Database { // StorageTrie returns the storage trie of an account. // The return value is a copy and is nil for non-existent accounts. func (self *StateDB) StorageTrie(addr common.Address) Trie { - stateObject := self.getStateObject(addr, true) + stateObject := self.getStateObject(addr) if stateObject == nil { return nil } @@ -358,7 +356,7 @@ func (self *StateDB) StorageTrie(addr common.Address) Trie { } func (self *StateDB) HasSuicided(addr common.Address) bool { - stateObject := self.getStateObject(addr, true) + stateObject := self.getStateObject(addr) if stateObject != nil { return stateObject.suicided } @@ -428,7 +426,7 @@ func (self *StateDB) SetStorage(addr common.Address, storage map[common.Hash]com // The account's state object is still available until the state is committed, // getStateObject will return a non-nil account after Suicide. func (self *StateDB) Suicide(addr common.Address) bool { - stateObject := self.getStateObject(addr, false) + stateObject := self.getStateObject(addr) if stateObject == nil { return false } @@ -487,7 +485,7 @@ func (s *StateDB) deleteStateObject(stateObject *stateObject) { } // Retrieve a state object given by the address. Returns nil if not found. -func (s *StateDB) getStateObject(addr common.Address, snapshot bool) (stateObject *stateObject) { +func (s *StateDB) getStateObject(addr common.Address) (stateObject *stateObject) { // Prefer live objects if obj := s.stateObjects[addr]; obj != nil { if obj.deleted { @@ -497,7 +495,7 @@ func (s *StateDB) getStateObject(addr common.Address, snapshot bool) (stateObjec } // If no live objects are available, attempt to use snapshots var data Account - if snapshot && s.snap != nil { + if s.snap != nil { if metrics.EnabledExpensive { defer func(start time.Time) { s.SnapshotAccountReads += time.Since(start) }(time.Now()) } @@ -540,7 +538,7 @@ func (self *StateDB) setStateObject(object *stateObject) { // Retrieve a state object or create a new state object if nil. func (self *StateDB) GetOrNewStateObject(addr common.Address) *stateObject { - stateObject := self.getStateObject(addr, true) + stateObject := self.getStateObject(addr) if stateObject == nil || stateObject.deleted { stateObject, _ = self.createObject(addr) } @@ -550,7 +548,7 @@ func (self *StateDB) GetOrNewStateObject(addr common.Address) *stateObject { // createObject creates a new state object. If there is an existing account with // the given address, it is overwritten and returned as the second return value. func (self *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) { - prev = self.getStateObject(addr, false) + prev = self.getStateObject(addr) newobj = newObject(self, addr, Account{}) newobj.setNonce(0) // sets the object to dirty if prev == nil { @@ -580,7 +578,7 @@ func (self *StateDB) CreateAccount(addr common.Address) { } func (db *StateDB) ForEachStorage(addr common.Address, cb func(key, value common.Hash) bool) error { - so := db.getStateObject(addr, true) + so := db.getStateObject(addr) if so == nil { return nil } diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go index cb85a05b578e..bb5db4ced1e0 100644 --- a/core/state_prefetcher.go +++ b/core/state_prefetcher.go @@ -65,6 +65,8 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c return // Ugh, something went horribly wrong, bail out } } + // All transactions processed, finalize the block to force loading written-only trie paths + statedb.Finalise(true) // TODO(karalabe): should we run this on interrupt too? } // precacheTransaction attempts to apply a transaction to the given state database