From 9ff02140a8bce741af148917ee36fccb7f412ed2 Mon Sep 17 00:00:00 2001 From: Brindrajsinh-Chauhan Date: Wed, 17 Apr 2024 14:27:47 -0400 Subject: [PATCH] update disk root more frequently --- core/blockchain.go | 27 +++++--- core/state/snapshot/difflayer.go | 3 + core/state/snapshot/snapshot.go | 50 +++++++++----- core/state/snapshot/snapshot_test.go | 97 ++++++++++++++++++++++++++++ eth/backend.go | 20 +++--- eth/ethconfig/config.go | 52 ++++++++------- eth/ethconfig/gen_config.go | 12 ++++ 7 files changed, 203 insertions(+), 58 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 56e00e85b6..2f03b9d97b 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -140,6 +140,9 @@ type CacheConfig struct { StateHistory uint64 // Number of blocks from head whose state histories are reserved. StateScheme string // Scheme used to store ethereum states and merkle tree nodes on top + EnableDiskRootInterval bool // Enable update disk root when time threshold is reached. Disabled by default + DiskRootThreshold time.Duration // Time threshold after which to flush the layers to disk + SnapshotNoBuild bool // Whether the background generation is allowed SnapshotWait bool // Wait for snapshot construction on startup. TODO(karalabe): This is a dirty hack for testing, nuke it } @@ -168,12 +171,14 @@ func (c *CacheConfig) triedbConfig(isVerkle bool) *triedb.Config { // defaultCacheConfig are the default caching values if none are specified by the // user (also used during testing). var defaultCacheConfig = &CacheConfig{ - TrieCleanLimit: 256, - TrieDirtyLimit: 256, - TrieTimeLimit: 5 * time.Minute, - SnapshotLimit: 256, - SnapshotWait: true, - StateScheme: rawdb.HashScheme, + TrieCleanLimit: 256, + TrieDirtyLimit: 256, + TrieTimeLimit: 5 * time.Minute, + SnapshotLimit: 256, + SnapshotWait: true, + StateScheme: rawdb.HashScheme, + EnableDiskRootInterval: false, + DiskRootThreshold: 60 * time.Minute, } // DefaultCacheConfigWithScheme returns a deep copied default cache config with @@ -441,10 +446,12 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis recover = true } snapconfig := snapshot.Config{ - CacheSize: bc.cacheConfig.SnapshotLimit, - Recovery: recover, - NoBuild: bc.cacheConfig.SnapshotNoBuild, - AsyncBuild: !bc.cacheConfig.SnapshotWait, + CacheSize: bc.cacheConfig.SnapshotLimit, + Recovery: recover, + NoBuild: bc.cacheConfig.SnapshotNoBuild, + AsyncBuild: !bc.cacheConfig.SnapshotWait, + EnableDiskRootInterval: bc.cacheConfig.EnableDiskRootInterval, + DiskRootThreshold: bc.cacheConfig.DiskRootThreshold, } bc.snaps, _ = snapshot.New(snapconfig, bc.db, bc.triedb, head.Root) } diff --git a/core/state/snapshot/difflayer.go b/core/state/snapshot/difflayer.go index 779c1ea98c..eabad86f65 100644 --- a/core/state/snapshot/difflayer.go +++ b/core/state/snapshot/difflayer.go @@ -76,6 +76,9 @@ var ( bloomDestructHasherOffset = 0 bloomAccountHasherOffset = 0 bloomStorageHasherOffset = 0 + + // Setting a minimum to prevent very low input from user + minTimeThreshold = 1 * time.Minute ) func init() { diff --git a/core/state/snapshot/snapshot.go b/core/state/snapshot/snapshot.go index 89a4c16c20..1bade13077 100644 --- a/core/state/snapshot/snapshot.go +++ b/core/state/snapshot/snapshot.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "sync" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" @@ -150,10 +151,12 @@ type snapshot interface { // Config includes the configurations for snapshots. type Config struct { - CacheSize int // Megabytes permitted to use for read caches - Recovery bool // Indicator that the snapshots is in the recovery mode - NoBuild bool // Indicator that the snapshots generation is disallowed - AsyncBuild bool // The snapshot generation is allowed to be constructed asynchronously + CacheSize int // Megabytes permitted to use for read caches + Recovery bool // Indicator that the snapshots is in the recovery mode + NoBuild bool // Indicator that the snapshots generation is disallowed + AsyncBuild bool // The snapshot generation is allowed to be constructed asynchronously + EnableDiskRootInterval bool // The disk root is allowed to update after a time threshold + DiskRootThreshold time.Duration // The threshold to update disk root } // Tree is an Ethereum state snapshot tree. It consists of one persistent base @@ -166,11 +169,12 @@ type Config struct { // storage data to avoid expensive multi-level trie lookups; and to allow sorted, // cheap iteration of the account/storage tries for sync aid. type Tree struct { - config Config // Snapshots configurations - diskdb ethdb.KeyValueStore // Persistent database to store the snapshot - triedb *triedb.Database // In-memory cache to access the trie through - layers map[common.Hash]snapshot // Collection of all known layers - lock sync.RWMutex + config Config // Snapshots configurations + diskdb ethdb.KeyValueStore // Persistent database to store the snapshot + triedb *triedb.Database // In-memory cache to access the trie through + layers map[common.Hash]snapshot // Collection of all known layers + lock sync.RWMutex + baseTime time.Time // Reference to calculate the time threshold // Test hooks onFlatten func() // Hook invoked when the bottom most diff layers are flattened @@ -195,11 +199,18 @@ type Tree struct { func New(config Config, diskdb ethdb.KeyValueStore, triedb *triedb.Database, root common.Hash) (*Tree, error) { // Create a new, empty snapshot tree snap := &Tree{ - config: config, - diskdb: diskdb, - triedb: triedb, - layers: make(map[common.Hash]snapshot), + config: config, + diskdb: diskdb, + triedb: triedb, + layers: make(map[common.Hash]snapshot), + baseTime: time.Now(), } + + // If user provided threshold smaller than minimum, set to minimum + if config.DiskRootThreshold < minTimeThreshold { + config.DiskRootThreshold = minTimeThreshold + } + // Attempt to load a previously persisted snapshot and rebuild one if failed head, disabled, err := loadSnapshot(diskdb, triedb, root, config.CacheSize, config.Recovery, config.NoBuild) if disabled { @@ -500,7 +511,7 @@ func (t *Tree) cap(diff *diffLayer, layers int) *diskLayer { t.onFlatten() } diff.parent = flattened - if flattened.memory < aggregatorMemoryLimit { + if (flattened.memory < aggregatorMemoryLimit) && !t.isPastThreshold() { // Accumulator layer is smaller than the limit, so we can abort, unless // there's a snapshot being generated currently. In that case, the trie // will move from underneath the generator so we **must** merge all the @@ -512,13 +523,16 @@ func (t *Tree) cap(diff *diffLayer, layers int) *diskLayer { default: panic(fmt.Sprintf("unknown data layer: %T", parent)) } - // If the bottom-most layer is larger than our memory cap, persist to disk + // If the bottom-most layer is larger than our memory cap or time elapsed greater than threshold, persist to disk bottom := diff.parent.(*diffLayer) bottom.lock.RLock() base := diffToDisk(bottom) bottom.lock.RUnlock() + // Reset the time reference for next update + t.baseTime = time.Now() + t.layers[base.root] = base diff.parent = base return base @@ -885,3 +899,9 @@ func (t *Tree) Size() (diffs common.StorageSize, buf common.StorageSize) { } return size, 0 } + +// Check if time threshold is enabled and if the time elapsed is more than the threshold. +// if false, we can abort else we proceed to update the disk root +func (t *Tree) isPastThreshold() bool { + return (t.config.EnableDiskRootInterval && (time.Since(t.baseTime) > t.config.DiskRootThreshold)) +} diff --git a/core/state/snapshot/snapshot_test.go b/core/state/snapshot/snapshot_test.go index a9ab3eaea3..69aeed24e3 100644 --- a/core/state/snapshot/snapshot_test.go +++ b/core/state/snapshot/snapshot_test.go @@ -330,6 +330,103 @@ func TestPostCapBasicDataAccess(t *testing.T) { } } +func TestForceSnapRootCaps(t *testing.T) { + // setAccount is a helper to construct a random account entry and assign it to + // an account slot in a snapshot + setAccount := func(accKey string) map[common.Hash][]byte { + return map[common.Hash][]byte{ + common.HexToHash(accKey): randomAccount(), + } + } + makeRoot := func(height uint64) common.Hash { + var buffer [8]byte + binary.BigEndian.PutUint64(buffer[:], height) + return common.BytesToHash(buffer[:]) + } + var ( + last = common.HexToHash("0x01") + head common.Hash + ) + // Create a starting base layer and a snapshot tree out of it + base := &diskLayer{ + diskdb: rawdb.NewMemoryDatabase(), + root: common.HexToHash("0x01"), + cache: fastcache.New(1024 * 500), + } + snaps := &Tree{ + layers: map[common.Hash]snapshot{ + base.root: base, + }, + baseTime: time.Now(), + config: Config{ + EnableDiskRootInterval: true, + DiskRootThreshold: 30 * time.Minute, + }, + } + + // adding layers to the tree more than 128 + for i := 0; i < 150; i++ { + head = makeRoot(uint64(i + 2)) + snaps.Update(head, last, nil, setAccount(fmt.Sprintf("%d", i+2)), nil) + last = head + } + + // Currently the tree should have all the 150 layers + disk layer + if count := len(snaps.layers); count != 151 { + t.Errorf("Unexpected number of layers - count %d, expected 151", count) + } + + // Now capping without time threshold reached + if err := snaps.Cap(head, 128); err != nil { + t.Error("Error while capping layers", err) + } + + // the diskRoot should not have been updated yet since + // the time and the memory limit have not been reached + if firstDiskRoot := snaps.diskRoot(); firstDiskRoot != base.root { + t.Errorf("Disk root should not have updated at this point - actual: %s, expected: %s", firstDiskRoot, base.root) + } + + // layers beyond 128 should be flattened into one so total layers + // 128 top layers + 1 flattened layer + disk layer + if newLayers := len(snaps.layers); newLayers != 130 { + t.Errorf("Unexpected number of layers after flatten - count: %d, expected: 130", newLayers) + } + + // Setting baseTime 100 min behind to trigger disk update + snaps.baseTime = time.Now().Add(time.Duration(-100) * time.Minute) + + // Check if forceSnapshot disabled, time threshold should not trigger snapshot + snaps.config.EnableDiskRootInterval = false + if err := snaps.Cap(head, 128); err != nil { + t.Error("Error while capping layers", err) + } + + // the diskRoot should not have been updated yet since + // the force snapshot is disabled + if firstDiskRoot := snaps.diskRoot(); firstDiskRoot != base.root { + t.Errorf("Disk root should not have updated at this point - actual: %s, expected: %s", firstDiskRoot, base.root) + } + + // Re-enable forceSnapshot, time threshold should trigger snapshot + snaps.config.EnableDiskRootInterval = true + if err := snaps.Cap(head, 128); err != nil { + t.Error("Error while capping layers", err) + } + + // the diskRoot should have been updated now since the time and the memory limit + // have not been reached + if updatedDiskRoot := snaps.diskRoot(); updatedDiskRoot == base.root { + t.Errorf("Disk root did not update at this point - actual: %s", updatedDiskRoot) + } + + // disk layer should be updated now to be the flattened layer + // 128 top layers + disk layer + if newLayers := len(snaps.layers); newLayers != 129 { + t.Errorf("Unexpected number of layers after flatten - count: %d, expected: 130", newLayers) + } +} + // TestSnaphots tests the functionality for retrieving the snapshot // with given head root and the desired depth. func TestSnaphots(t *testing.T) { diff --git a/eth/backend.go b/eth/backend.go index e616b5f2f1..7297cf321f 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -190,15 +190,17 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { EnablePreimageRecording: config.EnablePreimageRecording, } cacheConfig = &core.CacheConfig{ - TrieCleanLimit: config.TrieCleanCache, - TrieCleanNoPrefetch: config.NoPrefetch, - TrieDirtyLimit: config.TrieDirtyCache, - TrieDirtyDisabled: config.NoPruning, - TrieTimeLimit: config.TrieTimeout, - SnapshotLimit: config.SnapshotCache, - Preimages: config.Preimages, - StateHistory: config.StateHistory, - StateScheme: scheme, + TrieCleanLimit: config.TrieCleanCache, + TrieCleanNoPrefetch: config.NoPrefetch, + TrieDirtyLimit: config.TrieDirtyCache, + TrieDirtyDisabled: config.NoPruning, + TrieTimeLimit: config.TrieTimeout, + SnapshotLimit: config.SnapshotCache, + Preimages: config.Preimages, + StateHistory: config.StateHistory, + StateScheme: scheme, + EnableDiskRootInterval: config.EnableDiskRootInterval, + DiskRootThreshold: config.DiskRootThreshold, } ) if config.VMTrace != "" { diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index f36f212d9c..c56b532986 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -48,25 +48,27 @@ var FullNodeGPO = gasprice.Config{ // Defaults contains default settings for use on the Ethereum main net. var Defaults = Config{ - SyncMode: downloader.SnapSync, - NetworkId: 0, // enable auto configuration of networkID == chainID - TxLookupLimit: 2350000, - TransactionHistory: 2350000, - StateHistory: params.FullImmutabilityThreshold, - LightPeers: 100, - DatabaseCache: 512, - TrieCleanCache: 154, - TrieDirtyCache: 256, - TrieTimeout: 60 * time.Minute, - SnapshotCache: 102, - FilterLogCacheSize: 32, - Miner: miner.DefaultConfig, - TxPool: legacypool.DefaultConfig, - BlobPool: blobpool.DefaultConfig, - RPCGasCap: 50000000, - RPCEVMTimeout: 5 * time.Second, - GPO: FullNodeGPO, - RPCTxFeeCap: 1, // 1 ether + SyncMode: downloader.SnapSync, + NetworkId: 0, // enable auto configuration of networkID == chainID + TxLookupLimit: 2350000, + TransactionHistory: 2350000, + StateHistory: params.FullImmutabilityThreshold, + LightPeers: 100, + DatabaseCache: 512, + TrieCleanCache: 154, + TrieDirtyCache: 256, + TrieTimeout: 60 * time.Minute, + SnapshotCache: 102, + EnableDiskRootInterval: false, + DiskRootThreshold: 60 * time.Minute, + FilterLogCacheSize: 32, + Miner: miner.DefaultConfig, + TxPool: legacypool.DefaultConfig, + BlobPool: blobpool.DefaultConfig, + RPCGasCap: 50000000, + RPCEVMTimeout: 5 * time.Second, + GPO: FullNodeGPO, + RPCTxFeeCap: 1, // 1 ether } //go:generate go run github.com/fjl/gencodec -type Config -formats toml -out gen_config.go @@ -119,11 +121,13 @@ type Config struct { DatabaseCache int DatabaseFreezer string - TrieCleanCache int - TrieDirtyCache int - TrieTimeout time.Duration - SnapshotCache int - Preimages bool + TrieCleanCache int + TrieDirtyCache int + TrieTimeout time.Duration + SnapshotCache int + Preimages bool + EnableDiskRootInterval bool + DiskRootThreshold time.Duration // This is the number of blocks for which logs will be cached in the filter system. FilterLogCacheSize int diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go index b8b9eee294..bb131b6550 100644 --- a/eth/ethconfig/gen_config.go +++ b/eth/ethconfig/gen_config.go @@ -44,6 +44,8 @@ func (c Config) MarshalTOML() (interface{}, error) { TrieTimeout time.Duration SnapshotCache int Preimages bool + EnableDiskRootInterval bool + DiskRootThreshold time.Duration FilterLogCacheSize int Miner miner.Config TxPool legacypool.Config @@ -87,6 +89,8 @@ func (c Config) MarshalTOML() (interface{}, error) { enc.TrieTimeout = c.TrieTimeout enc.SnapshotCache = c.SnapshotCache enc.Preimages = c.Preimages + enc.EnableDiskRootInterval = c.EnableDiskRootInterval + enc.DiskRootThreshold = c.DiskRootThreshold enc.FilterLogCacheSize = c.FilterLogCacheSize enc.Miner = c.Miner enc.TxPool = c.TxPool @@ -134,6 +138,8 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { TrieTimeout *time.Duration SnapshotCache *int Preimages *bool + EnableDiskRootInterval *bool + DiskRootThreshold *time.Duration FilterLogCacheSize *int Miner *miner.Config TxPool *legacypool.Config @@ -234,6 +240,12 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { if dec.Preimages != nil { c.Preimages = *dec.Preimages } + if dec.EnableDiskRootInterval != nil { + c.EnableDiskRootInterval = *dec.EnableDiskRootInterval + } + if dec.DiskRootThreshold != nil { + c.DiskRootThreshold = *dec.DiskRootThreshold + } if dec.FilterLogCacheSize != nil { c.FilterLogCacheSize = *dec.FilterLogCacheSize }