From fc19bf5808b0e84aa267c18e7d90ac8eb92c4f0a Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Fri, 18 Aug 2023 17:16:00 +0800 Subject: [PATCH 01/65] flags: add state expiry enable flag; --- cmd/geth/config.go | 7 +++++++ cmd/geth/main.go | 5 +++++ cmd/utils/flags.go | 9 +++++++++ eth/ethconfig/config.go | 3 +++ internal/flags/categories.go | 1 + 5 files changed, 25 insertions(+) diff --git a/cmd/geth/config.go b/cmd/geth/config.go index b1744c8040..8efce7dec5 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -157,6 +157,7 @@ func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) { cfg.Ethstats.URL = ctx.String(utils.EthStatsURLFlag.Name) } applyMetricConfig(ctx, &cfg) + applyStateExpiryConfig(ctx, &cfg) return stack, cfg } @@ -270,6 +271,12 @@ func applyMetricConfig(ctx *cli.Context, cfg *gethConfig) { } } +func applyStateExpiryConfig(ctx *cli.Context, cfg *gethConfig) { + if ctx.IsSet(utils.StateExpiryEnableFlag.Name) { + cfg.Eth.StateExpiryEnable = ctx.Bool(utils.StateExpiryEnableFlag.Name) + } +} + func deprecated(field string) bool { switch field { case "ethconfig.Config.EVMInterpreter": diff --git a/cmd/geth/main.go b/cmd/geth/main.go index b1b08588e2..c1434bb755 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -215,6 +215,10 @@ var ( utils.MetricsInfluxDBBucketFlag, utils.MetricsInfluxDBOrganizationFlag, } + + stateExpiryFlags = []cli.Flag{ + utils.StateExpiryEnableFlag, + } ) var app = flags.NewApp("the go-ethereum command line interface") @@ -265,6 +269,7 @@ func init() { consoleFlags, debug.Flags, metricsFlags, + stateExpiryFlags, ) app.Before = func(ctx *cli.Context) error { diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 2f23414ee8..d80a39c7bc 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1118,6 +1118,15 @@ var ( } ) +var ( + // State Expiry Flags + StateExpiryEnableFlag = &cli.BoolFlag{ + Name: "state-expiry", + Usage: "Enable state expiry, it will mark state's epoch meta and prune un-accessed states later", + Category: flags.StateExpiryCategory, + } +) + func init() { if rawdb.PebbleEnabled { DatabasePathFlags = append(DatabasePathFlags, DBEngineFlag) diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 384996e7fc..9b0b564884 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -103,6 +103,9 @@ type Config struct { // to turn it on. DisablePeerTxBroadcast bool + // state expiry configs + StateExpiryEnable bool + // This can be set to list of enrtree:// URLs which will be queried for // for nodes to connect to. EthDiscoveryURLs []string diff --git a/internal/flags/categories.go b/internal/flags/categories.go index 7a6b8d374c..1627cbf72f 100644 --- a/internal/flags/categories.go +++ b/internal/flags/categories.go @@ -40,6 +40,7 @@ const ( FastNodeCategory = "FAST NODE" FastFinalityCategory = "FAST FINALITY" HistoryCategory = "HISTORY" + StateExpiryCategory = "STATE EXPIRY" ) func init() { From e14697827e46906e4b341ee375f08c201cc85904 Mon Sep 17 00:00:00 2001 From: asyukii Date: Fri, 18 Aug 2023 13:48:12 +0800 Subject: [PATCH 02/65] feat: add meta info --- core/types/meta.go | 23 +++++++++++++++++++++++ core/types/state_account.go | 1 + 2 files changed, 24 insertions(+) create mode 100644 core/types/meta.go diff --git a/core/types/meta.go b/core/types/meta.go new file mode 100644 index 0000000000..c1c42e2774 --- /dev/null +++ b/core/types/meta.go @@ -0,0 +1,23 @@ +package types + +import ( + "github.com/ethereum/go-ethereum/common" +) + +type StateMeta interface { + GetVersionNumber() uint8 + Hash() common.Hash +} + +type MetaNoConsensus struct { + Version uint8 + Epoch uint16 +} + +func (m *MetaNoConsensus) GetVersionNumber() uint8 { + return m.Version +} + +func (m *MetaNoConsensus) Hash() common.Hash { + return rlpHash(m.Epoch) +} diff --git a/core/types/state_account.go b/core/types/state_account.go index 314f4943ec..0fdc8b8f27 100644 --- a/core/types/state_account.go +++ b/core/types/state_account.go @@ -33,6 +33,7 @@ type StateAccount struct { Balance *big.Int Root common.Hash // merkle root of the storage trie CodeHash []byte + MetaHash common.Hash `rlp:"-"` // TODO (asyukii): handle this } // NewEmptyStateAccount constructs an empty state account. From 29adf96e518a3856d157890268914f758f991280 Mon Sep 17 00:00:00 2001 From: asyukii Date: Mon, 21 Aug 2023 09:56:09 +0800 Subject: [PATCH 03/65] feat: add path proof generation --- trie/proof.go | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/trie/proof.go b/trie/proof.go index a463c80b48..2318610d7c 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -111,6 +111,109 @@ func (t *StateTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error { return t.trie.Prove(key, proofDb) } +// traverseNodes traverses the trie with the given key starting at the given node. +// If the trie contains the key, the returned node is the node that contains the +// value for the key. If nodes is specified, the traversed nodes are appended to +// it. +func (t *Trie) traverseNodes(tn node, key []byte, nodes *[]node) (node, error) { + + var prefix []byte + + for len(key) > 0 && tn != nil { + switch n := tn.(type) { + case *shortNode: + if len(key) < len(n.Key) || !bytes.Equal(n.Key, key[:len(n.Key)]) { + // The trie doesn't contain the key. + tn = nil + } else { + tn = n.Val + prefix = append(prefix, n.Key...) + key = key[len(n.Key):] + } + if nodes != nil { + *nodes = append(*nodes, n) + } + case *fullNode: + tn = n.Children[key[0]] + prefix = append(prefix, key[0]) + key = key[1:] + if nodes != nil { + *nodes = append(*nodes, n) + } + case hashNode: + // Retrieve the specified node from the underlying node reader. + // trie.resolveAndTrack is not used since in that function the + // loaded blob will be tracked, while it's not required here since + // all loaded nodes won't be linked to trie at all and track nodes + // may lead to out-of-memory issue. + blob, err := t.reader.node(prefix, common.BytesToHash(n)) + if err != nil { + log.Error("Unhandled trie error in traverseNodes", "err", err) + return nil, err + } + // The raw-blob format nodes are loaded either from the + // clean cache or the database, they are all in their own + // copy and safe to use unsafe decoder. + tn = mustDecodeNodeUnsafe(n, blob) + default: + panic(fmt.Sprintf("%T: invalid node: %v", tn, tn)) + } + } + + return tn, nil +} + +func (t *Trie) ProvePath(key []byte, prefixKeyHex []byte, proofDb ethdb.KeyValueWriter) error { + + if t.committed { + return ErrCommitted + } + + if len(key) == 0 { + return fmt.Errorf("key is empty") + } + + key = keybytesToHex(key) + + // traverse down using the prefixKeyHex + var nodes []node + tn := t.root + startNode, err := t.traverseNodes(tn, prefixKeyHex, nil) // obtain the node where the prefixKeyHex leads to + if err != nil { + return err + } + + key = key[len(prefixKeyHex):] // obtain the suffix key + + // traverse through the suffix key + _, err = t.traverseNodes(startNode, key, &nodes) + if err != nil { + return err + } + + hasher := newHasher(false) + defer returnHasherToPool(hasher) + + // construct the proof + for _, n := range nodes { + var hn node + n, hn = hasher.proofHash(n) + if hash, ok := hn.(hashNode); ok { + enc := nodeToBytes(n) + if !ok { + hash = hasher.hashData(enc) + } + proofDb.Put(hash, enc) + } + } + + return nil +} + +func (t *StateTrie) ProvePath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error { + return t.trie.ProvePath(key, path, proofDb) +} + // VerifyProof checks merkle proofs. The given proof must contain the value for // key in a trie with the given root hash. VerifyProof returns an error if the // proof contains invalid trie nodes or the wrong value. From 887dfdc98c4a2a6e9bb3b22d72c12f992ceb21ff Mon Sep 17 00:00:00 2001 From: asyukii Date: Mon, 21 Aug 2023 17:44:13 +0800 Subject: [PATCH 04/65] feat: add revive operation fix: verify proof first then only revive remove comments edit function comments verify child hash while constructing trie from proof remove unnecessary comments and parameters --- trie/proof.go | 87 +++++++++++++++++++++++++++++ trie/proof_test.go | 36 ++++++++++++ trie/trie.go | 133 +++++++++++++++++++++++++++++++++++++++++++++ trie/trie_test.go | 81 +++++++++++++++++++++++++++ 4 files changed, 337 insertions(+) diff --git a/trie/proof.go b/trie/proof.go index 2318610d7c..c7108ba138 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -210,6 +210,93 @@ func (t *Trie) ProvePath(key []byte, prefixKeyHex []byte, proofDb ethdb.KeyValue return nil } +// VerifyPathProof reconstructs the trie from the given proof and verifies the root hash. +func VerifyPathProof(keyHex []byte, prefixKeyHex []byte, proofList [][]byte) (node, hashNode, error) { + + if len(proofList) == 0 { + return nil, nil, fmt.Errorf("proof list is empty") + } + + n, err := ConstructTrieFromProof(keyHex, prefixKeyHex, proofList) + if err != nil { + return nil, nil, err + } + + // hash the root node + hasher := newHasher(false) + defer returnHasherToPool(hasher) + hn, cn := hasher.hash(n, true) + if hash, ok := hn.(hashNode); ok { + return cn, hash, nil + } + + return nil, nil, fmt.Errorf("path proof verification failed") +} + +// ConstructTrieFromProof constructs a trie from the given proof. It returns the root node of the trie. +func ConstructTrieFromProof(keyHex []byte, prefixKeyHex []byte, proofList [][]byte) (node, error) { + var parentNode node + var root node + + keyHex = keyHex[len(prefixKeyHex):] + + for i := 0; i < len(proofList); i++ { + + var n node + + node := proofList[i] + n, err := decodeNode(nil, node) + if err != nil { + return nil, fmt.Errorf("decode proof item %064x, err: %x", node, err) + } + + if parentNode == nil { + parentNode = n + root = parentNode + } + + keyrest, cld := get(n, keyHex, false) + switch cld := cld.(type) { + case nil: + return nil, fmt.Errorf("the trie doesn't contain the key") + case hashNode: + keyHex = keyrest + // Verify that the child node is a hashNode and matches the hash in the proof + switch sn := parentNode.(type) { + case *shortNode: + if hash, ok := sn.Val.(hashNode); ok && !bytes.Equal(hash, cld) { + return nil, fmt.Errorf("the child node of shortNode is not a hashNode or doesn't match the hash in the proof") + } + case *fullNode: + if hash, ok := sn.Children[keyHex[0]].(hashNode); ok && !bytes.Equal(hash, cld) { + return nil, fmt.Errorf("the child node of fullNode is not a hashNode or doesn't match the hash in the proof") + } + } + case valueNode: + switch sn := parentNode.(type) { + case *shortNode: + sn.Val = cld + return root, nil + case *fullNode: + sn.Children[keyHex[0]] = cld + return root, nil + } + } + + // Link the parent and child. + switch sn := parentNode.(type) { + case *shortNode: + sn.Val = n + parentNode = n + case *fullNode: + sn.Children[keyHex[0]] = n + parentNode = n + } + } + + return root, nil +} + func (t *StateTrie) ProvePath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error { return t.trie.ProvePath(key, path, proofDb) } diff --git a/trie/proof_test.go b/trie/proof_test.go index fc2de62649..3f07ae7c51 100644 --- a/trie/proof_test.go +++ b/trie/proof_test.go @@ -73,6 +73,31 @@ func makeProvers(trie *Trie) []func(key []byte) *memorydb.Database { return provers } +func TestOneElementPathProof(t *testing.T) { + trie := NewEmpty(NewDatabase(rawdb.NewMemoryDatabase())) + updateString(trie, "k", "v") + + var proofList proofList + + trie.Prove([]byte("k"), &proofList) + if proofList == nil { + t.Fatalf("nil proof") + } + + if len(proofList) != 1 { + t.Errorf("proof should have one element") + } + + _, hn, err := VerifyPathProof(keybytesToHex([]byte("k")), nil, proofList) + if err != nil { + t.Fatalf("failed to verify proof: %v\nraw proof: %x", err, proofList) + } + + if common.BytesToHash(hn) != trie.Hash() { + t.Fatalf("verified root mismatch: have %x, want %x", hn, trie.Hash()) + } +} + func TestProof(t *testing.T) { trie, vals := randomTrie(500) root := trie.Hash() @@ -901,6 +926,17 @@ func TestAllElementsEmptyValueRangeProof(t *testing.T) { } } +type proofList [][]byte + +func (n *proofList) Put(key []byte, value []byte) error { + *n = append(*n, value) + return nil +} + +func (n *proofList) Delete(key []byte) error { + panic("not supported") +} + // mutateByte changes one byte in b. func mutateByte(b []byte) { for r := mrand.Intn(len(b)); ; { diff --git a/trie/trie.go b/trie/trie.go index d19cb31063..1c8cbaeb99 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -682,3 +682,136 @@ func (t *Trie) Size() int { func (t *Trie) Owner() common.Hash { return t.owner } + +// ReviveTrie revives a trie by prefix key with the given proof list. +func (t *Trie) ReviveTrie(key []byte, prefixKeyHex []byte, proofList [][]byte) { + key = keybytesToHex(key) + + // Verify the proof first + revivedNode, revivedHash, err := VerifyPathProof(key, prefixKeyHex, proofList) + if err != nil { + log.Error("Failed to verify proof", "err", err) + } + + newRoot, _, err := t.revive(t.root, key, prefixKeyHex, 0, revivedNode, common.BytesToHash(revivedHash)) + if err != nil { + log.Error("Failed to revive trie", "err", err) + } + t.root = newRoot +} + +func (t *Trie) revive(n node, key []byte, prefixKeyHex []byte, pos int, revivedNode node, revivedHash common.Hash) (node, bool, error) { + + if pos > len(prefixKeyHex) { + return nil, false, fmt.Errorf("target revive node not found") + } + + if pos == len(prefixKeyHex) { + hn, ok := n.(hashNode) + if !ok { + return nil, false, fmt.Errorf("prefix key path does not lead to a hash node") + } + + // Compare the hash of the revived node with the hash of the hash node + if revivedHash != common.BytesToHash(hn) { + return nil, false, fmt.Errorf("revived node hash does not match the hash node hash") + } + + return revivedNode, true, nil + } + + switch n := n.(type) { + case *shortNode: + if len(key)-pos < len(n.Key) || !bytes.Equal(n.Key, key[pos:pos+len(n.Key)]) { + // key not found in trie + return n, false, nil + } + newNode, didRevived, err := t.revive(n.Val, key, prefixKeyHex, pos+len(n.Key), revivedNode, revivedHash) + if err == nil && didRevived { + n = n.copy() + n.Val = newNode + } + return n, didRevived, err + case *fullNode: + childIndex := int(key[pos]) + newNode, didRevived, err := t.revive(n.Children[childIndex], key, prefixKeyHex, pos+1, revivedNode, revivedHash) + if err == nil && didRevived { + n = n.copy() + n.Children[key[pos]] = newNode + } + return n, didRevived, err + case hashNode: + child, err := t.resolveAndTrack(n, key[:pos]) + if err != nil { + return nil, false, err + } + newNode, _, err := t.revive(child, key, prefixKeyHex, pos, revivedNode, revivedHash) + return newNode, true, err + case nil: + return nil, false, nil + default: + panic(fmt.Sprintf("invalid node: %T", n)) + } +} + +// ExpireByPrefix is used to simulate the expiration of a trie by prefix key. +// It is not used in the actual trie implementation. ExpireByPrefix makes sure +// only a child node of a full node is expired, if not an error is returned. +func (t *Trie) ExpireByPrefix(prefixKeyHex []byte) error { + hn, err := t.expireByPrefix(t.root, prefixKeyHex) + if prefixKeyHex == nil && hn != nil { + t.root = hn + } + if err != nil { + return err + } + return nil +} + +func (t *Trie) expireByPrefix(n node, prefixKeyHex []byte) (node, error) { + // Loop through prefix key + // When prefix key is empty, generate the hash node of the current node + // Replace current node with the hash node + + if len(prefixKeyHex) == 0 { + hasher := newHasher(false) + defer returnHasherToPool(hasher) + var hn node + _, hn = hasher.proofHash(n) + if _, ok := hn.(hashNode); ok { + return hn, nil + } + + return nil, nil + } + + switch n := n.(type) { + case *shortNode: + matchLen := prefixLen(prefixKeyHex, n.Key) + hn, err := t.expireByPrefix(n.Val, prefixKeyHex[matchLen:]) + if err != nil { + return nil, err + } + + if hn != nil { + return nil, fmt.Errorf("can only expire child short node") + } + + return nil, err + case *fullNode: + childIndex := int(prefixKeyHex[0]) + hn, err := t.expireByPrefix(n.Children[childIndex], prefixKeyHex[1:]) + if err != nil { + return nil, err + } + + // Replace child node with hash node + if hn != nil { + n.Children[prefixKeyHex[0]] = hn + } + + return nil, err + default: + return nil, fmt.Errorf("invalid node type") + } +} diff --git a/trie/trie_test.go b/trie/trie_test.go index 35ccc77201..60daed97de 100644 --- a/trie/trie_test.go +++ b/trie/trie_test.go @@ -36,6 +36,7 @@ import ( "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie/trienode" + "github.com/stretchr/testify/assert" "golang.org/x/crypto/sha3" ) @@ -995,6 +996,86 @@ func TestCommitSequenceSmallRoot(t *testing.T) { } } +func TestReviveCustom(t *testing.T) { + + data := map[string]string{ + "abcd": "A", "abce": "B", "abde": "C", "abdf": "D", + "defg": "E", "defh": "F", "degh": "G", "degi": "H", + } + + trie := createCustomTrie(data) + + oriRootHash := trie.Hash() + + for k, v := range data { + key := []byte(k) + val := []byte(v) + prefixKeys := getFullNodePrefixKeys(trie, key) + for _, prefixKey := range prefixKeys { + var proofList proofList + err := trie.ProvePath(key, prefixKey, &proofList) + assert.NoError(t, err) + + trie.ExpireByPrefix(prefixKey) + + trie.ReviveTrie(key, prefixKey, proofList) + + v := trie.MustGet(key) + assert.Equal(t, val, v) + + // Verify root hash + currRootHash := trie.Hash() + assert.Equal(t, oriRootHash, currRootHash, "root hash mismatch, got %x, exp %x, key %x, prefixKey %x", currRootHash, oriRootHash, key, prefixKey) + + // Reset trie + trie = createCustomTrie(data) + } + } +} + +func createCustomTrie(data map[string]string) *Trie { + trie := NewEmpty(NewDatabase(rawdb.NewMemoryDatabase())) + for k, v := range data { + trie.MustUpdate([]byte(k), []byte(v)) + } + + return trie +} + +func getFullNodePrefixKeys(t *Trie, key []byte) [][]byte { + var prefixKeys [][]byte + key = keybytesToHex(key) + tn := t.root + currPath := []byte{} + for len(key) > 0 && tn != nil { + switch n := tn.(type) { + case *shortNode: + if len(key) < len(n.Key) || !bytes.Equal(n.Key, key[:len(n.Key)]) { + // The trie doesn't contain the key. + tn = nil + } else { + tn = n.Val + prefixKeys = append(prefixKeys, currPath) + currPath = append(currPath, n.Key...) + key = key[len(n.Key):] + } + case *fullNode: + tn = n.Children[key[0]] + currPath = append(currPath, key[0]) + key = key[1:] + default: + return nil + } + } + + // Remove the first item in prefixKeys, which is the empty key + if len(prefixKeys) > 0 { + prefixKeys = prefixKeys[1:] + } + + return prefixKeys +} + // BenchmarkCommitAfterHashFixedSize benchmarks the Commit (after Hash) of a fixed number of updates to a trie. // This benchmark is meant to capture the difference on efficiency of small versus large changes. Typically, // storage tries are small (a couple of entries), whereas the full post-block account trie update is large (a couple From 3a98bb658aa0bc63cf2409405e60df8221ac5770 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Thu, 24 Aug 2023 22:29:27 +0800 Subject: [PATCH 05/65] state/snapshot: support snapshot prune, epoch meta; --- core/state/snapshot/snapshot_expire_prune.go | 19 ++ .../snapshot/snapshot_expire_prune_test.go | 33 ++++ core/state/snapshot/snapshot_value.go | 162 ++++++++++++++++++ core/state/snapshot/snapshot_value_test.go | 47 +++++ 4 files changed, 261 insertions(+) create mode 100644 core/state/snapshot/snapshot_expire_prune.go create mode 100644 core/state/snapshot/snapshot_expire_prune_test.go create mode 100644 core/state/snapshot/snapshot_value.go create mode 100644 core/state/snapshot/snapshot_value_test.go diff --git a/core/state/snapshot/snapshot_expire_prune.go b/core/state/snapshot/snapshot_expire_prune.go new file mode 100644 index 0000000000..84f2e987c8 --- /dev/null +++ b/core/state/snapshot/snapshot_expire_prune.go @@ -0,0 +1,19 @@ +package snapshot + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb" +) + +// ShrinkExpiredLeaf tool function for snapshot kv prune +func ShrinkExpiredLeaf(db ethdb.KeyValueStore, accountHash common.Hash, storageHash common.Hash, epoch types.StateEpoch) error { + valWithEpoch := NewValueWithEpoch(epoch, common.Hash{}) + enc, err := EncodeValueToRLPBytes(valWithEpoch) + if err != nil { + return err + } + rawdb.WriteStorageSnapshot(db, accountHash, storageHash, enc) + return nil +} diff --git a/core/state/snapshot/snapshot_expire_prune_test.go b/core/state/snapshot/snapshot_expire_prune_test.go new file mode 100644 index 0000000000..9fa4982736 --- /dev/null +++ b/core/state/snapshot/snapshot_expire_prune_test.go @@ -0,0 +1,33 @@ +package snapshot + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb/memorydb" + "github.com/stretchr/testify/assert" + "testing" +) + +var ( + accountHash = common.HexToHash("0x31b67165f56d0ac50814cafa06748fb3b8fccd3c611a8117350e7a49b44ce130") + storageHash1 = common.HexToHash("0x0bb2f3e66816c6fd12513f053d5ee034b1fa2d448a1dc8ee7f56e4c87d6c53fe") + storageHash2 = common.HexToHash("0x0bb2f3e66816c93e142cd336c411ebd5576a90739bad7ec1ec0d4a63ea0ec1dc") + storageShrink1 = common.FromHex("0x0bb2f3e66816c") + storageNodeHash1 = common.HexToHash("0xcf0e24cb9417a38ff61d11def23fb0ec041257c81c04dec6156d5e6b30f4ec28") +) + +func TestShrinkExpiredLeaf(t *testing.T) { + db := memorydb.New() + rawdb.WriteStorageSnapshot(db, accountHash, storageHash1, encodeSnapVal(NewRawValue([]byte("val1")))) + + err := ShrinkExpiredLeaf(db, accountHash, storageHash1, types.StateEpoch0) + assert.NoError(t, err) + + assert.Equal(t, encodeSnapVal(NewValueWithEpoch(types.StateEpoch0, common.Hash{})), rawdb.ReadStorageSnapshot(db, accountHash, storageHash1)) +} + +func encodeSnapVal(val SnapValue) []byte { + enc, _ := EncodeValueToRLPBytes(val) + return enc +} diff --git a/core/state/snapshot/snapshot_value.go b/core/state/snapshot/snapshot_value.go new file mode 100644 index 0000000000..ddaf446641 --- /dev/null +++ b/core/state/snapshot/snapshot_value.go @@ -0,0 +1,162 @@ +package snapshot + +import ( + "bytes" + "errors" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" +) + +const ( + RawValueType = iota // simple value, cannot exceed 32 bytes + ValueWithEpochType // value add epoch meta +) + +var ( + ErrSnapValueNotSupport = errors.New("the snapshot type not support now") +) + +type SnapValue interface { + GetType() byte + GetEpoch() types.StateEpoch + GetVal() common.Hash // may cannot provide val in some value types + EncodeToRLPBytes(buf *rlp.EncoderBuffer) +} + +type RawValue []byte + +func NewRawValue(val []byte) SnapValue { + value := RawValue(val) + return &value +} + +func (v *RawValue) GetType() byte { + return RawValueType +} + +func (v *RawValue) GetEpoch() types.StateEpoch { + return types.StateEpoch0 +} + +func (v *RawValue) GetVal() common.Hash { + return common.BytesToHash(*v) +} + +func (v *RawValue) EncodeToRLPBytes(buf *rlp.EncoderBuffer) { + buf.WriteBytes(*v) +} + +type ValueWithEpoch struct { + Epoch types.StateEpoch // kv's epoch meta + Val common.Hash // if val is empty hash, just encode as empty string in RLP +} + +func NewValueWithEpoch(epoch types.StateEpoch, val common.Hash) SnapValue { + return &ValueWithEpoch{ + Epoch: epoch, + Val: val, + } +} + +func (v *ValueWithEpoch) GetType() byte { + return ValueWithEpochType +} + +func (v *ValueWithEpoch) GetEpoch() types.StateEpoch { + return v.Epoch +} + +func (v *ValueWithEpoch) GetVal() common.Hash { + return v.Val +} + +func (v *ValueWithEpoch) EncodeToRLPBytes(buf *rlp.EncoderBuffer) { + offset := buf.List() + buf.WriteUint64(uint64(v.Epoch)) + if v.Val == (common.Hash{}) { + buf.Write(rlp.EmptyString) + } else { + buf.WriteBytes(v.Val[:]) + } + buf.ListEnd(offset) +} + +func EncodeValueToRLPBytes(val SnapValue) ([]byte, error) { + switch raw := val.(type) { + case *RawValue: + return rlp.EncodeToBytes(raw) + default: + return encodeTypedVal(val) + } +} + +func DecodeValueFromRLPBytes(b []byte) (SnapValue, error) { + if len(b) > 0 && b[0] > 0x7f { + var data RawValue + _, data, _, err := rlp.Split(b) + if err != nil { + return nil, err + } + return &data, nil + } + return decodeTypedVal(b) +} + +func GetValueTypeFromRLPBytes(b []byte) byte { + if len(b) == 0 { + return RawValueType + } + if b[0] > 0x7f { + return RawValueType + } + return b[0] +} + +func decodeTypedVal(b []byte) (SnapValue, error) { + switch b[0] { + case ValueWithEpochType: + var data ValueWithEpoch + if err := decodeValueWithEpoch(b[1:], &data); err != nil { + return nil, err + } + return &data, nil + default: + return nil, ErrSnapValueNotSupport + } +} + +func decodeValueWithEpoch(data []byte, v *ValueWithEpoch) error { + elems, _, err := rlp.SplitList(data) + if err != nil { + return err + } + + epoch, left, err := rlp.SplitUint64(elems) + if err != nil { + return err + } + v.Epoch = types.StateEpoch(epoch) + + val, _, err := rlp.SplitString(left) + if err != nil { + return err + } + if len(val) == 0 { + v.Val = common.Hash{} + } else { + v.Val = common.BytesToHash(val) + } + return nil +} + +func encodeTypedVal(val SnapValue) ([]byte, error) { + buf := bytes.NewBuffer(make([]byte, 0, 40)) + buf.WriteByte(val.GetType()) + encoder := rlp.NewEncoderBuffer(buf) + val.EncodeToRLPBytes(&encoder) + if err := encoder.Flush(); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/core/state/snapshot/snapshot_value_test.go b/core/state/snapshot/snapshot_value_test.go new file mode 100644 index 0000000000..b81b06d8e8 --- /dev/null +++ b/core/state/snapshot/snapshot_value_test.go @@ -0,0 +1,47 @@ +package snapshot + +import ( + "encoding/hex" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/assert" + "testing" +) + +var ( + val, _ = hex.DecodeString("0000f9eef0150e074b32e3b3b6d34d2534222292e3953019a41d714d135763a6") + hash, _ = hex.DecodeString("2b6fad2e1335b0b4debd3de01c91f3f45d2b88465ff42ae2c53900f2f702101d") +) + +func TestRawValueEncode(t *testing.T) { + value := NewRawValue(val) + enc1, _ := rlp.EncodeToBytes(value) + buf := rlp.NewEncoderBuffer(nil) + value.EncodeToRLPBytes(&buf) + assert.Equal(t, enc1, buf.ToBytes()) +} + +func TestSnapValEncodeDecode(t *testing.T) { + tests := []struct { + raw SnapValue + }{ + { + raw: NewRawValue(val), + }, + { + raw: NewValueWithEpoch(types.StateEpoch(1000), common.BytesToHash(val)), + }, + { + raw: NewValueWithEpoch(types.StateEpoch(1000), common.Hash{}), + }, + } + for _, item := range tests { + enc, err := EncodeValueToRLPBytes(item.raw) + assert.NoError(t, err) + t.Log(hex.EncodeToString(enc)) + tmp, err := DecodeValueFromRLPBytes(enc) + assert.NoError(t, err) + assert.Equal(t, item.raw, tmp) + } +} From 48859f74e4326116a834ebf3fb2f987a67fbeb08 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Thu, 24 Aug 2023 18:04:40 +0800 Subject: [PATCH 06/65] state/snapshot: support get/put in statedb; state/statedb: support state expiry; state/state_object: add revive trie, access state, etc.; --- ...hot_expire_prune.go => snapshot_expire.go} | 7 +- ..._prune_test.go => snapshot_expire_test.go} | 2 +- core/state/snapshot/snapshot_value.go | 24 +- core/state/snapshot/snapshot_value_test.go | 5 +- core/state/state_object.go | 266 ++++++++++++++++-- core/state/statedb.go | 25 ++ core/types/state_epoch.go | 62 ++++ 7 files changed, 352 insertions(+), 39 deletions(-) rename core/state/snapshot/{snapshot_expire_prune.go => snapshot_expire.go} (80%) rename core/state/snapshot/{snapshot_expire_prune_test.go => snapshot_expire_test.go} (93%) create mode 100644 core/types/state_epoch.go diff --git a/core/state/snapshot/snapshot_expire_prune.go b/core/state/snapshot/snapshot_expire.go similarity index 80% rename from core/state/snapshot/snapshot_expire_prune.go rename to core/state/snapshot/snapshot_expire.go index 84f2e987c8..17584e3f56 100644 --- a/core/state/snapshot/snapshot_expire_prune.go +++ b/core/state/snapshot/snapshot_expire.go @@ -1,15 +1,20 @@ package snapshot import ( + "errors" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" ) +var ( + ErrStorageExpired = errors.New("access expired account storage kv") +) + // ShrinkExpiredLeaf tool function for snapshot kv prune func ShrinkExpiredLeaf(db ethdb.KeyValueStore, accountHash common.Hash, storageHash common.Hash, epoch types.StateEpoch) error { - valWithEpoch := NewValueWithEpoch(epoch, common.Hash{}) + valWithEpoch := NewValueWithEpoch(epoch, nil) enc, err := EncodeValueToRLPBytes(valWithEpoch) if err != nil { return err diff --git a/core/state/snapshot/snapshot_expire_prune_test.go b/core/state/snapshot/snapshot_expire_test.go similarity index 93% rename from core/state/snapshot/snapshot_expire_prune_test.go rename to core/state/snapshot/snapshot_expire_test.go index 9fa4982736..6aff97a6f2 100644 --- a/core/state/snapshot/snapshot_expire_prune_test.go +++ b/core/state/snapshot/snapshot_expire_test.go @@ -24,7 +24,7 @@ func TestShrinkExpiredLeaf(t *testing.T) { err := ShrinkExpiredLeaf(db, accountHash, storageHash1, types.StateEpoch0) assert.NoError(t, err) - assert.Equal(t, encodeSnapVal(NewValueWithEpoch(types.StateEpoch0, common.Hash{})), rawdb.ReadStorageSnapshot(db, accountHash, storageHash1)) + assert.Equal(t, encodeSnapVal(NewValueWithEpoch(types.StateEpoch0, nil)), rawdb.ReadStorageSnapshot(db, accountHash, storageHash1)) } func encodeSnapVal(val SnapValue) []byte { diff --git a/core/state/snapshot/snapshot_value.go b/core/state/snapshot/snapshot_value.go index ddaf446641..b6c479be84 100644 --- a/core/state/snapshot/snapshot_value.go +++ b/core/state/snapshot/snapshot_value.go @@ -3,7 +3,6 @@ package snapshot import ( "bytes" "errors" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" ) @@ -20,7 +19,7 @@ var ( type SnapValue interface { GetType() byte GetEpoch() types.StateEpoch - GetVal() common.Hash // may cannot provide val in some value types + GetVal() []byte // may cannot provide val in some value types EncodeToRLPBytes(buf *rlp.EncoderBuffer) } @@ -39,8 +38,8 @@ func (v *RawValue) GetEpoch() types.StateEpoch { return types.StateEpoch0 } -func (v *RawValue) GetVal() common.Hash { - return common.BytesToHash(*v) +func (v *RawValue) GetVal() []byte { + return *v } func (v *RawValue) EncodeToRLPBytes(buf *rlp.EncoderBuffer) { @@ -49,10 +48,13 @@ func (v *RawValue) EncodeToRLPBytes(buf *rlp.EncoderBuffer) { type ValueWithEpoch struct { Epoch types.StateEpoch // kv's epoch meta - Val common.Hash // if val is empty hash, just encode as empty string in RLP + Val []byte // if val is empty hash, just encode as empty string in RLP } -func NewValueWithEpoch(epoch types.StateEpoch, val common.Hash) SnapValue { +func NewValueWithEpoch(epoch types.StateEpoch, val []byte) SnapValue { + if epoch == types.StateEpoch0 { + return NewRawValue(val) + } return &ValueWithEpoch{ Epoch: epoch, Val: val, @@ -67,17 +69,17 @@ func (v *ValueWithEpoch) GetEpoch() types.StateEpoch { return v.Epoch } -func (v *ValueWithEpoch) GetVal() common.Hash { +func (v *ValueWithEpoch) GetVal() []byte { return v.Val } func (v *ValueWithEpoch) EncodeToRLPBytes(buf *rlp.EncoderBuffer) { offset := buf.List() buf.WriteUint64(uint64(v.Epoch)) - if v.Val == (common.Hash{}) { + if len(v.Val) == 0 { buf.Write(rlp.EmptyString) } else { - buf.WriteBytes(v.Val[:]) + buf.WriteBytes(v.Val) } buf.ListEnd(offset) } @@ -143,9 +145,9 @@ func decodeValueWithEpoch(data []byte, v *ValueWithEpoch) error { return err } if len(val) == 0 { - v.Val = common.Hash{} + v.Val = []byte{} } else { - v.Val = common.BytesToHash(val) + v.Val = val } return nil } diff --git a/core/state/snapshot/snapshot_value_test.go b/core/state/snapshot/snapshot_value_test.go index b81b06d8e8..7af79a6772 100644 --- a/core/state/snapshot/snapshot_value_test.go +++ b/core/state/snapshot/snapshot_value_test.go @@ -2,7 +2,6 @@ package snapshot import ( "encoding/hex" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" "github.com/stretchr/testify/assert" @@ -30,10 +29,10 @@ func TestSnapValEncodeDecode(t *testing.T) { raw: NewRawValue(val), }, { - raw: NewValueWithEpoch(types.StateEpoch(1000), common.BytesToHash(val)), + raw: NewValueWithEpoch(types.StateEpoch(1000), val), }, { - raw: NewValueWithEpoch(types.StateEpoch(1000), common.Hash{}), + raw: NewValueWithEpoch(types.StateEpoch(1000), []byte{}), }, } for _, item := range tests { diff --git a/core/state/state_object.go b/core/state/state_object.go index ed67fceefb..d21007e3d4 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -19,6 +19,7 @@ package state import ( "bytes" "fmt" + "github.com/ethereum/go-ethereum/core/state/snapshot" "io" "math/big" "sync" @@ -69,7 +70,7 @@ type stateObject struct { data types.StateAccount // Account data with all mutations applied in the scope of block // Write caches. - trie Trie // storage trie, which becomes non-nil on first access + trie Trie // storage trie, which becomes non-nil on first access, it's committed trie code Code // contract bytecode, which gets set when code is loaded sharedOriginStorage *sync.Map // Point to the entry of the stateObject in sharedPool @@ -77,6 +78,12 @@ type stateObject struct { pendingStorage Storage // Storage entries that need to be flushed to disk, at the end of an entire block dirtyStorage Storage // Storage entries that have been modified in the current transaction execution, reset for every transaction + // for state expiry feature + pendingReviveTrie Trie // pendingReviveTrie it contains pending revive trie nodes, could update & commit later + pendingReviveState map[string]common.Hash // pendingReviveState for block, when R&W, access revive state first + pendingAccessedState map[common.Hash]int // pendingAccessedState record which state is accessed(only read now, update/delete/insert will auto update epoch), it will update epoch index late + originStorageEpoch map[common.Hash]types.StateEpoch // originStorageEpoch record origin state epoch, prevent frequency epoch update + // Cache flags. dirtyCode bool // true if the code was updated @@ -111,15 +118,18 @@ func newObject(db *StateDB, address common.Address, acct *types.StateAccount) *s } return &stateObject{ - db: db, - address: address, - addrHash: crypto.Keccak256Hash(address[:]), - origin: origin, - data: *acct, - sharedOriginStorage: storageMap, - originStorage: make(Storage), - pendingStorage: make(Storage), - dirtyStorage: make(Storage), + db: db, + address: address, + addrHash: crypto.Keccak256Hash(address[:]), + origin: origin, + data: *acct, + sharedOriginStorage: storageMap, + originStorage: make(Storage), + pendingStorage: make(Storage), + dirtyStorage: make(Storage), + pendingReviveState: make(map[string]common.Hash), + pendingAccessedState: make(map[common.Hash]int), + originStorageEpoch: make(map[common.Hash]types.StateEpoch), } } @@ -154,6 +164,7 @@ func (s *stateObject) getTrie() (Trie, error) { s.trie = s.db.prefetcher.trie(s.addrHash, s.data.Root) } if s.trie == nil { + // TODO(0xbundler): if any change to open a storage trie in state expiry feature? tr, err := s.db.db.OpenStorageTrie(s.db.originalRoot, s.address, s.data.Root) if err != nil { return nil, err @@ -164,6 +175,17 @@ func (s *stateObject) getTrie() (Trie, error) { return s.trie, nil } +func (s *stateObject) getPendingReviveTrie() (Trie, error) { + if s.pendingReviveTrie == nil { + src, err := s.getTrie() + if err != nil { + return nil, err + } + s.pendingReviveTrie = s.db.db.CopyTrie(src) + } + return s.pendingReviveTrie, nil +} + // GetState retrieves a value from the account storage trie. func (s *stateObject) GetState(key common.Hash) common.Hash { // If we have a dirty value for this state entry, return it @@ -172,7 +194,11 @@ func (s *stateObject) GetState(key common.Hash) common.Hash { return value } // Otherwise return the entry's original value - return s.GetCommittedState(key) + value = s.GetCommittedState(key) + if value != (common.Hash{}) { + s.accessState(key) + } + return value } func (s *stateObject) getOriginStorage(key common.Hash) (common.Hash, bool) { @@ -206,9 +232,20 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { return value } + if s.db.EnableExpire() { + if revived, revive := s.queryFromReviveState(s.pendingReviveState, key); revive { + return revived + } + } + if value, cached := s.getOriginStorage(key); cached { return value } + + if value, cached := s.originStorage[key]; cached { + return value + } + // If the object was destructed in *this* block (and potentially resurrected), // the storage has been cleared out, and we should *not* consult the previous // database about any storage values. The only possible alternatives are: @@ -226,20 +263,52 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { ) if s.db.snap != nil { start := time.Now() - enc, err = s.db.snap.Storage(s.addrHash, crypto.Keccak256Hash(key.Bytes())) + // handle state expiry situation + if s.db.EnableExpire() { + enc, err = s.db.snap.Storage(s.addrHash, crypto.Keccak256Hash(key.Bytes())) + if err == snapshot.ErrStorageExpired { + // TODO(0xbundler): request from remote node; + // TODO: query from dirty revive trie, got the newest expired info + // TODO: handle from remoteDB, if got err just setError, just return to revert in consensus version . + //_, err = s.getDirtyReviveTrie(db).TryGet(key.Bytes()) + //if enErr, ok := err.(*trie.ExpiredNodeError); ok { + // return common.Hash{}, NewExpiredStateError(s.address, key, enErr).Reason("snap query") + //} + // proof, remoteErr := request() + // if remoteErr != nil { + // s.db.setError(remoteErr) + // return common.hash{} + // } + // TODO: add to revive trie with new epoch, add pending state for snapshot + err = nil + } + if len(enc) > 0 { + var sv snapshot.SnapValue + sv, err = snapshot.DecodeValueFromRLPBytes(enc) + if err == nil { + value.SetBytes(sv.GetVal()) + s.originStorageEpoch[key] = sv.GetEpoch() + } else { + enc = []byte{} + } + } + } else { + enc, err = s.db.snap.Storage(s.addrHash, crypto.Keccak256Hash(key.Bytes())) + if len(enc) > 0 { + _, content, _, err := rlp.Split(enc) + if err != nil { + s.db.setError(err) + } + value.SetBytes(content) + } + } if metrics.EnabledExpensive { s.db.SnapshotStorageReads += time.Since(start) } - if len(enc) > 0 { - _, content, _, err := rlp.Split(enc) - if err != nil { - s.db.setError(err) - } - value.SetBytes(content) - } } + // If the snapshot is unavailable or reading from it fails, load from the database. - if s.db.snap == nil || err != nil { + if s.needLoadFromTrie(enc, err) { start := time.Now() tr, err := s.getTrie() if err != nil { @@ -250,6 +319,28 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { if metrics.EnabledExpensive { s.db.StorageReads += time.Since(start) } + // handle state expiry situation + if s.db.EnableExpire() { + // TODO(0xbundler): using trie expired error + if err == snapshot.ErrStorageExpired { + // TODO(0xbundler): request from remote node; + // TODO: query from dirty revive trie, got the newest expired info + // TODO: handle from remoteDB, if got err just setError, just return to revert in consensus version . + //_, err = s.getDirtyReviveTrie(db).TryGet(key.Bytes()) + //if enErr, ok := err.(*trie.ExpiredNodeError); ok { + // return common.Hash{}, NewExpiredStateError(s.address, key, enErr).Reason("snap query") + //} + // proof, remoteErr := request() + // if remoteErr != nil { + // s.db.setError(remoteErr) + // } + // TODO: add to revive trie with new epoch + err = nil + } + //val = queried + // TODO(0xbundler): add epoch record cache for prevent frequency access epoch update, may implement later + //s.originStorageEpoch[key] = epoch + } if err != nil { s.db.setError(err) return common.Hash{} @@ -260,6 +351,22 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { return value } +// needLoadFromTrie If not found in snap when EnableExpire(), need check insert duplication from trie. +func (s *stateObject) needLoadFromTrie(enc []byte, err error) bool { + if s.db.snap == nil { + return true + } + if !s.db.EnableExpire() { + return err != nil + } + + if err != nil || len(enc) == 0 { + return true + } + + return false +} + // SetState updates a value in account storage. func (s *stateObject) SetState(key, value common.Hash) { // If the new value is the same as old, don't set @@ -304,7 +411,7 @@ func (s *stateObject) finalise(prefetch bool) { func (s *stateObject) updateTrie() (Trie, error) { // Make sure all dirty slots are finalized into the pending storage area s.finalise(false) // Don't prefetch anymore, pull directly if need be - if len(s.pendingStorage) == 0 { + if !s.needUpdateTrie() { return s.trie, nil } // Track the amount of time wasted on updating the storage trie @@ -320,8 +427,14 @@ func (s *stateObject) updateTrie() (Trie, error) { storage map[common.Hash][]byte origin map[common.Hash][]byte hasher = crypto.NewKeccakState() + tr Trie + err error ) - tr, err := s.getTrie() + if s.db.EnableExpire() { + tr, err = s.getPendingReviveTrie() + } else { + tr, err = s.getTrie() + } if err != nil { s.db.setError(err) return nil, err @@ -341,6 +454,19 @@ func (s *stateObject) updateTrie() (Trie, error) { } dirtyStorage[key] = v } + + if s.db.EnableExpire() { + // append more access slots to update in db + for key := range s.pendingAccessedState { + if _, ok := dirtyStorage[key]; ok { + continue + } + // it must hit in cache + value := s.GetState(key) + dirtyStorage[key] = common.TrimLeftZeroes(value[:]) + } + } + var wg sync.WaitGroup wg.Add(1) go func() { @@ -384,7 +510,10 @@ func (s *stateObject) updateTrie() (Trie, error) { // rlp-encoded value to be used by the snapshot var snapshotVal []byte - if len(value) != 0 { + // Encoding []byte cannot fail, ok to ignore the error. + if s.db.EnableExpire() { + snapshotVal, _ = snapshot.EncodeValueToRLPBytes(snapshot.NewValueWithEpoch(s.db.epoch, value)) + } else { snapshotVal, _ = rlp.EncodeToBytes(value) } storage[khash] = snapshotVal // snapshotVal will be nil if it's deleted @@ -412,9 +541,36 @@ func (s *stateObject) updateTrie() (Trie, error) { if len(s.pendingStorage) > 0 { s.pendingStorage = make(Storage) } + if s.db.EnableExpire() { + if len(s.pendingReviveState) > 0 { + s.pendingReviveState = make(map[string]common.Hash) + } + if len(s.pendingAccessedState) > 0 { + s.pendingAccessedState = make(map[common.Hash]int) + } + if len(s.originStorageEpoch) > 0 { + s.originStorageEpoch = make(map[common.Hash]types.StateEpoch) + } + if s.pendingReviveTrie != nil { + s.pendingReviveTrie = nil + } + // reset trie as pending trie, will commit later + if tr != nil { + s.trie = s.db.db.CopyTrie(tr) + } + } return tr, nil } +func (s *stateObject) needUpdateTrie() bool { + if !s.db.EnableExpire() { + return len(s.pendingStorage) > 0 + } + + return len(s.pendingStorage) > 0 || len(s.pendingReviveState) > 0 || + len(s.pendingAccessedState) > 0 +} + // UpdateRoot sets the trie root to the current root hash of. An error // will be returned if trie root hash is not computed correctly. func (s *stateObject) updateRoot() { @@ -526,6 +682,24 @@ func (s *stateObject) deepCopy(db *StateDB) *stateObject { obj.selfDestructed = s.selfDestructed obj.dirtyCode = s.dirtyCode obj.deleted = s.deleted + + if s.db.EnableExpire() { + if s.pendingReviveTrie != nil { + obj.pendingReviveTrie = db.db.CopyTrie(s.pendingReviveTrie) + } + obj.pendingReviveState = make(map[string]common.Hash, len(s.pendingReviveState)) + for k, v := range s.pendingReviveState { + obj.pendingReviveState[k] = v + } + obj.pendingAccessedState = make(map[common.Hash]int, len(s.pendingAccessedState)) + for k, v := range s.pendingAccessedState { + obj.pendingAccessedState[k] = v + } + obj.originStorageEpoch = make(map[common.Hash]types.StateEpoch, len(s.originStorageEpoch)) + for k, v := range s.originStorageEpoch { + obj.originStorageEpoch[k] = v + } + } return obj } @@ -610,3 +784,49 @@ func (s *stateObject) Balance() *big.Int { func (s *stateObject) Nonce() uint64 { return s.data.Nonce } + +// ReviveStorageTrie TODO(0xbundler): combine with trie +//func (s *stateObject) ReviveStorageTrie(proofCache trie.MPTProofCache) error { +// dr := s.getDirtyReviveTrie(s.db.db) +// s.db.journal.append(reviveStorageTrieNodeChange{ +// address: &s.address, +// }) +// // revive nub and cache revive state TODO(0xbundler): support proofs merge, revive in nubs +// for _, nub := range dr.ReviveTrie(proofCache.CacheNubs()) { +// kv, err := nub.ResolveKV() +// if err != nil { +// return err +// } +// for k, enc := range kv { +// var value common.Hash +// if len(enc) > 0 { +// _, content, _, err := rlp.Split(enc) +// if err != nil { +// return err +// } +// value.SetBytes(content) +// } +// s.dirtyReviveState[k] = value +// } +// } +// return nil +//} + +// accessState record all access states, now in pendingAccessedStateEpoch without consensus +func (s *stateObject) accessState(key common.Hash) { + if !s.db.EnableExpire() { + return + } + + if s.db.epoch > s.originStorageEpoch[key] { + count := s.pendingAccessedState[key] + s.pendingAccessedState[key] = count + 1 + } +} + +// TODO(0xbundler): add hash key cache later +func (s *stateObject) queryFromReviveState(reviveState map[string]common.Hash, key common.Hash) (common.Hash, bool) { + khash := crypto.HashData(s.db.hasher, key[:]) + val, ok := reviveState[string(khash[:])] + return val, ok +} diff --git a/core/state/statedb.go b/core/state/statedb.go index 62397b083e..96b4999bb4 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -141,6 +141,11 @@ type StateDB struct { validRevisions []revision nextRevisionId int + // state expiry feature + enableStateExpiry bool + epoch types.StateEpoch // epoch indicate stateDB start at which block's target epoch + remoteNode interface{} //RemoteFullStateNode //TODO(0xbundler): add interface to fetch expired proof from remote + // Measurements gathered during execution for debugging purposes // MetricsMux should be used in more places, but will affect on performance, so following meteration is not accruate MetricsMux sync.Mutex @@ -193,6 +198,7 @@ func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) accessList: newAccessList(), transientStorage: newTransientStorage(), hasher: crypto.NewKeccakState(), + epoch: types.StateEpoch0, } if sdb.snaps != nil { @@ -232,6 +238,18 @@ func (s *StateDB) TransferPrefetcher(prev *StateDB) { s.prefetcherLock.Unlock() } +// SetEpoch it must set in initial, reset later will cause wrong result +func (s *StateDB) SetEpoch(config *params.ChainConfig, height *big.Int) *StateDB { + s.epoch = types.GetStateEpoch(config, height) + return s +} + +// SetRemoteNode it must set in initial, reset later will cause wrong result +func (s *StateDB) SetRemoteNode(remote interface{}) *StateDB { + s.remoteNode = remote + return s +} + // StartPrefetcher initializes a new trie prefetcher to pull in nodes from the // state trie concurrently while the state is mutated so that when we reach the // commit phase, most of the needed data is already hot. @@ -1886,6 +1904,13 @@ func (s *StateDB) AddSlotToAccessList(addr common.Address, slot common.Hash) { } } +func (s *StateDB) EnableExpire() bool { + if !s.enableStateExpiry { + return false + } + return types.EpochExpired(types.StateEpoch0, s.epoch) +} + // AddressInAccessList returns true if the given address is in the access list. func (s *StateDB) AddressInAccessList(addr common.Address) bool { if s.accessList == nil { diff --git a/core/types/state_epoch.go b/core/types/state_epoch.go new file mode 100644 index 0000000000..741367fb30 --- /dev/null +++ b/core/types/state_epoch.go @@ -0,0 +1,62 @@ +package types + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" +) + +const ( + DefaultStateEpochPeriod = uint64(7_008_000) + StateEpoch0 = StateEpoch(0) + StateEpoch1 = StateEpoch(1) + StateEpochKeepLiveNum = StateEpoch(2) +) + +type StateEpoch uint16 + +// GetStateEpoch computes the current state epoch by hard fork and block number +// state epoch will indicate if the state is accessible or expiry. +// Before ClaudeBlock indicates state epoch0. +// ClaudeBlock indicates start state epoch1. +// ElwoodBlock indicates start state epoch2 and start epoch rotate by StateEpochPeriod. +// When N>=2 and epochN started, epoch(N-2)'s state will expire. +func GetStateEpoch(config *params.ChainConfig, blockNumber *big.Int) StateEpoch { + if blockNumber == nil || config == nil { + return StateEpoch0 + } + epochPeriod := new(big.Int).SetUint64(DefaultStateEpochPeriod) + epoch1Block := epochPeriod + epoch2Block := new(big.Int).Add(epoch1Block, epochPeriod) + + if config.Clique != nil && config.Clique.StateEpochPeriod != 0 { + epochPeriod = new(big.Int).SetUint64(config.Clique.StateEpochPeriod) + epoch1Block = new(big.Int).SetUint64(config.Clique.StateEpoch1Block) + epoch2Block = new(big.Int).SetUint64(config.Clique.StateEpoch2Block) + } + if isBlockReached(blockNumber, epoch2Block) { + ret := new(big.Int).Sub(blockNumber, epoch2Block) + ret.Div(ret, epochPeriod) + ret.Add(ret, common.Big2) + return StateEpoch(ret.Uint64()) + } + if isBlockReached(blockNumber, epoch1Block) { + return 1 + } + + return 0 +} + +// EpochExpired check pre epoch if expired compared to current epoch +func EpochExpired(pre StateEpoch, cur StateEpoch) bool { + return cur > pre && cur-pre >= StateEpochKeepLiveNum +} + +// isBlockReached check if reach expected block number +func isBlockReached(block, expected *big.Int) bool { + if block == nil || expected == nil { + return false + } + return block.Cmp(expected) >= 0 +} From abed24844a04ca7683ae9a4adbf5f39e1e0278a7 Mon Sep 17 00:00:00 2001 From: asyukii Date: Wed, 23 Aug 2023 11:48:47 +0800 Subject: [PATCH 07/65] feat: implement epoch-based trie fix minor test bug --- trie/errors.go | 17 ++ trie/node.go | 58 ++++- trie/proof.go | 60 +++++- trie/proof_test.go | 19 +- trie/trie.go | 513 +++++++++++++++++++++++++++++++++++++++++---- trie/trie_test.go | 53 ++++- 6 files changed, 663 insertions(+), 57 deletions(-) diff --git a/trie/errors.go b/trie/errors.go index 7be7041c7f..199856a585 100644 --- a/trie/errors.go +++ b/trie/errors.go @@ -21,6 +21,7 @@ import ( "fmt" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" ) // ErrCommitted is returned when a already committed trie is requested for usage. @@ -50,3 +51,19 @@ func (err *MissingNodeError) Error() string { } return fmt.Sprintf("missing trie node %x (owner %x) (path %x) %v", err.NodeHash, err.Owner, err.Path, err.err) } + +type ExpiredNodeError struct { + Path []byte // hex-encoded path to the expired node + Epoch types.StateEpoch +} + +func NewExpiredNodeError(path []byte, epoch types.StateEpoch) error { + return &ExpiredNodeError{ + Path: path, + Epoch: epoch, + } +} + +func (err *ExpiredNodeError) Error() string { + return "expired trie node" +} diff --git a/trie/node.go b/trie/node.go index d78ed5c569..19fb2562fe 100644 --- a/trie/node.go +++ b/trie/node.go @@ -22,6 +22,7 @@ import ( "strings" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" ) @@ -37,11 +38,14 @@ type ( fullNode struct { Children [17]node // Actual trie node data to encode/decode (needs custom encoder) flags nodeFlag + EpochMap [16]types.StateEpoch `rlp:"-" json:"-"` + epoch types.StateEpoch `rlp:"-" json:"-"` } shortNode struct { Key []byte Val node flags nodeFlag + epoch types.StateEpoch `rlp:"-" json:"-"` } hashNode []byte valueNode []byte @@ -58,6 +62,56 @@ func (n *fullNode) EncodeRLP(w io.Writer) error { return eb.Flush() } +func (n *fullNode) setEpoch(epoch types.StateEpoch) { + if n.epoch >= epoch { + return + } + n.epoch = epoch +} + +func (n *shortNode) setEpoch(epoch types.StateEpoch) { + if n.epoch >= epoch { + return + } + n.epoch = epoch +} + +func (n *fullNode) getEpoch() types.StateEpoch { + return n.epoch +} + +func (n *shortNode) getEpoch() types.StateEpoch { + return n.epoch +} + +func (n *fullNode) GetChildEpoch(index int) types.StateEpoch { + if index < 16 { + return n.EpochMap[index] + } + return n.epoch +} + +func (n *fullNode) UpdateChildEpoch(index int, epoch types.StateEpoch) { + if index < 16 { + n.EpochMap[index] = epoch + } +} + +func (n *fullNode) SetEpochMap(epochMap [16]types.StateEpoch) { + n.EpochMap = epochMap +} + +func (n *fullNode) ChildExpired(prefix []byte, index int, currentEpoch types.StateEpoch) (bool, error) { + childEpoch := n.GetChildEpoch(index) + if types.EpochExpired(childEpoch, currentEpoch) { + return true, &ExpiredNodeError{ + Path: prefix, + Epoch: childEpoch, + } + } + return false, nil +} + func (n *fullNode) copy() *fullNode { copy := *n; return © } func (n *shortNode) copy() *shortNode { copy := *n; return © } @@ -181,13 +235,13 @@ func decodeShort(hash, elems []byte) (node, error) { if err != nil { return nil, fmt.Errorf("invalid value node: %v", err) } - return &shortNode{key, valueNode(val), flag}, nil + return &shortNode{Key: key, Val: valueNode(val), flags: flag}, nil } r, _, err := decodeRef(rest) if err != nil { return nil, wrapError(err, "val") } - return &shortNode{key, r, flag}, nil + return &shortNode{Key: key, Val: r, flags: flag}, nil } func decodeFull(hash, elems []byte) (*fullNode, error) { diff --git a/trie/proof.go b/trie/proof.go index c7108ba138..ecd9f9d7cb 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -22,6 +22,7 @@ import ( "fmt" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" ) @@ -115,7 +116,7 @@ func (t *StateTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error { // If the trie contains the key, the returned node is the node that contains the // value for the key. If nodes is specified, the traversed nodes are appended to // it. -func (t *Trie) traverseNodes(tn node, key []byte, nodes *[]node) (node, error) { +func (t *Trie) traverseNodes(tn node, key []byte, nodes *[]node, epoch types.StateEpoch, updateEpoch bool) (node, error) { var prefix []byte @@ -178,7 +179,7 @@ func (t *Trie) ProvePath(key []byte, prefixKeyHex []byte, proofDb ethdb.KeyValue // traverse down using the prefixKeyHex var nodes []node tn := t.root - startNode, err := t.traverseNodes(tn, prefixKeyHex, nil) // obtain the node where the prefixKeyHex leads to + startNode, err := t.traverseNodes(tn, prefixKeyHex, nil, 0, false) // obtain the node where the prefixKeyHex leads to if err != nil { return err } @@ -186,7 +187,7 @@ func (t *Trie) ProvePath(key []byte, prefixKeyHex []byte, proofDb ethdb.KeyValue key = key[len(prefixKeyHex):] // obtain the suffix key // traverse through the suffix key - _, err = t.traverseNodes(startNode, key, &nodes) + _, err = t.traverseNodes(startNode, key, &nodes, 0, false) if err != nil { return err } @@ -211,13 +212,13 @@ func (t *Trie) ProvePath(key []byte, prefixKeyHex []byte, proofDb ethdb.KeyValue } // VerifyPathProof reconstructs the trie from the given proof and verifies the root hash. -func VerifyPathProof(keyHex []byte, prefixKeyHex []byte, proofList [][]byte) (node, hashNode, error) { +func VerifyPathProof(keyHex []byte, prefixKeyHex []byte, proofList [][]byte, epoch types.StateEpoch) (node, hashNode, error) { if len(proofList) == 0 { return nil, nil, fmt.Errorf("proof list is empty") } - n, err := ConstructTrieFromProof(keyHex, prefixKeyHex, proofList) + n, err := ConstructTrieFromProof(keyHex, prefixKeyHex, proofList, epoch) if err != nil { return nil, nil, err } @@ -234,7 +235,7 @@ func VerifyPathProof(keyHex []byte, prefixKeyHex []byte, proofList [][]byte) (no } // ConstructTrieFromProof constructs a trie from the given proof. It returns the root node of the trie. -func ConstructTrieFromProof(keyHex []byte, prefixKeyHex []byte, proofList [][]byte) (node, error) { +func ConstructTrieFromProof(keyHex []byte, prefixKeyHex []byte, proofList [][]byte, epoch types.StateEpoch) (node, error) { var parentNode node var root node @@ -276,9 +277,11 @@ func ConstructTrieFromProof(keyHex []byte, prefixKeyHex []byte, proofList [][]by switch sn := parentNode.(type) { case *shortNode: sn.Val = cld + sn.setEpoch(epoch) return root, nil case *fullNode: sn.Children[keyHex[0]] = cld + sn.setEpoch(epoch) return root, nil } } @@ -287,16 +290,61 @@ func ConstructTrieFromProof(keyHex []byte, prefixKeyHex []byte, proofList [][]by switch sn := parentNode.(type) { case *shortNode: sn.Val = n + sn.setEpoch(epoch) parentNode = n case *fullNode: sn.Children[keyHex[0]] = n + sn.setEpoch(epoch) + sn.UpdateChildEpoch(int(keyHex[0]), epoch) parentNode = n } } + // Continue traverse down the trie to update the epoch of the child nodes + err := updateEpochInChildNodes(&parentNode, keyHex, epoch) + if err != nil { + return nil, err + } + return root, nil } +// updateEpochInChildNodes traverse down a node and update the epoch of the child nodes +func updateEpochInChildNodes(tn *node, key []byte, epoch types.StateEpoch) error { + + node := *tn + startNode := node + + for len(key) > 0 && tn != nil { + switch n := node.(type) { + case *shortNode: + if len(key) < len(n.Key) || !bytes.Equal(n.Key, key[:len(n.Key)]) { + // The trie doesn't contain the key. + node = nil + } else { + node = n.Val + key = key[len(n.Key):] + } + n.setEpoch(epoch) + case *fullNode: + node = n.Children[key[0]] + n.UpdateChildEpoch(int(key[0]), epoch) + n.setEpoch(epoch) + + key = key[1:] + case hashNode: + return fmt.Errorf("cannot resolve hash node") + case valueNode: + default: + panic(fmt.Sprintf("%T: invalid node: %v", tn, tn)) + } + } + + *tn = startNode + + return nil +} + func (t *StateTrie) ProvePath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error { return t.trie.ProvePath(key, path, proofDb) } diff --git a/trie/proof_test.go b/trie/proof_test.go index 3f07ae7c51..f10a38d931 100644 --- a/trie/proof_test.go +++ b/trie/proof_test.go @@ -88,7 +88,7 @@ func TestOneElementPathProof(t *testing.T) { t.Errorf("proof should have one element") } - _, hn, err := VerifyPathProof(keybytesToHex([]byte("k")), nil, proofList) + _, hn, err := VerifyPathProof(keybytesToHex([]byte("k")), nil, proofList, 0) if err != nil { t.Fatalf("failed to verify proof: %v\nraw proof: %x", err, proofList) } @@ -1107,6 +1107,23 @@ func nonRandomTrie(n int) (*Trie, map[string]*kv) { return trie, vals } +func nonRandomTrieWithExpiry(n int) (*Trie, map[string]*kv) { + db := NewDatabase(rawdb.NewMemoryDatabase()) + trie := NewEmptyWithExpiry(db, 10) + vals := make(map[string]*kv) + max := uint64(0xffffffffffffffff) + for i := uint64(0); i < uint64(n); i++ { + value := make([]byte, 32) + key := make([]byte, 32) + binary.LittleEndian.PutUint64(key, i) + binary.LittleEndian.PutUint64(value, i-max) + elem := &kv{key, value, false} + trie.MustUpdate(elem.k, elem.v) + vals[string(elem.k)] = elem + } + return trie, vals +} + func TestRangeProofKeysWithSharedPrefix(t *testing.T) { keys := [][]byte{ common.Hex2Bytes("aa10000000000000000000000000000000000000000000000000000000000000"), diff --git a/trie/trie.go b/trie/trie.go index 1c8cbaeb99..166cf2413f 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -37,7 +37,7 @@ import ( // Trie is not safe for concurrent use. type Trie struct { root node - owner common.Hash + owner common.Hash // Can be used to identify account vs storage trie // Flag whether the commit operation is already performed. If so the // trie is not usable(latest states is invisible). @@ -49,11 +49,15 @@ type Trie struct { unhashed int // reader is the handler trie can retrieve nodes from. - reader *trieReader + reader *trieReader // TODO (asyukii): create a reader for state expiry metadata // tracer is the tool to track the trie changes. // It will be reset after each commit operation. tracer *tracer + + // fields for state expiry + rootEpoch types.StateEpoch + enableExpiry bool } // newFlag returns the cache flag value for a newly created node. @@ -64,12 +68,14 @@ func (t *Trie) newFlag() nodeFlag { // Copy returns a copy of Trie. func (t *Trie) Copy() *Trie { return &Trie{ - root: t.root, - owner: t.owner, - committed: t.committed, - unhashed: t.unhashed, - reader: t.reader, - tracer: t.tracer.copy(), + root: t.root, + owner: t.owner, + committed: t.committed, + unhashed: t.unhashed, + reader: t.reader, + tracer: t.tracer.copy(), + rootEpoch: t.rootEpoch, + enableExpiry: t.enableExpiry, } } @@ -105,6 +111,36 @@ func NewEmpty(db *Database) *Trie { return tr } +func NewEmptyWithExpiry(db *Database, rootEpoch types.StateEpoch) *Trie { + tr, _ := New(TrieID(types.EmptyRootHash), db) + tr.enableExpiry = true + tr.rootEpoch = rootEpoch + return tr +} + +// TODO (asyukii): handle meta storage later +func NewWithExpiry(id *ID, db *Database, rootEpoch types.StateEpoch) (*Trie, error) { + reader, err := newTrieReader(id.StateRoot, id.Owner, db) + if err != nil { + return nil, err + } + trie := &Trie{ + owner: id.Owner, + reader: reader, + tracer: newTracer(), + rootEpoch: rootEpoch, + enableExpiry: true, + } + if id.Root != (common.Hash{}) && id.Root != types.EmptyRootHash { + rootnode, err := trie.resolveAndTrack(id.Root[:], nil) + if err != nil { + return nil, err + } + trie.root = rootnode + } + return trie, nil +} + // MustNodeIterator is a wrapper of NodeIterator and will omit any encountered // error but just print out an error message. func (t *Trie) MustNodeIterator(start []byte) NodeIterator { @@ -140,12 +176,20 @@ func (t *Trie) MustGet(key []byte) []byte { // // If the requested node is not present in trie, no error will be returned. // If the trie is corrupted, a MissingNodeError is returned. -func (t *Trie) Get(key []byte) ([]byte, error) { +func (t *Trie) Get(key []byte) (value []byte, err error) { + var newroot node + var didResolve bool + // Short circuit if the trie is already committed and not usable. if t.committed { return nil, ErrCommitted } - value, newroot, didResolve, err := t.get(t.root, keybytesToHex(key), 0) + + if t.enableExpiry { + value, newroot, didResolve, err = t.getWithEpoch(t.root, keybytesToHex(key), 0, t.getRootEpoch()) + } else { + value, newroot, didResolve, err = t.get(t.root, keybytesToHex(key), 0) + } if err == nil && didResolve { t.root = newroot } @@ -188,6 +232,56 @@ func (t *Trie) get(origNode node, key []byte, pos int) (value []byte, newnode no } } +func (t *Trie) getWithEpoch(origNode node, key []byte, pos int, epoch types.StateEpoch) (value []byte, newnode node, didResolve bool, err error) { + if t.epochExpired(origNode, epoch) { + return nil, nil, false, NewExpiredNodeError(key[:pos], epoch) + } + switch n := (origNode).(type) { + case nil: + return nil, nil, false, nil + case valueNode: + return n, n, false, nil + case *shortNode: + if len(key)-pos < len(n.Key) || !bytes.Equal(n.Key, key[pos:pos+len(n.Key)]) { + // key not found in trie + return nil, n, false, nil + } + value, newnode, didResolve, err = t.getWithEpoch(n.Val, key, pos+len(n.Key), epoch) + if err == nil && t.renewNode(epoch, didResolve, true) { + n = n.copy() + n.Val = newnode + n.setEpoch(t.getRootEpoch()) + } + return value, n, didResolve, err + case *fullNode: + value, newnode, didResolve, err = t.getWithEpoch(n.Children[key[pos]], key, pos+1, n.GetChildEpoch(int(key[pos]))) + if err == nil && t.renewNode(epoch, didResolve, true) { + n = n.copy() + n.Children[key[pos]] = newnode + n.setEpoch(t.getRootEpoch()) + n.UpdateChildEpoch(int(key[pos]), t.getRootEpoch()) + } + return value, n, didResolve, err + case hashNode: + child, err := t.resolveAndTrack(n, key[:pos]) + if err != nil { + return nil, n, true, err + } + + if child, ok := child.(*fullNode); ok { + epochMap, err := t.resolveMeta(child, epoch, key[:pos]) + if err != nil { + return nil, n, true, err + } + child.SetEpochMap(epochMap) + } + value, newnode, _, err := t.getWithEpoch(child, key, pos, epoch) + return value, newnode, true, err + default: + panic(fmt.Sprintf("%T: invalid node: %v", origNode, origNode)) + } +} + // MustGetNode is a wrapper of GetNode and will omit any encountered error but // just print out an error message. func (t *Trie) MustGetNode(path []byte) ([]byte, int) { @@ -304,6 +398,10 @@ func (t *Trie) Update(key, value []byte) error { if t.committed { return ErrCommitted } + + if t.enableExpiry { + return t.updateWithEpoch(key, value, t.getRootEpoch()) + } return t.update(key, value) } @@ -326,6 +424,25 @@ func (t *Trie) update(key, value []byte) error { return nil } +func (t *Trie) updateWithEpoch(key, value []byte, epoch types.StateEpoch) error { + t.unhashed++ + k := keybytesToHex(key) + if len(value) != 0 { + _, n, err := t.insertWithEpoch(t.root, nil, k, valueNode(value), epoch) + if err != nil { + return err + } + t.root = n + } else { + _, n, err := t.deleteWithEpoch(t.root, nil, k, epoch) + if err != nil { + return err + } + t.root = n + } + return nil +} + func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error) { if len(key) == 0 { if v, ok := n.(valueNode); ok { @@ -343,7 +460,7 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error if !dirty || err != nil { return false, n, err } - return true, &shortNode{n.Key, nn, t.newFlag()}, nil + return true, &shortNode{Key: n.Key, Val: nn, flags: t.newFlag()}, nil } // Otherwise branch out at the index where they differ. branch := &fullNode{flags: t.newFlag()} @@ -366,7 +483,7 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error t.tracer.onInsert(append(prefix, key[:matchlen]...)) // Replace it with a short node leading up to the branch. - return true, &shortNode{key[:matchlen], branch, t.newFlag()}, nil + return true, &shortNode{Key: key[:matchlen], Val: branch, flags: t.newFlag()}, nil case *fullNode: dirty, nn, err := t.insert(n.Children[key[0]], append(prefix, key[0]), key[1:], value) @@ -384,7 +501,7 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error // since it's always embedded in its parent. t.tracer.onInsert(prefix) - return true, &shortNode{key, value, t.newFlag()}, nil + return true, &shortNode{Key: key, Val: value, flags: t.newFlag()}, nil case hashNode: // We've hit a part of the trie that isn't loaded yet. Load @@ -405,6 +522,103 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error } } +func (t *Trie) insertWithEpoch(n node, prefix, key []byte, value node, epoch types.StateEpoch) (bool, node, error) { + if t.epochExpired(n, epoch) { + return false, nil, NewExpiredNodeError(prefix, epoch) + } + + if len(key) == 0 { + if v, ok := n.(valueNode); ok { + return !bytes.Equal(v, value.(valueNode)), value, nil + } + return true, value, nil + } + switch n := n.(type) { + case *shortNode: + matchlen := prefixLen(key, n.Key) + // If the whole key matches, keep this short node as is + // and only update the value. + if matchlen == len(n.Key) { + dirty, nn, err := t.insertWithEpoch(n.Val, append(prefix, key[:matchlen]...), key[matchlen:], value, epoch) + if !t.renewNode(epoch, dirty, true) || err != nil { + return false, n, err + } + return true, &shortNode{Key: n.Key, Val: nn, flags: t.newFlag(), epoch: epoch}, nil + } + // Otherwise branch out at the index where they differ. + branch := &fullNode{flags: t.newFlag(), epoch: epoch} + var err error + _, branch.Children[n.Key[matchlen]], err = t.insertWithEpoch(nil, append(prefix, n.Key[:matchlen+1]...), n.Key[matchlen+1:], n.Val, epoch) + if err != nil { + return false, nil, err + } + branch.UpdateChildEpoch(int(n.Key[matchlen]), epoch) + + _, branch.Children[key[matchlen]], err = t.insertWithEpoch(nil, append(prefix, key[:matchlen+1]...), key[matchlen+1:], value, epoch) + if err != nil { + return false, nil, err + } + branch.UpdateChildEpoch(int(key[matchlen]), epoch) + + // Replace this shortNode with the branch if it occurs at index 0. + if matchlen == 0 { + return true, branch, nil + } + // New branch node is created as a child of the original short node. + // Track the newly inserted node in the tracer. The node identifier + // passed is the path from the root node. + t.tracer.onInsert(append(prefix, key[:matchlen]...)) + + // Replace it with a short node leading up to the branch. + return true, &shortNode{Key: key[:matchlen], Val: branch, flags: t.newFlag(), epoch: epoch}, nil + + case *fullNode: + dirty, nn, err := t.insertWithEpoch(n.Children[key[0]], append(prefix, key[0]), key[1:], value, n.GetChildEpoch(int(key[0]))) + if !dirty || err != nil { + return false, n, err + } + n = n.copy() + n.flags = t.newFlag() + n.Children[key[0]] = nn + return true, n, nil + + case nil: + // New short node is created and track it in the tracer. The node identifier + // passed is the path from the root node. Note the valueNode won't be tracked + // since it's always embedded in its parent. + t.tracer.onInsert(prefix) + + return true, &shortNode{Key: key, Val: value, flags: t.newFlag(), epoch: epoch}, nil + + case hashNode: + // We've hit a part of the trie that isn't loaded yet. Load + // the node and insert into it. This leaves all child nodes on + // the path to the value in the trie. + rn, err := t.resolveAndTrack(n, prefix) + if err != nil { + return false, nil, err + } + + // TODO(asyukii): if resolved node is a full node, then resolve epochMap as well + if child, ok := rn.(*fullNode); ok { + epochMap, err := t.resolveMeta(child, epoch, prefix) + if err != nil { + return false, nil, err + } + child.SetEpochMap(epochMap) + } + + dirty, nn, err := t.insertWithEpoch(rn, prefix, key, value, epoch) + if !t.renewNode(epoch, dirty, true) || err != nil { + return false, rn, err + } + return true, nn, nil + + default: + panic(fmt.Sprintf("%T: invalid node: %v", n, n)) + } +} + // MustDelete is a wrapper of Delete and will omit any encountered error but // just print out an error message. func (t *Trie) MustDelete(key []byte) { @@ -418,13 +632,21 @@ func (t *Trie) MustDelete(key []byte) { // If the requested node is not present in trie, no error will be returned. // If the trie is corrupted, a MissingNodeError is returned. func (t *Trie) Delete(key []byte) error { + var n node + var err error // Short circuit if the trie is already committed and not usable. if t.committed { return ErrCommitted } t.unhashed++ k := keybytesToHex(key) - _, n, err := t.delete(t.root, nil, k) + + if t.enableExpiry { + _, n, err = t.deleteWithEpoch(t.root, nil, k, t.getRootEpoch()) + } else { + _, n, err = t.delete(t.root, nil, k) + } + if err != nil { return err } @@ -470,9 +692,9 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) { // always creates a new slice) instead of append to // avoid modifying n.Key since it might be shared with // other nodes. - return true, &shortNode{concat(n.Key, child.Key...), child.Val, t.newFlag()}, nil + return true, &shortNode{Key: concat(n.Key, child.Key...), Val: child.Val, flags: t.newFlag()}, nil default: - return true, &shortNode{n.Key, child, t.newFlag()}, nil + return true, &shortNode{Key: n.Key, Val: child, flags: t.newFlag()}, nil } case *fullNode: @@ -531,12 +753,12 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) { t.tracer.onDelete(append(prefix, byte(pos))) k := append([]byte{byte(pos)}, cnode.Key...) - return true, &shortNode{k, cnode.Val, t.newFlag()}, nil + return true, &shortNode{Key: k, Val: cnode.Val, flags: t.newFlag()}, nil } } // Otherwise, n is replaced by a one-nibble short node // containing the child. - return true, &shortNode{[]byte{byte(pos)}, n.Children[pos], t.newFlag()}, nil + return true, &shortNode{Key: []byte{byte(pos)}, Val: n.Children[pos], flags: t.newFlag()}, nil } // n still contains at least two values and cannot be reduced. return true, n, nil @@ -566,6 +788,151 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) { } } +func (t *Trie) deleteWithEpoch(n node, prefix, key []byte, epoch types.StateEpoch) (bool, node, error) { + if t.epochExpired(n, epoch) { + return false, nil, NewExpiredNodeError(prefix, epoch) + } + + switch n := n.(type) { + case *shortNode: + matchlen := prefixLen(key, n.Key) + if matchlen < len(n.Key) { + return false, n, nil // don't replace n on mismatch + } + if matchlen == len(key) { + // The matched short node is deleted entirely and track + // it in the deletion set. The same the valueNode doesn't + // need to be tracked at all since it's always embedded. + t.tracer.onDelete(prefix) + + return true, nil, nil // remove n entirely for whole matches + } + // The key is longer than n.Key. Remove the remaining suffix + // from the subtrie. Child can never be nil here since the + // subtrie must contain at least two other values with keys + // longer than n.Key. + dirty, child, err := t.deleteWithEpoch(n.Val, append(prefix, key[:len(n.Key)]...), key[len(n.Key):], epoch) + if !t.renewNode(epoch, dirty, true) || err != nil { + return false, n, err + } + switch child := child.(type) { + case *shortNode: + // The child shortNode is merged into its parent, track + // is deleted as well. + t.tracer.onDelete(append(prefix, n.Key...)) + + // Deleting from the subtrie reduced it to another + // short node. Merge the nodes to avoid creating a + // shortNode{..., shortNode{...}}. Use concat (which + // always creates a new slice) instead of append to + // avoid modifying n.Key since it might be shared with + // other nodes. + return true, &shortNode{Key: concat(n.Key, child.Key...), Val: child.Val, flags: t.newFlag(), epoch: epoch}, nil + default: + return true, &shortNode{Key: n.Key, Val: child, flags: t.newFlag(), epoch: epoch}, nil + } + + case *fullNode: + dirty, nn, err := t.deleteWithEpoch(n.Children[key[0]], append(prefix, key[0]), key[1:], n.GetChildEpoch(int(key[0]))) + if !t.renewNode(epoch, dirty, true) || err != nil { + return false, n, err + } + n = n.copy() + n.flags = t.newFlag() + n.Children[key[0]] = nn + + // Because n is a full node, it must've contained at least two children + // before the delete operation. If the new child value is non-nil, n still + // has at least two children after the deletion, and cannot be reduced to + // a short node. + if nn != nil { + return true, n, nil + } + // Reduction: + // Check how many non-nil entries are left after deleting and + // reduce the full node to a short node if only one entry is + // left. Since n must've contained at least two children + // before deletion (otherwise it would not be a full node) n + // can never be reduced to nil. + // + // When the loop is done, pos contains the index of the single + // value that is left in n or -2 if n contains at least two + // values. + pos := -1 + for i, cld := range &n.Children { + if cld != nil { + if pos == -1 { + pos = i + } else { + pos = -2 + break + } + } + } + if pos >= 0 { + if pos != 16 { + // If the remaining entry is a short node, it replaces + // n and its key gets the missing nibble tacked to the + // front. This avoids creating an invalid + // shortNode{..., shortNode{...}}. Since the entry + // might not be loaded yet, resolve it just for this + // check. + cnode, err := t.resolve(n.Children[pos], append(prefix, byte(pos))) + if err != nil { + return false, nil, err + } + if cnode, ok := cnode.(*shortNode); ok { + // Replace the entire full node with the short node. + // Mark the original short node as deleted since the + // value is embedded into the parent now. + t.tracer.onDelete(append(prefix, byte(pos))) + + k := append([]byte{byte(pos)}, cnode.Key...) + return true, &shortNode{Key: k, Val: cnode.Val, flags: t.newFlag()}, nil + } + } + // Otherwise, n is replaced by a one-nibble short node + // containing the child. + return true, &shortNode{Key: []byte{byte(pos)}, Val: n.Children[pos], flags: t.newFlag(), epoch: epoch}, nil + } + // n still contains at least two values and cannot be reduced. + return true, n, nil + + case valueNode: + return true, nil, nil + + case nil: + return false, nil, nil + + case hashNode: + // We've hit a part of the trie that isn't loaded yet. Load + // the node and delete from it. This leaves all child nodes on + // the path to the value in the trie. + rn, err := t.resolveAndTrack(n, prefix) + if err != nil { + return false, nil, err + } + + if child, ok := rn.(*fullNode); ok { + epochMap, err := t.resolveMeta(child, epoch, prefix) + if err != nil { + return false, nil, err + } + child.SetEpochMap(epochMap) + } + + dirty, nn, err := t.deleteWithEpoch(rn, prefix, key, epoch) + if !dirty || err != nil { + return false, rn, err + } + return true, nn, nil + + default: + panic(fmt.Sprintf("%T: invalid node: %v (%v)", n, n, key)) + } + +} + func concat(s1 []byte, s2 ...byte) []byte { r := make([]byte, len(s1)+len(s2)) copy(r, s1) @@ -593,6 +960,12 @@ func (t *Trie) resolveAndTrack(n hashNode, prefix []byte) (node, error) { return mustDecodeNode(n, blob), nil } +// TODO(asyukii): implement resolve full node's epoch map. +func (t *Trie) resolveMeta(n node, epoch types.StateEpoch, prefix []byte) ([16]types.StateEpoch, error) { + // 1. Check if the node is a full node + panic("implement me!") +} + // Hash returns the root hash of the trie. It does not write to the // database and can be used even if the trie doesn't have one. func (t *Trie) Hash() common.Hash { @@ -684,29 +1057,38 @@ func (t *Trie) Owner() common.Hash { } // ReviveTrie revives a trie by prefix key with the given proof list. -func (t *Trie) ReviveTrie(key []byte, prefixKeyHex []byte, proofList [][]byte) { +func (t *Trie) ReviveTrie(key []byte, prefixKeyHex []byte, proofList [][]byte) error { + key = keybytesToHex(key) // Verify the proof first - revivedNode, revivedHash, err := VerifyPathProof(key, prefixKeyHex, proofList) + revivedNode, revivedHash, err := VerifyPathProof(key, prefixKeyHex, proofList, t.getRootEpoch()) if err != nil { - log.Error("Failed to verify proof", "err", err) + return err } - newRoot, _, err := t.revive(t.root, key, prefixKeyHex, 0, revivedNode, common.BytesToHash(revivedHash)) + newRoot, _, err := t.revive(t.root, key, prefixKeyHex, 0, revivedNode, common.BytesToHash(revivedHash), t.getRootEpoch(), false) if err != nil { - log.Error("Failed to revive trie", "err", err) + return err } + t.root = newRoot + + return nil } -func (t *Trie) revive(n node, key []byte, prefixKeyHex []byte, pos int, revivedNode node, revivedHash common.Hash) (node, bool, error) { +func (t *Trie) revive(n node, key []byte, prefixKeyHex []byte, pos int, revivedNode node, revivedHash common.Hash, epoch types.StateEpoch, isExpired bool) (node, bool, error) { if pos > len(prefixKeyHex) { return nil, false, fmt.Errorf("target revive node not found") } if pos == len(prefixKeyHex) { + + if !isExpired { + return nil, false, fmt.Errorf("target revive node is not expired") + } + hn, ok := n.(hashNode) if !ok { return nil, false, fmt.Errorf("prefix key path does not lead to a hash node") @@ -720,24 +1102,32 @@ func (t *Trie) revive(n node, key []byte, prefixKeyHex []byte, pos int, revivedN return revivedNode, true, nil } + if isExpired { + return nil, false, NewExpiredNodeError(key[:pos], epoch) + } + switch n := n.(type) { case *shortNode: if len(key)-pos < len(n.Key) || !bytes.Equal(n.Key, key[pos:pos+len(n.Key)]) { // key not found in trie return n, false, nil } - newNode, didRevived, err := t.revive(n.Val, key, prefixKeyHex, pos+len(n.Key), revivedNode, revivedHash) + newNode, didRevived, err := t.revive(n.Val, key, prefixKeyHex, pos+len(n.Key), revivedNode, revivedHash, epoch, isExpired) if err == nil && didRevived { n = n.copy() n.Val = newNode + n.setEpoch(t.getRootEpoch()) } return n, didRevived, err case *fullNode: childIndex := int(key[pos]) - newNode, didRevived, err := t.revive(n.Children[childIndex], key, prefixKeyHex, pos+1, revivedNode, revivedHash) + childExpired, _ := n.ChildExpired(key[:pos], childIndex, t.getRootEpoch()) + newNode, didRevived, err := t.revive(n.Children[childIndex], key, prefixKeyHex, pos+1, revivedNode, revivedHash, n.GetChildEpoch(childIndex), childExpired) if err == nil && didRevived { n = n.copy() n.Children[key[pos]] = newNode + n.setEpoch(t.getRootEpoch()) + n.UpdateChildEpoch(childIndex, t.getRootEpoch()) } return n, didRevived, err case hashNode: @@ -745,7 +1135,16 @@ func (t *Trie) revive(n node, key []byte, prefixKeyHex []byte, pos int, revivedN if err != nil { return nil, false, err } - newNode, _, err := t.revive(child, key, prefixKeyHex, pos, revivedNode, revivedHash) + + if child, ok := child.(*fullNode); ok { + epochMap, err := t.resolveMeta(child, epoch, key[:pos]) + if err != nil { + return nil, false, err + } + child.SetEpochMap(epochMap) + } + + newNode, _, err := t.revive(child, key, prefixKeyHex, pos, revivedNode, revivedHash, epoch, isExpired) return newNode, true, err case nil: return nil, false, nil @@ -758,7 +1157,7 @@ func (t *Trie) revive(n node, key []byte, prefixKeyHex []byte, pos int, revivedN // It is not used in the actual trie implementation. ExpireByPrefix makes sure // only a child node of a full node is expired, if not an error is returned. func (t *Trie) ExpireByPrefix(prefixKeyHex []byte) error { - hn, err := t.expireByPrefix(t.root, prefixKeyHex) + hn, _, err := t.expireByPrefix(t.root, prefixKeyHex) if prefixKeyHex == nil && hn != nil { t.root = hn } @@ -768,41 +1167,42 @@ func (t *Trie) ExpireByPrefix(prefixKeyHex []byte) error { return nil } -func (t *Trie) expireByPrefix(n node, prefixKeyHex []byte) (node, error) { +func (t *Trie) expireByPrefix(n node, prefixKeyHex []byte) (node, bool, error) { // Loop through prefix key // When prefix key is empty, generate the hash node of the current node // Replace current node with the hash node + // If length of prefix key is empty if len(prefixKeyHex) == 0 { hasher := newHasher(false) defer returnHasherToPool(hasher) var hn node _, hn = hasher.proofHash(n) if _, ok := hn.(hashNode); ok { - return hn, nil + return hn, false, nil } - return nil, nil + return nil, true, nil } switch n := n.(type) { case *shortNode: matchLen := prefixLen(prefixKeyHex, n.Key) - hn, err := t.expireByPrefix(n.Val, prefixKeyHex[matchLen:]) + hn, didUpdateEpoch, err := t.expireByPrefix(n.Val, prefixKeyHex[matchLen:]) if err != nil { - return nil, err + return nil, didUpdateEpoch, err } if hn != nil { - return nil, fmt.Errorf("can only expire child short node") + return nil, didUpdateEpoch, fmt.Errorf("can only expire child short node") } - return nil, err + return nil, didUpdateEpoch, err case *fullNode: childIndex := int(prefixKeyHex[0]) - hn, err := t.expireByPrefix(n.Children[childIndex], prefixKeyHex[1:]) + hn, didUpdateEpoch, err := t.expireByPrefix(n.Children[childIndex], prefixKeyHex[1:]) if err != nil { - return nil, err + return nil, didUpdateEpoch, err } // Replace child node with hash node @@ -810,8 +1210,43 @@ func (t *Trie) expireByPrefix(n node, prefixKeyHex []byte) (node, error) { n.Children[prefixKeyHex[0]] = hn } - return nil, err + // Update the epoch so that it is expired + if !didUpdateEpoch { + n.UpdateChildEpoch(childIndex, 0) + didUpdateEpoch = true + } + + return nil, didUpdateEpoch, err default: - return nil, fmt.Errorf("invalid node type") + return nil, false, fmt.Errorf("invalid node type") + } +} + +func (t *Trie) getRootEpoch() types.StateEpoch { + return t.rootEpoch +} + +// renewNode check if renew node, according to trie node epoch and childDirty, +// childDirty or updateEpoch need copy for prevent reuse trie cache +func (t *Trie) renewNode(epoch types.StateEpoch, childDirty bool, updateEpoch bool) bool { + // when !updateEpoch, it same as !t.withShadowNodes + if !t.enableExpiry || !updateEpoch { + return childDirty + } + + // when no epoch update, same as before + if epoch == t.getRootEpoch() { + return childDirty + } + + // node need update epoch, just renew + return true +} + +func (t *Trie) epochExpired(n node, epoch types.StateEpoch) bool { + // when node is nil, skip epoch check + if !t.enableExpiry || n == nil { + return false } + return types.EpochExpired(epoch, t.getRootEpoch()) } diff --git a/trie/trie_test.go b/trie/trie_test.go index 60daed97de..6364499608 100644 --- a/trie/trie_test.go +++ b/trie/trie_test.go @@ -996,6 +996,40 @@ func TestCommitSequenceSmallRoot(t *testing.T) { } } +func TestRevive(t *testing.T) { + trie, vals := nonRandomTrieWithExpiry(100) + + oriRootHash := trie.Hash() + + for _, kv := range vals { + key := kv.k + val := kv.v + prefixKeys := getFullNodePrefixKeys(trie, key) + for _, prefixKey := range prefixKeys { + // Generate proof + var proof proofList + err := trie.ProvePath(key, prefixKey, &proof) + assert.NoError(t, err) + + // Expire trie + trie.ExpireByPrefix(prefixKey) + + // Revive trie + trie.ReviveTrie(key, prefixKey, proof) + + v := trie.MustGet(key) + assert.Equal(t, val, v, "value mismatch, got %x, exp %x, key %x, prefixKey %x", v, val, key, prefixKey) + + // Verify root hash + currRootHash := trie.Hash() + assert.Equal(t, oriRootHash, currRootHash, "root hash mismatch, got %x, exp %x, key %x, prefixKey %x", currRootHash, oriRootHash, key, prefixKey) + + // Reset trie + trie, _ = nonRandomTrieWithExpiry(100) + } + } +} + func TestReviveCustom(t *testing.T) { data := map[string]string{ @@ -1003,7 +1037,7 @@ func TestReviveCustom(t *testing.T) { "defg": "E", "defh": "F", "degh": "G", "degi": "H", } - trie := createCustomTrie(data) + trie := createCustomTrie(data, 10) oriRootHash := trie.Hash() @@ -1021,20 +1055,21 @@ func TestReviveCustom(t *testing.T) { trie.ReviveTrie(key, prefixKey, proofList) v := trie.MustGet(key) - assert.Equal(t, val, v) + assert.Equal(t, val, v, "value mismatch, got %x, exp %x, key %x, prefixKey %x", v, val, key, prefixKey) // Verify root hash currRootHash := trie.Hash() assert.Equal(t, oriRootHash, currRootHash, "root hash mismatch, got %x, exp %x, key %x, prefixKey %x", currRootHash, oriRootHash, key, prefixKey) // Reset trie - trie = createCustomTrie(data) + trie = createCustomTrie(data, 10) } } } -func createCustomTrie(data map[string]string) *Trie { - trie := NewEmpty(NewDatabase(rawdb.NewMemoryDatabase())) +func createCustomTrie(data map[string]string, epoch types.StateEpoch) *Trie { + db := NewDatabase(rawdb.NewMemoryDatabase()) + trie := NewEmptyWithExpiry(db, epoch) for k, v := range data { trie.MustUpdate([]byte(k), []byte(v)) } @@ -1068,10 +1103,10 @@ func getFullNodePrefixKeys(t *Trie, key []byte) [][]byte { } } - // Remove the first item in prefixKeys, which is the empty key - if len(prefixKeys) > 0 { - prefixKeys = prefixKeys[1:] - } + // // Remove the first item in prefixKeys, which is the empty key + // if len(prefixKeys) > 0 { + // prefixKeys = prefixKeys[1:] + // } return prefixKeys } From 7ce6bebfb57c614bf5becdb7ed479fa82dfa52b8 Mon Sep 17 00:00:00 2001 From: asyukii Date: Thu, 24 Aug 2023 13:17:32 +0800 Subject: [PATCH 08/65] feat: add eth_getStorageReviveProof minor fix change rpc blockNum to hash --- core/state/database.go | 2 + ethclient/gethclient/gethclient.go | 24 ++++++++ ethclient/gethclient/gethclient_test.go | 24 ++++++++ internal/ethapi/api.go | 76 +++++++++++++++++++++++++ light/trie.go | 4 ++ trie/proof.go | 2 +- 6 files changed, 131 insertions(+), 1 deletion(-) diff --git a/core/state/database.go b/core/state/database.go index cc5dc73c77..2810d91058 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -154,6 +154,8 @@ type Trie interface { // nodes of the longest existing prefix of the key (at least the root), ending // with the node that proves the absence of the key. Prove(key []byte, proofDb ethdb.KeyValueWriter) error + + ProvePath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error } // NewDatabase creates a backing store for state. The returned database is safe for diff --git a/ethclient/gethclient/gethclient.go b/ethclient/gethclient/gethclient.go index c029611678..9ded8cb9e2 100644 --- a/ethclient/gethclient/gethclient.go +++ b/ethclient/gethclient/gethclient.go @@ -78,6 +78,12 @@ type StorageResult struct { Proof []string `json:"proof"` } +type ReviveStorageResult struct { + Key string `json:"key"` + PrefixKey string `json:"prefixKey"` + Proof []string `json:"proof"` +} + // GetProof returns the account and storage values of the specified account including the Merkle-proof. // The block number can be nil, in which case the value is taken from the latest known block. func (ec *Client) GetProof(ctx context.Context, account common.Address, keys []string, blockNumber *big.Int) (*AccountResult, error) { @@ -125,6 +131,24 @@ func (ec *Client) GetProof(ctx context.Context, account common.Address, keys []s return &result, err } +// GetStorageReviveProof returns the proof for the given keys. Prefix keys can be specified to obtain partial proof for a given key. +// Both keys and prefix keys should have the same length. If user wish to obtain full proof for a given key, the corresponding prefix key should be empty string. +func (ec *Client) GetStorageReviveProof(ctx context.Context, account common.Address, keys []string, prefixKeys []string, hash common.Hash) ([]ReviveStorageResult, error) { + var err error + storageResults := make([]ReviveStorageResult, 0, len(keys)) + + if len(keys) != len(prefixKeys) { + return nil, fmt.Errorf("keys and prefixKeys must be same length") + } + + if hash == (common.Hash{}) { + err = ec.c.CallContext(ctx, &storageResults, "eth_getStorageReviveProof", account, keys, prefixKeys, "latest") + } else { + err = ec.c.CallContext(ctx, &storageResults, "eth_getStorageReviveProof", account, keys, prefixKeys, hash) + } + return storageResults, err +} + // CallContract executes a message call transaction, which is directly executed in the VM // of the node, but never mined into the blockchain. // diff --git a/ethclient/gethclient/gethclient_test.go b/ethclient/gethclient/gethclient_test.go index 65adae8ea7..b1ea98f19f 100644 --- a/ethclient/gethclient/gethclient_test.go +++ b/ethclient/gethclient/gethclient_test.go @@ -105,6 +105,9 @@ func TestGethClient(t *testing.T) { { "TestGetProof", func(t *testing.T) { testGetProof(t, client) }, + }, { + "TestGetStorageReviveProof", + func(t *testing.T) { testGetStorageReviveProof(t, client) }, }, { "TestGetProofCanonicalizeKeys", func(t *testing.T) { testGetProofCanonicalizeKeys(t, client) }, @@ -236,6 +239,27 @@ func testGetProof(t *testing.T, client *rpc.Client) { } } +func testGetStorageReviveProof(t *testing.T, client *rpc.Client) { + ec := New(client) + result, err := ec.GetStorageReviveProof(context.Background(), testAddr, []string{testSlot.String()}, []string{""}, common.Hash{}) + if err != nil { + t.Fatal(err) + } + + // test storage + if len(result) != 1 { + t.Fatalf("invalid storage proof, want 1 proof, got %v proof(s)", len(result)) + } + + if result[0].Key != testSlot.String() { + t.Fatalf("invalid storage proof key, want: %q, got: %q", testSlot.String(), result[0].Key) + } + + if result[0].PrefixKey != "" { + t.Fatalf("invalid storage proof prefix key, want: %q, got: %q", "", result[0].PrefixKey) + } +} + func testGetProofCanonicalizeKeys(t *testing.T, client *rpc.Client) { ec := New(client) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 810e8bba60..4b649b8f36 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -669,6 +669,12 @@ type StorageResult struct { Proof []string `json:"proof"` } +type ReviveStorageResult struct { + Key string `json:"key"` + PrefixKey string `json:"prefixKey"` + Proof []string `json:"proof"` +} + // proofList implements ethdb.KeyValueWriter and collects the proofs as // hex-strings for delivery to rpc-caller. type proofList []string @@ -757,6 +763,76 @@ func (s *BlockChainAPI) GetProof(ctx context.Context, address common.Address, st }, state.Error() } +// GetStorageReviveProof returns the proof for the given keys. Prefix keys can be specified to obtain partial proof for a given key. +// Both keys and prefix keys should have the same length. If user wish to obtain full proof for a given key, the corresponding prefix key should be empty string. +func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, address common.Address, storageKeys []string, storagePrefixKeys []string, blockNrOrHash rpc.BlockNumberOrHash) ([]ReviveStorageResult, error) { + + if len(storageKeys) != len(storagePrefixKeys) { + return nil, errors.New("storageKeys and storagePrefixKeys must be same length") + } + + var ( + keys = make([]common.Hash, len(storageKeys)) + keyLengths = make([]int, len(storageKeys)) + prefixKeys = make([][]byte, len(storagePrefixKeys)) + storageProof = make([]ReviveStorageResult, len(storageKeys)) + storageTrie state.Trie + ) + // Deserialize all keys. This prevents state access on invalid input. + for i, hexKey := range storageKeys { + var err error + keys[i], keyLengths[i], err = decodeHash(hexKey) + if err != nil { + return nil, err + } + } + + // Decode prefix keys + for i, prefixKey := range storagePrefixKeys { + var err error + prefixKeys[i], err = hex.DecodeString(prefixKey) + if err != nil { + return nil, err + } + } + + state, _, err := s.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) + if state == nil || err != nil { + return nil, err + } + if storageTrie, err = state.StorageTrie(address); err != nil { + return nil, err + } + + // Must have storage trie + if storageTrie == nil { + return nil, errors.New("storageTrie is nil") + } + + // Create the proofs for the storageKeys. + for i, key := range keys { + // Output key encoding is a bit special: if the input was a 32-byte hash, it is + // returned as such. Otherwise, we apply the QUANTITY encoding mandated by the + // JSON-RPC spec for getProof. This behavior exists to preserve backwards + // compatibility with older client versions. + var outputKey string + if keyLengths[i] != 32 { + outputKey = hexutil.EncodeBig(key.Big()) + } else { + outputKey = hexutil.Encode(key[:]) + } + + var proof proofList + prefixKey := prefixKeys[i] + if err := storageTrie.ProvePath(crypto.Keccak256(key.Bytes()), prefixKey, &proof); err != nil { + return nil, err + } + storageProof[i] = ReviveStorageResult{outputKey, storagePrefixKeys[i], proof} + } + + return storageProof, nil +} + // decodeHash parses a hex-encoded 32-byte hash. The input may optionally // be prefixed by 0x and can have a byte length up to 32. func decodeHash(s string) (h common.Hash, inputLength int, err error) { diff --git a/light/trie.go b/light/trie.go index 529f1e5d89..3c5d2a7665 100644 --- a/light/trie.go +++ b/light/trie.go @@ -212,6 +212,10 @@ func (t *odrTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error { return errors.New("not implemented, needs client/server interface split") } +func (t *odrTrie) ProvePath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error { + return errors.New("not implemented, needs client/server interface split") +} + // do tries and retries to execute a function until it returns with no error or // an error type other than MissingNodeError func (t *odrTrie) do(key []byte, fn func() error) error { diff --git a/trie/proof.go b/trie/proof.go index ecd9f9d7cb..e4268f60c4 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -315,7 +315,7 @@ func updateEpochInChildNodes(tn *node, key []byte, epoch types.StateEpoch) error node := *tn startNode := node - for len(key) > 0 && tn != nil { + for len(key) > 0 && node != nil { switch n := node.(type) { case *shortNode: if len(key) < len(n.Key) || !bytes.Equal(n.Key, key[:len(n.Key)]) { From d700e95a4ba0e14ef04c78806e51f43fc6a0986f Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Mon, 28 Aug 2023 17:14:59 +0800 Subject: [PATCH 09/65] ethdb/fullstatedb: add FullStateDB interface; state/statedb: fetch expired state from remote; --- accounts/abi/bind/backends/simulated.go | 2 +- cmd/geth/config.go | 3 + cmd/geth/main.go | 1 + cmd/utils/flags.go | 5 ++ core/blockchain.go | 40 +++++++++++- core/blockchain_reader.go | 13 +++- core/blockchain_test.go | 2 +- core/state/state_object.go | 78 +++++++++++++---------- core/state/statedb.go | 26 ++++---- core/txpool/blobpool/blobpool.go | 4 +- core/txpool/blobpool/blobpool_test.go | 2 +- core/txpool/blobpool/interface.go | 3 +- core/txpool/legacypool/legacypool.go | 4 +- core/txpool/legacypool/legacypool_test.go | 2 +- core/types/revive_state.go | 7 ++ eth/api_backend.go | 4 +- eth/api_debug.go | 6 +- eth/backend.go | 5 ++ eth/ethconfig/config.go | 3 +- eth/protocols/eth/handler_test.go | 5 +- eth/state_accessor.go | 16 ++++- eth/tracers/api_test.go | 2 +- ethclient/gethclient/gethclient.go | 10 +-- ethdb/fullstatedb.go | 54 ++++++++++++++++ internal/ethapi/api.go | 16 ++--- internal/ethapi/api_test.go | 2 +- miner/miner_test.go | 2 +- miner/worker.go | 2 +- 28 files changed, 228 insertions(+), 91 deletions(-) create mode 100644 core/types/revive_state.go create mode 100644 ethdb/fullstatedb.go diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go index 3633ac5c4a..acc1143497 100644 --- a/accounts/abi/bind/backends/simulated.go +++ b/accounts/abi/bind/backends/simulated.go @@ -188,7 +188,7 @@ func (b *SimulatedBackend) stateByBlockNumber(ctx context.Context, blockNumber * if err != nil { return nil, err } - return b.blockchain.StateAt(block.Root()) + return b.blockchain.StateAt(block.Root(), blockNumber) } // CodeAt returns the code associated with a certain account in the blockchain. diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 8efce7dec5..764a3fbadf 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -275,6 +275,9 @@ func applyStateExpiryConfig(ctx *cli.Context, cfg *gethConfig) { if ctx.IsSet(utils.StateExpiryEnableFlag.Name) { cfg.Eth.StateExpiryEnable = ctx.Bool(utils.StateExpiryEnableFlag.Name) } + if ctx.IsSet(utils.StateExpiryFullStateEndpointFlag.Name) { + cfg.Eth.StateExpiryFullStateEndpoint = ctx.String(utils.StateExpiryFullStateEndpointFlag.Name) + } } func deprecated(field string) bool { diff --git a/cmd/geth/main.go b/cmd/geth/main.go index c1434bb755..abbe56a38e 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -218,6 +218,7 @@ var ( stateExpiryFlags = []cli.Flag{ utils.StateExpiryEnableFlag, + utils.StateExpiryFullStateEndpointFlag, } ) diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index d80a39c7bc..32068879e7 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1125,6 +1125,11 @@ var ( Usage: "Enable state expiry, it will mark state's epoch meta and prune un-accessed states later", Category: flags.StateExpiryCategory, } + StateExpiryFullStateEndpointFlag = &cli.StringFlag{ + Name: "state-expiry.remote", + Usage: "set state expiry remote full state rpc endpoint, every expired state will fetch from remote", + Category: flags.StateExpiryCategory, + } ) func init() { diff --git a/core/blockchain.go b/core/blockchain.go index 94c54fc996..32cbf1cd95 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -293,6 +293,10 @@ type BlockChain struct { // monitor doubleSignMonitor *monitor.DoubleSignMonitor + + // state expiry feature + enableStateExpiry bool + fullStateDB ethdb.FullStateDB } // NewBlockChain returns a fully initialised block chain using information @@ -599,6 +603,24 @@ func (bc *BlockChain) cacheBlock(hash common.Hash, block *types.Block) { bc.blockCache.Add(hash, block) } +func (bc *BlockChain) EnableStateExpiry() bool { + return bc.enableStateExpiry +} + +func (bc *BlockChain) FullStateDB() ethdb.FullStateDB { + return bc.fullStateDB +} + +func (bc *BlockChain) InitStateExpiry(endpoint string) error { + rpcServer, err := ethdb.NewFullStateRPCServer(endpoint) + if err != nil { + return err + } + bc.enableStateExpiry = true + bc.fullStateDB = rpcServer + return nil +} + // empty returns an indicator whether the blockchain is empty. // Note, it's a special case that we connect a non-empty ancient // database with an empty node, so that we can plugin the ancient @@ -1015,8 +1037,15 @@ func (bc *BlockChain) SnapSyncCommitHead(hash common.Hash) error { } // StateAtWithSharedPool returns a new mutable state based on a particular point in time with sharedStorage -func (bc *BlockChain) StateAtWithSharedPool(root common.Hash) (*state.StateDB, error) { - return state.NewWithSharedPool(root, bc.stateCache, bc.snaps) +func (bc *BlockChain) StateAtWithSharedPool(root common.Hash, height *big.Int) (*state.StateDB, error) { + stateDB, err := state.NewWithSharedPool(root, bc.stateCache, bc.snaps) + if err != nil { + return nil, err + } + if bc.enableStateExpiry { + stateDB.InitStateExpiry(bc.chainConfig, height, bc.fullStateDB) + } + return stateDB, err } // Reset purges the entire blockchain, restoring it to its genesis state. @@ -2015,6 +2044,9 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) return it.index, err } bc.updateHighestVerifiedHeader(block.Header()) + if bc.enableStateExpiry { + statedb.InitStateExpiry(bc.chainConfig, block.Number(), bc.fullStateDB) + } // Enable prefetching to pull in trie node paths while processing transactions statedb.StartPrefetcher("chain") @@ -2022,9 +2054,11 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) // For diff sync, it may fallback to full sync, so we still do prefetch if len(block.Transactions()) >= prefetchTxNumber { // do Prefetch in a separate goroutine to avoid blocking the critical path - // 1.do state prefetch for snapshot cache throwaway := statedb.CopyDoPrefetch() + if throwaway != nil && bc.enableStateExpiry { + throwaway.InitStateExpiry(bc.chainConfig, block.Number(), bc.fullStateDB) + } go bc.prefetcher.Prefetch(block, throwaway, &bc.vmConfig, interruptCh) // 2.do trie prefetch for MPT trie node cache diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index 802b979a10..1626fe46fa 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -347,12 +347,19 @@ func (bc *BlockChain) ContractCodeWithPrefix(hash common.Hash) ([]byte, error) { // State returns a new mutable state based on the current HEAD block. func (bc *BlockChain) State() (*state.StateDB, error) { - return bc.StateAt(bc.CurrentBlock().Root) + return bc.StateAt(bc.CurrentBlock().Root, bc.CurrentBlock().Number) } // 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, bc.snaps) +func (bc *BlockChain) StateAt(root common.Hash, height *big.Int) (*state.StateDB, error) { + sdb, err := state.New(root, bc.stateCache, bc.snaps) + if err != nil { + return nil, err + } + if bc.enableStateExpiry { + sdb.InitStateExpiry(bc.chainConfig, height, bc.fullStateDB) + } + return sdb, err } // Config retrieves the chain's fork configuration. diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 56a5c7763e..98015411f0 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -4346,7 +4346,7 @@ func TestTransientStorageReset(t *testing.T) { t.Fatalf("failed to insert into chain: %v", err) } // Check the storage - state, err := chain.StateAt(chain.CurrentHeader().Root) + state, err := chain.StateAt(chain.CurrentHeader().Root, chain.CurrentHeader().Number) if err != nil { t.Fatalf("Failed to load state %v", err) } diff --git a/core/state/state_object.go b/core/state/state_object.go index d21007e3d4..c6eca81644 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -20,6 +20,7 @@ import ( "bytes" "fmt" "github.com/ethereum/go-ethereum/core/state/snapshot" + "github.com/ethereum/go-ethereum/trie" "io" "math/big" "sync" @@ -266,30 +267,22 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { // handle state expiry situation if s.db.EnableExpire() { enc, err = s.db.snap.Storage(s.addrHash, crypto.Keccak256Hash(key.Bytes())) + // handle from remoteDB, if got err just setError, just return to revert in consensus version. if err == snapshot.ErrStorageExpired { - // TODO(0xbundler): request from remote node; - // TODO: query from dirty revive trie, got the newest expired info - // TODO: handle from remoteDB, if got err just setError, just return to revert in consensus version . - //_, err = s.getDirtyReviveTrie(db).TryGet(key.Bytes()) - //if enErr, ok := err.(*trie.ExpiredNodeError); ok { - // return common.Hash{}, NewExpiredStateError(s.address, key, enErr).Reason("snap query") - //} - // proof, remoteErr := request() - // if remoteErr != nil { - // s.db.setError(remoteErr) - // return common.hash{} - // } - // TODO: add to revive trie with new epoch, add pending state for snapshot - err = nil + enc, err = s.fetchExpiredFromRemote(nil, key) + if err != nil { + s.db.setError(err) + return common.Hash{} + } } if len(enc) > 0 { var sv snapshot.SnapValue sv, err = snapshot.DecodeValueFromRLPBytes(enc) - if err == nil { + if err != nil { + s.db.setError(err) + } else { value.SetBytes(sv.GetVal()) s.originStorageEpoch[key] = sv.GetEpoch() - } else { - enc = []byte{} } } } else { @@ -321,25 +314,13 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { } // handle state expiry situation if s.db.EnableExpire() { - // TODO(0xbundler): using trie expired error - if err == snapshot.ErrStorageExpired { - // TODO(0xbundler): request from remote node; - // TODO: query from dirty revive trie, got the newest expired info - // TODO: handle from remoteDB, if got err just setError, just return to revert in consensus version . - //_, err = s.getDirtyReviveTrie(db).TryGet(key.Bytes()) - //if enErr, ok := err.(*trie.ExpiredNodeError); ok { - // return common.Hash{}, NewExpiredStateError(s.address, key, enErr).Reason("snap query") - //} - // proof, remoteErr := request() - // if remoteErr != nil { - // s.db.setError(remoteErr) - // } - // TODO: add to revive trie with new epoch - err = nil + if enErr, ok := err.(*trie.ExpiredNodeError); ok { + val, err = s.fetchExpiredFromRemote(enErr.Path, key) } - //val = queried // TODO(0xbundler): add epoch record cache for prevent frequency access epoch update, may implement later - //s.originStorageEpoch[key] = epoch + //if err != nil { + // s.originStorageEpoch[key] = epoch + //} } if err != nil { s.db.setError(err) @@ -830,3 +811,32 @@ func (s *stateObject) queryFromReviveState(reviveState map[string]common.Hash, k val, ok := reviveState[string(khash[:])] return val, ok } + +// fetchExpiredFromRemote request expired state from remote full state node; +func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash) ([]byte, error) { + // if no prefix, query from revive trie, got the newest expired info + if len(prefixKey) == 0 { + tr, err := s.getPendingReviveTrie() + if err != nil { + return nil, err + } + _, err = tr.GetStorage(s.address, key.Bytes()) + if enErr, ok := err.(*trie.ExpiredNodeError); ok { + prefixKey = enErr.Path + } + } + proofs, err := s.db.fullStateDB.GetStorageReviveProof(s.db.originalRoot, s.address, []string{common.Bytes2Hex(prefixKey)}, []string{common.Bytes2Hex(key[:])}) + if err != nil { + return nil, err + } + + var val []byte + for _, proof := range proofs { + _ = proof + // TODO(0xbundler): s.ReviveStorageTrie(proof) + // query key: val + // val = + } + + return val, nil +} diff --git a/core/state/statedb.go b/core/state/statedb.go index 96b4999bb4..c5c33fb3e5 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -20,6 +20,7 @@ package state import ( "errors" "fmt" + "github.com/ethereum/go-ethereum/ethdb" "math/big" "runtime" "sort" @@ -32,7 +33,6 @@ import ( "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/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/params" @@ -143,8 +143,8 @@ type StateDB struct { // state expiry feature enableStateExpiry bool - epoch types.StateEpoch // epoch indicate stateDB start at which block's target epoch - remoteNode interface{} //RemoteFullStateNode //TODO(0xbundler): add interface to fetch expired proof from remote + epoch types.StateEpoch // epoch indicate stateDB start at which block's target epoch + fullStateDB ethdb.FullStateDB //RemoteFullStateNode // Measurements gathered during execution for debugging purposes // MetricsMux should be used in more places, but will affect on performance, so following meteration is not accruate @@ -238,18 +238,17 @@ func (s *StateDB) TransferPrefetcher(prev *StateDB) { s.prefetcherLock.Unlock() } -// SetEpoch it must set in initial, reset later will cause wrong result -func (s *StateDB) SetEpoch(config *params.ChainConfig, height *big.Int) *StateDB { +// InitStateExpiry it must set in initial, reset later will cause wrong result +func (s *StateDB) InitStateExpiry(config *params.ChainConfig, height *big.Int, remote ethdb.FullStateDB) *StateDB { + if config == nil || height == nil || remote == nil { + panic("cannot init state expiry stateDB with nil config/height/remote") + } + s.enableStateExpiry = true + s.fullStateDB = remote s.epoch = types.GetStateEpoch(config, height) return s } -// SetRemoteNode it must set in initial, reset later will cause wrong result -func (s *StateDB) SetRemoteNode(remote interface{}) *StateDB { - s.remoteNode = remote - return s -} - // StartPrefetcher initializes a new trie prefetcher to pull in nodes from the // state trie concurrently while the state is mutated so that when we reach the // commit phase, most of the needed data is already hot. @@ -983,6 +982,11 @@ func (s *StateDB) copyInternal(doPrefetch bool) *StateDB { journal: newJournal(), hasher: crypto.NewKeccakState(), + // state expiry copy + epoch: s.epoch, + enableStateExpiry: s.enableStateExpiry, + fullStateDB: s.fullStateDB, + // In order for the block producer to be able to use and make additions // to the snapshot tree, we need to copy that as well. Otherwise, any // block mined by ourselves will cause gaps in the tree, and force the diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index 71cb2cb53f..a36ed15d3a 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -365,7 +365,7 @@ func (p *BlobPool) Init(gasTip *big.Int, head *types.Header, reserve txpool.Addr return err } } - state, err := p.chain.StateAt(head.Root) + state, err := p.chain.StateAt(head.Root, head.Number) if err != nil { return err } @@ -746,7 +746,7 @@ func (p *BlobPool) Reset(oldHead, newHead *types.Header) { resettimeHist.Update(time.Since(start).Nanoseconds()) }(time.Now()) - statedb, err := p.chain.StateAt(newHead.Root) + statedb, err := p.chain.StateAt(newHead.Root, newHead.Number) if err != nil { log.Error("Failed to reset blobpool state", "err", err) return diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go index f8ddcc0c10..3db5336c48 100644 --- a/core/txpool/blobpool/blobpool_test.go +++ b/core/txpool/blobpool/blobpool_test.go @@ -158,7 +158,7 @@ func (bt *testBlockChain) GetBlock(hash common.Hash, number uint64) *types.Block return nil } -func (bc *testBlockChain) StateAt(common.Hash) (*state.StateDB, error) { +func (bc *testBlockChain) StateAt(common.Hash, *big.Int) (*state.StateDB, error) { return bc.statedb, nil } diff --git a/core/txpool/blobpool/interface.go b/core/txpool/blobpool/interface.go index 6f296a54bd..98fe44d3a4 100644 --- a/core/txpool/blobpool/interface.go +++ b/core/txpool/blobpool/interface.go @@ -21,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" + "math/big" ) // BlockChain defines the minimal set of methods needed to back a blob pool with @@ -40,5 +41,5 @@ type BlockChain interface { GetBlock(hash common.Hash, number uint64) *types.Block // StateAt returns a state database for a given root hash (generally the head). - StateAt(root common.Hash) (*state.StateDB, error) + StateAt(root common.Hash, height *big.Int) (*state.StateDB, error) } diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 2c23f142b1..b8df608f8c 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -125,7 +125,7 @@ type BlockChain interface { GetBlock(hash common.Hash, number uint64) *types.Block // StateAt returns a state database for a given root hash (generally the head). - StateAt(root common.Hash) (*state.StateDB, error) + StateAt(root common.Hash, height *big.Int) (*state.StateDB, error) } // Config are the configuration parameters of the transaction pool. @@ -1470,7 +1470,7 @@ func (pool *LegacyPool) reset(oldHead, newHead *types.Header) { if newHead == nil { newHead = pool.chain.CurrentBlock() // Special case during testing } - statedb, err := pool.chain.StateAt(newHead.Root) + statedb, err := pool.chain.StateAt(newHead.Root, newHead.Number) if err != nil { log.Error("Failed to reset txpool state", "err", err) return diff --git a/core/txpool/legacypool/legacypool_test.go b/core/txpool/legacypool/legacypool_test.go index 05ff64aed1..4c95d50f8c 100644 --- a/core/txpool/legacypool/legacypool_test.go +++ b/core/txpool/legacypool/legacypool_test.go @@ -88,7 +88,7 @@ func (bc *testBlockChain) GetBlock(hash common.Hash, number uint64) *types.Block return types.NewBlock(bc.CurrentBlock(), nil, nil, nil, trie.NewStackTrie(nil)) } -func (bc *testBlockChain) StateAt(common.Hash) (*state.StateDB, error) { +func (bc *testBlockChain) StateAt(common.Hash, *big.Int) (*state.StateDB, error) { return bc.statedb, nil } diff --git a/core/types/revive_state.go b/core/types/revive_state.go new file mode 100644 index 0000000000..27a80a2e1e --- /dev/null +++ b/core/types/revive_state.go @@ -0,0 +1,7 @@ +package types + +type ReviveStorageProof struct { + Key string `json:"key"` + PrefixKey string `json:"prefixKey"` + Proof []string `json:"proof"` +} diff --git a/eth/api_backend.go b/eth/api_backend.go index 3192823148..13a6793a6f 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -204,7 +204,7 @@ func (b *EthAPIBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.B if header == nil { return nil, nil, errors.New("header not found") } - stateDb, err := b.eth.BlockChain().StateAt(header.Root) + stateDb, err := b.eth.BlockChain().StateAt(header.Root, header.Number) return stateDb, header, err } @@ -223,7 +223,7 @@ func (b *EthAPIBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockN if blockNrOrHash.RequireCanonical && b.eth.blockchain.GetCanonicalHash(header.Number.Uint64()) != hash { return nil, nil, errors.New("hash is not currently canonical") } - stateDb, err := b.eth.BlockChain().StateAt(header.Root) + stateDb, err := b.eth.BlockChain().StateAt(header.Root, header.Number) return stateDb, header, err } return nil, nil, errors.New("invalid arguments; neither block nor hash specified") diff --git a/eth/api_debug.go b/eth/api_debug.go index 6afa046787..66b7c6e6f1 100644 --- a/eth/api_debug.go +++ b/eth/api_debug.go @@ -79,7 +79,7 @@ func (api *DebugAPI) DumpBlock(blockNr rpc.BlockNumber) (state.Dump, error) { if header == nil { return state.Dump{}, fmt.Errorf("block #%d not found", blockNr) } - stateDb, err := api.eth.BlockChain().StateAt(header.Root) + stateDb, err := api.eth.BlockChain().StateAt(header.Root, header.Number) if err != nil { return state.Dump{}, err } @@ -164,7 +164,7 @@ func (api *DebugAPI) AccountRange(blockNrOrHash rpc.BlockNumberOrHash, start hex if header == nil { return state.IteratorDump{}, fmt.Errorf("block #%d not found", number) } - stateDb, err = api.eth.BlockChain().StateAt(header.Root) + stateDb, err = api.eth.BlockChain().StateAt(header.Root, header.Number) if err != nil { return state.IteratorDump{}, err } @@ -174,7 +174,7 @@ func (api *DebugAPI) AccountRange(blockNrOrHash rpc.BlockNumberOrHash, start hex if block == nil { return state.IteratorDump{}, fmt.Errorf("block %s not found", hash.Hex()) } - stateDb, err = api.eth.BlockChain().StateAt(block.Root()) + stateDb, err = api.eth.BlockChain().StateAt(block.Root(), block.Number()) if err != nil { return state.IteratorDump{}, err } diff --git a/eth/backend.go b/eth/backend.go index cf593fbb71..6d771adca3 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -242,6 +242,11 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { if err != nil { return nil, err } + if config.StateExpiryEnable { + if err = eth.blockchain.InitStateExpiry(config.StateExpiryFullStateEndpoint); err != nil { + return nil, err + } + } eth.bloomIndexer.Start(eth.blockchain) if config.BlobPool.Datadir != "" { diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 9b0b564884..c2a5c94d3b 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -104,7 +104,8 @@ type Config struct { DisablePeerTxBroadcast bool // state expiry configs - StateExpiryEnable bool + StateExpiryEnable bool + StateExpiryFullStateEndpoint string // This can be set to list of enrtree:// URLs which will be queried for // for nodes to connect to. diff --git a/eth/protocols/eth/handler_test.go b/eth/protocols/eth/handler_test.go index f2f8ee2d2b..d254f96d71 100644 --- a/eth/protocols/eth/handler_test.go +++ b/eth/protocols/eth/handler_test.go @@ -534,10 +534,11 @@ func testGetNodeData(t *testing.T, protocol uint, drop bool) { // Sanity check whether all state matches. accounts := []common.Address{testAddr, acc1Addr, acc2Addr} for i := uint64(0); i <= backend.chain.CurrentBlock().Number.Uint64(); i++ { - root := backend.chain.GetBlockByNumber(i).Root() + block := backend.chain.GetBlockByNumber(i) + root := block.Root() reconstructed, _ := state.New(root, state.NewDatabase(reconstructDB), nil) for j, acc := range accounts { - state, _ := backend.chain.StateAt(root) + state, _ := backend.chain.StateAt(root, block.Number()) bw := state.GetBalance(acc) bh := reconstructed.GetBalance(acc) diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 71a38253ef..18c9fb14aa 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -53,8 +53,8 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u // The state is available in live database, create a reference // on top to prevent garbage collection and return a release // function to deref it. - if statedb, err = eth.blockchain.StateAt(block.Root()); err == nil { - eth.blockchain.TrieDB().Reference(block.Root(), common.Hash{}) + if statedb, err = eth.blockchain.StateAt(block.Root(), block.Hash(), block.Number()); err == nil { + statedb.Database().TrieDB().Reference(block.Root(), common.Hash{}) return statedb, func() { eth.blockchain.TrieDB().Dereference(block.Root()) }, nil @@ -71,6 +71,9 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u // please re-enable it for better performance. database = state.NewDatabaseWithConfig(eth.chainDb, trie.HashDefaults) if statedb, err = state.New(block.Root(), database, nil); err == nil { + if eth.blockchain.EnableStateExpiry() { + statedb.InitStateExpiry(eth.blockchain.Config(), block.Number(), eth.blockchain.FullStateDB()) + } log.Info("Found disk backend for state trie", "root", block.Root(), "number", block.Number()) return statedb, noopReleaser, nil } @@ -98,6 +101,9 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u if !readOnly { statedb, err = state.New(current.Root(), database, nil) if err == nil { + if eth.blockchain.EnableStateExpiry() { + statedb.InitStateExpiry(eth.blockchain.Config(), block.Number(), eth.blockchain.FullStateDB()) + } return statedb, noopReleaser, nil } } @@ -117,6 +123,9 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u statedb, err = state.New(current.Root(), database, nil) if err == nil { + if eth.blockchain.EnableStateExpiry() { + statedb.InitStateExpiry(eth.blockchain.Config(), block.Number(), eth.blockchain.FullStateDB()) + } break } } @@ -166,6 +175,9 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u if err != nil { return nil, nil, fmt.Errorf("state reset after block %d failed: %v", current.NumberU64(), err) } + if eth.blockchain.EnableStateExpiry() { + statedb.InitStateExpiry(eth.blockchain.Config(), block.Number(), eth.blockchain.FullStateDB()) + } // Hold the state reference and also drop the parent state // to prevent accumulating too many nodes in memory. triedb.Reference(root, common.Hash{}) diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go index c665f8c32b..661aab83e7 100644 --- a/eth/tracers/api_test.go +++ b/eth/tracers/api_test.go @@ -141,7 +141,7 @@ func (b *testBackend) teardown() { } func (b *testBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, StateReleaseFunc, error) { - statedb, err := b.chain.StateAt(block.Root()) + statedb, err := b.chain.StateAt(block.Root(), block.Number()) if err != nil { return nil, nil, errStateNotFound } diff --git a/ethclient/gethclient/gethclient.go b/ethclient/gethclient/gethclient.go index 9ded8cb9e2..f24b83d5cd 100644 --- a/ethclient/gethclient/gethclient.go +++ b/ethclient/gethclient/gethclient.go @@ -78,12 +78,6 @@ type StorageResult struct { Proof []string `json:"proof"` } -type ReviveStorageResult struct { - Key string `json:"key"` - PrefixKey string `json:"prefixKey"` - Proof []string `json:"proof"` -} - // GetProof returns the account and storage values of the specified account including the Merkle-proof. // The block number can be nil, in which case the value is taken from the latest known block. func (ec *Client) GetProof(ctx context.Context, account common.Address, keys []string, blockNumber *big.Int) (*AccountResult, error) { @@ -133,9 +127,9 @@ func (ec *Client) GetProof(ctx context.Context, account common.Address, keys []s // GetStorageReviveProof returns the proof for the given keys. Prefix keys can be specified to obtain partial proof for a given key. // Both keys and prefix keys should have the same length. If user wish to obtain full proof for a given key, the corresponding prefix key should be empty string. -func (ec *Client) GetStorageReviveProof(ctx context.Context, account common.Address, keys []string, prefixKeys []string, hash common.Hash) ([]ReviveStorageResult, error) { +func (ec *Client) GetStorageReviveProof(ctx context.Context, account common.Address, keys []string, prefixKeys []string, hash common.Hash) ([]types.ReviveStorageProof, error) { var err error - storageResults := make([]ReviveStorageResult, 0, len(keys)) + storageResults := make([]types.ReviveStorageProof, 0, len(keys)) if len(keys) != len(prefixKeys) { return nil, fmt.Errorf("keys and prefixKeys must be same length") diff --git a/ethdb/fullstatedb.go b/ethdb/fullstatedb.go new file mode 100644 index 0000000000..91af886af4 --- /dev/null +++ b/ethdb/fullstatedb.go @@ -0,0 +1,54 @@ +package ethdb + +import ( + "context" + "errors" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" + "strings" + "time" +) + +// FullStateDB expired state could fetch from it +type FullStateDB interface { + // GetStorageReviveProof fetch target proof according to specific params + GetStorageReviveProof(root common.Hash, account common.Address, prefixKeys, keys []string) ([]types.ReviveStorageProof, error) +} + +type FullStateRPCServer struct { + endpoint string + client *rpc.Client +} + +func NewFullStateRPCServer(endpoint string) (FullStateDB, error) { + if endpoint == "" { + return nil, errors.New("endpoint must be specified") + } + if strings.HasPrefix(endpoint, "rpc:") || strings.HasPrefix(endpoint, "ipc:") { + // Backwards compatibility with geth < 1.5 which required + // these prefixes. + endpoint = endpoint[4:] + } + // TODO(0xbundler): add more opts, like auth? + client, err := rpc.DialOptions(context.Background(), endpoint, nil) + if err != nil { + return nil, err + } + return &FullStateRPCServer{ + endpoint: endpoint, + client: client, + }, nil +} + +func (f *FullStateRPCServer) GetStorageReviveProof(root common.Hash, account common.Address, prefixKeys, keys []string) ([]types.ReviveStorageProof, error) { + // TODO(0xbundler): add timeout in flags? + ctx, cancelFunc := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancelFunc() + proofs := make([]types.ReviveStorageProof, 0, len(keys)) + err := f.client.CallContext(ctx, &proofs, "eth_getStorageReviveProof", account, prefixKeys, keys, root) + if err != nil { + return nil, err + } + return proofs, err +} diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 4b649b8f36..5f9a8b7032 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -669,12 +669,6 @@ type StorageResult struct { Proof []string `json:"proof"` } -type ReviveStorageResult struct { - Key string `json:"key"` - PrefixKey string `json:"prefixKey"` - Proof []string `json:"proof"` -} - // proofList implements ethdb.KeyValueWriter and collects the proofs as // hex-strings for delivery to rpc-caller. type proofList []string @@ -765,7 +759,7 @@ func (s *BlockChainAPI) GetProof(ctx context.Context, address common.Address, st // GetStorageReviveProof returns the proof for the given keys. Prefix keys can be specified to obtain partial proof for a given key. // Both keys and prefix keys should have the same length. If user wish to obtain full proof for a given key, the corresponding prefix key should be empty string. -func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, address common.Address, storageKeys []string, storagePrefixKeys []string, blockNrOrHash rpc.BlockNumberOrHash) ([]ReviveStorageResult, error) { +func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, address common.Address, storageKeys []string, storagePrefixKeys []string, blockNrOrHash rpc.BlockNumberOrHash) ([]types.ReviveStorageProof, error) { if len(storageKeys) != len(storagePrefixKeys) { return nil, errors.New("storageKeys and storagePrefixKeys must be same length") @@ -775,7 +769,7 @@ func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, address commo keys = make([]common.Hash, len(storageKeys)) keyLengths = make([]int, len(storageKeys)) prefixKeys = make([][]byte, len(storagePrefixKeys)) - storageProof = make([]ReviveStorageResult, len(storageKeys)) + storageProof = make([]types.ReviveStorageProof, len(storageKeys)) storageTrie state.Trie ) // Deserialize all keys. This prevents state access on invalid input. @@ -827,7 +821,11 @@ func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, address commo if err := storageTrie.ProvePath(crypto.Keccak256(key.Bytes()), prefixKey, &proof); err != nil { return nil, err } - storageProof[i] = ReviveStorageResult{outputKey, storagePrefixKeys[i], proof} + storageProof[i] = types.ReviveStorageProof{ + Key: outputKey, + PrefixKey: storagePrefixKeys[i], + Proof: proof, + } } return storageProof, nil diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 6d61d6103c..57a5914409 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -454,7 +454,7 @@ func (b testBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.Bloc if header == nil { return nil, nil, errors.New("header not found") } - stateDb, err := b.chain.StateAt(header.Root) + stateDb, err := b.chain.StateAt(header.Root, header.Number) return stateDb, header, err } func (b testBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*state.StateDB, *types.Header, error) { diff --git a/miner/miner_test.go b/miner/miner_test.go index 489bc46a91..20e9969fa5 100644 --- a/miner/miner_test.go +++ b/miner/miner_test.go @@ -85,7 +85,7 @@ func (bc *testBlockChain) GetBlock(hash common.Hash, number uint64) *types.Block return types.NewBlock(bc.CurrentBlock(), nil, nil, nil, trie.NewStackTrie(nil)) } -func (bc *testBlockChain) StateAt(common.Hash) (*state.StateDB, error) { +func (bc *testBlockChain) StateAt(common.Hash, *big.Int) (*state.StateDB, error) { return bc.statedb, nil } diff --git a/miner/worker.go b/miner/worker.go index ffade84a39..326f94e531 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -650,7 +650,7 @@ func (w *worker) makeEnv(parent *types.Header, header *types.Header, coinbase co prevEnv *environment) (*environment, error) { // Retrieve the parent state to execute on top and start a prefetcher for // the miner to speed block sealing up a bit - state, err := w.chain.StateAtWithSharedPool(parent.Root) + state, err := w.chain.StateAtWithSharedPool(parent.Root, header.Number) if err != nil { return nil, err } From 48d0784ae6a3580181835c589d636ecc35435473 Mon Sep 17 00:00:00 2001 From: asyukii Date: Tue, 29 Aug 2023 10:45:12 +0800 Subject: [PATCH 10/65] core/state: implement state object revive storage trie minor change to key remove journal codes --- core/state/database.go | 2 ++ core/state/state_object.go | 55 ++++++++++++++++++++------------------ light/trie.go | 4 +++ trie/secure_trie.go | 4 +++ 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/core/state/database.go b/core/state/database.go index 2810d91058..a0764c843c 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -156,6 +156,8 @@ type Trie interface { Prove(key []byte, proofDb ethdb.KeyValueWriter) error ProvePath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error + + ReviveTrie(key []byte, prefixKeyHex []byte, proofList [][]byte) error } // NewDatabase creates a backing store for state. The returned database is safe for diff --git a/core/state/state_object.go b/core/state/state_object.go index c6eca81644..c4468ec978 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -766,32 +766,35 @@ func (s *stateObject) Nonce() uint64 { return s.data.Nonce } -// ReviveStorageTrie TODO(0xbundler): combine with trie -//func (s *stateObject) ReviveStorageTrie(proofCache trie.MPTProofCache) error { -// dr := s.getDirtyReviveTrie(s.db.db) -// s.db.journal.append(reviveStorageTrieNodeChange{ -// address: &s.address, -// }) -// // revive nub and cache revive state TODO(0xbundler): support proofs merge, revive in nubs -// for _, nub := range dr.ReviveTrie(proofCache.CacheNubs()) { -// kv, err := nub.ResolveKV() -// if err != nil { -// return err -// } -// for k, enc := range kv { -// var value common.Hash -// if len(enc) > 0 { -// _, content, _, err := rlp.Split(enc) -// if err != nil { -// return err -// } -// value.SetBytes(content) -// } -// s.dirtyReviveState[k] = value -// } -// } -// return nil -//} +func (s *stateObject) ReviveStorageTrie(proof types.ReviveStorageProof) error { + dr, err := s.getPendingReviveTrie() + if err != nil { + return err + } + + key := common.Hex2Bytes(proof.Key) + prefixKey := common.Hex2Bytes(proof.PrefixKey) + proofList := make([][]byte, 0, len(proof.Proof)) + + for _, p := range proof.Proof { + proofList = append(proofList, common.Hex2Bytes(p)) + } + + // TODO(asyukii): support proofs merge, revive in nubs + err = dr.ReviveTrie(crypto.Keccak256(key), prefixKey, proofList) + if err != nil { + return fmt.Errorf("revive storage trie failed, err: %v", err) + } + + // Update pending revive state + val, err := dr.GetStorage(s.address, key) // TODO(asyukii): may optimize this, return value when revive trie + if err != nil { + return fmt.Errorf("get storage value failed, err: %v", err) + } + + s.pendingReviveState[proof.Key] = common.BytesToHash(val) + return nil +} // accessState record all access states, now in pendingAccessedStateEpoch without consensus func (s *stateObject) accessState(key common.Hash) { diff --git a/light/trie.go b/light/trie.go index 3c5d2a7665..4a87da2eef 100644 --- a/light/trie.go +++ b/light/trie.go @@ -115,6 +115,10 @@ type odrTrie struct { trie *trie.Trie } +func (t *odrTrie) ReviveTrie(key []byte, prefixKeyHex []byte, proofList [][]byte) error { + panic("not implemented") +} + func (t *odrTrie) GetStorage(_ common.Address, key []byte) ([]byte, error) { key = crypto.Keccak256(key) var enc []byte diff --git a/trie/secure_trie.go b/trie/secure_trie.go index ffe006c1ff..2317f477f8 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -302,3 +302,7 @@ func (t *StateTrie) getSecKeyCache() map[string][]byte { } return t.secKeyCache } + +func (t *StateTrie) ReviveTrie(key []byte, prefixKeyHex []byte, proofList [][]byte) error { + return t.trie.ReviveTrie(key, prefixKeyHex, proofList) +} From f0435dda479f61ca256aed058479faff068cf379 Mon Sep 17 00:00:00 2001 From: asyukii Date: Tue, 29 Aug 2023 21:11:06 +0800 Subject: [PATCH 11/65] core/types: modify metadata type --- core/types/meta.go | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/core/types/meta.go b/core/types/meta.go index c1c42e2774..2e6f4912e3 100644 --- a/core/types/meta.go +++ b/core/types/meta.go @@ -1,23 +1,44 @@ package types import ( + "errors" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" +) + +const ( + MetaNoConsensusType = iota +) + +var ( + ErrMetaNotSupport = errors.New("the meta type not support now") ) +type MetaNoConsensus StateEpoch // Represents the epoch number + type StateMeta interface { - GetVersionNumber() uint8 + GetType() byte Hash() common.Hash + EncodeToRLPBytes() ([]byte, error) +} + +func NewMetaNoConsensus(epoch StateEpoch) StateMeta { + return MetaNoConsensus(epoch) } -type MetaNoConsensus struct { - Version uint8 - Epoch uint16 +func (m MetaNoConsensus) GetType() byte { + return MetaNoConsensusType } -func (m *MetaNoConsensus) GetVersionNumber() uint8 { - return m.Version +func (m MetaNoConsensus) Hash() common.Hash { + return common.Hash{} } -func (m *MetaNoConsensus) Hash() common.Hash { - return rlpHash(m.Epoch) +func (m MetaNoConsensus) EncodeToRLPBytes() ([]byte, error) { + enc, err := rlp.EncodeToBytes(m) + if err != nil { + return nil, err + } + return enc, nil } From b393825715cf9a65f7d551c601792e2d3e83e250 Mon Sep 17 00:00:00 2001 From: asyukii Date: Tue, 29 Aug 2023 21:13:28 +0800 Subject: [PATCH 12/65] core/state, trie: access trie updates with current epoch --- core/state/database.go | 2 + core/state/state_object.go | 4 +- light/trie.go | 4 ++ trie/secure_trie.go | 9 +++++ trie/trie.go | 82 +++++++++++++++++++++++++------------- 5 files changed, 71 insertions(+), 30 deletions(-) diff --git a/core/state/database.go b/core/state/database.go index a0764c843c..31ef1bd75c 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -99,6 +99,8 @@ type Trie interface { // a trie.MissingNodeError is returned. GetStorage(addr common.Address, key []byte) ([]byte, error) + GetStorageAndUpdateEpoch(addr common.Address, key []byte) ([]byte, error) + // GetAccount abstracts an account read from the trie. It retrieves the // account blob from the trie with provided account address and decodes it // with associated decoding algorithm. If the specified account is not in diff --git a/core/state/state_object.go b/core/state/state_object.go index c4468ec978..0afc3628fd 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -308,7 +308,7 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { s.db.setError(err) return common.Hash{} } - val, err := tr.GetStorage(s.address, key.Bytes()) + val, err := tr.GetStorageAndUpdateEpoch(s.address, key.Bytes()) if metrics.EnabledExpensive { s.db.StorageReads += time.Since(start) } @@ -787,7 +787,7 @@ func (s *stateObject) ReviveStorageTrie(proof types.ReviveStorageProof) error { } // Update pending revive state - val, err := dr.GetStorage(s.address, key) // TODO(asyukii): may optimize this, return value when revive trie + val, err := dr.GetStorageAndUpdateEpoch(s.address, key) // TODO(asyukii): may optimize this, return value when revive trie if err != nil { return fmt.Errorf("get storage value failed, err: %v", err) } diff --git a/light/trie.go b/light/trie.go index 4a87da2eef..5ec3ac965b 100644 --- a/light/trie.go +++ b/light/trie.go @@ -133,6 +133,10 @@ func (t *odrTrie) GetStorage(_ common.Address, key []byte) ([]byte, error) { return content, err } +func (t *odrTrie) GetStorageAndUpdateEpoch(_ common.Address, key []byte) ([]byte, error) { + panic("not implemented") +} + func (t *odrTrie) GetAccount(address common.Address) (*types.StateAccount, error) { var ( enc []byte diff --git a/trie/secure_trie.go b/trie/secure_trie.go index 2317f477f8..d4cbce873e 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -94,6 +94,15 @@ func (t *StateTrie) GetStorage(_ common.Address, key []byte) ([]byte, error) { return content, err } +func (t *StateTrie) GetStorageAndUpdateEpoch(addr common.Address, key []byte) ([]byte, error) { + enc, err := t.trie.GetAndUpdateEpoch(t.hashKey(key)) + if err != nil || len(enc) == 0 { + return nil, err + } + _, content, _, err := rlp.Split(enc) + return content, err +} + // GetAccount attempts to retrieve an account with provided account address. // If the specified account is not in the trie, nil will be returned. // If a trie node is not found in the database, a MissingNodeError is returned. diff --git a/trie/trie.go b/trie/trie.go index 166cf2413f..d715544a33 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -56,6 +56,7 @@ type Trie struct { tracer *tracer // fields for state expiry + currentEpoch types.StateEpoch rootEpoch types.StateEpoch enableExpiry bool } @@ -186,7 +187,7 @@ func (t *Trie) Get(key []byte) (value []byte, err error) { } if t.enableExpiry { - value, newroot, didResolve, err = t.getWithEpoch(t.root, keybytesToHex(key), 0, t.getRootEpoch()) + value, newroot, didResolve, err = t.getWithEpoch(t.root, keybytesToHex(key), 0, t.getRootEpoch(), false) } else { value, newroot, didResolve, err = t.get(t.root, keybytesToHex(key), 0) } @@ -196,6 +197,19 @@ func (t *Trie) Get(key []byte) (value []byte, err error) { return value, err } +func (t *Trie) GetAndUpdateEpoch(key []byte) (value []byte, err error) { + if !t.enableExpiry { + return nil, errors.New("expiry is not enabled") + } + + value, newroot, didResolve, err := t.getWithEpoch(t.root, keybytesToHex(key), 0, t.getRootEpoch(), true) + + if err == nil && didResolve { + t.root = newroot + } + return value, err +} + func (t *Trie) get(origNode node, key []byte, pos int) (value []byte, newnode node, didResolve bool, err error) { switch n := (origNode).(type) { case nil: @@ -232,7 +246,7 @@ func (t *Trie) get(origNode node, key []byte, pos int) (value []byte, newnode no } } -func (t *Trie) getWithEpoch(origNode node, key []byte, pos int, epoch types.StateEpoch) (value []byte, newnode node, didResolve bool, err error) { +func (t *Trie) getWithEpoch(origNode node, key []byte, pos int, epoch types.StateEpoch, updateEpoch bool) (value []byte, newnode node, didResolve bool, err error) { if t.epochExpired(origNode, epoch) { return nil, nil, false, NewExpiredNodeError(key[:pos], epoch) } @@ -246,20 +260,28 @@ func (t *Trie) getWithEpoch(origNode node, key []byte, pos int, epoch types.Stat // key not found in trie return nil, n, false, nil } - value, newnode, didResolve, err = t.getWithEpoch(n.Val, key, pos+len(n.Key), epoch) - if err == nil && t.renewNode(epoch, didResolve, true) { + value, newnode, didResolve, err = t.getWithEpoch(n.Val, key, pos+len(n.Key), epoch, updateEpoch) + if err == nil && t.renewNode(epoch, didResolve, updateEpoch) { n = n.copy() n.Val = newnode - n.setEpoch(t.getRootEpoch()) + if updateEpoch { + n.setEpoch(t.currentEpoch) + } + didResolve = true } return value, n, didResolve, err case *fullNode: - value, newnode, didResolve, err = t.getWithEpoch(n.Children[key[pos]], key, pos+1, n.GetChildEpoch(int(key[pos]))) - if err == nil && t.renewNode(epoch, didResolve, true) { + value, newnode, didResolve, err = t.getWithEpoch(n.Children[key[pos]], key, pos+1, n.GetChildEpoch(int(key[pos])), updateEpoch) + if err == nil && t.renewNode(epoch, didResolve, updateEpoch) { n = n.copy() n.Children[key[pos]] = newnode - n.setEpoch(t.getRootEpoch()) - n.UpdateChildEpoch(int(key[pos]), t.getRootEpoch()) + if updateEpoch { + n.setEpoch(t.currentEpoch) + } + if updateEpoch && newnode != nil { + n.UpdateChildEpoch(int(key[pos]), t.currentEpoch) + } + didResolve = true } return value, n, didResolve, err case hashNode: @@ -275,7 +297,7 @@ func (t *Trie) getWithEpoch(origNode node, key []byte, pos int, epoch types.Stat } child.SetEpochMap(epochMap) } - value, newnode, _, err := t.getWithEpoch(child, key, pos, epoch) + value, newnode, _, err := t.getWithEpoch(child, key, pos, epoch, updateEpoch) return value, newnode, true, err default: panic(fmt.Sprintf("%T: invalid node: %v", origNode, origNode)) @@ -543,22 +565,23 @@ func (t *Trie) insertWithEpoch(n node, prefix, key []byte, value node, epoch typ if !t.renewNode(epoch, dirty, true) || err != nil { return false, n, err } - return true, &shortNode{Key: n.Key, Val: nn, flags: t.newFlag(), epoch: epoch}, nil + return true, &shortNode{Key: n.Key, Val: nn, flags: t.newFlag(), epoch: t.currentEpoch}, nil } // Otherwise branch out at the index where they differ. - branch := &fullNode{flags: t.newFlag(), epoch: epoch} + branch := &fullNode{flags: t.newFlag(), epoch: t.currentEpoch} var err error - _, branch.Children[n.Key[matchlen]], err = t.insertWithEpoch(nil, append(prefix, n.Key[:matchlen+1]...), n.Key[matchlen+1:], n.Val, epoch) + _, branch.Children[n.Key[matchlen]], err = t.insertWithEpoch(nil, append(prefix, n.Key[:matchlen+1]...), n.Key[matchlen+1:], n.Val, t.currentEpoch) if err != nil { return false, nil, err } - branch.UpdateChildEpoch(int(n.Key[matchlen]), epoch) + branch.UpdateChildEpoch(int(n.Key[matchlen]), t.currentEpoch) - _, branch.Children[key[matchlen]], err = t.insertWithEpoch(nil, append(prefix, key[:matchlen+1]...), key[matchlen+1:], value, epoch) + _, branch.Children[key[matchlen]], err = t.insertWithEpoch(nil, append(prefix, key[:matchlen+1]...), key[matchlen+1:], value, t.currentEpoch) if err != nil { return false, nil, err } - branch.UpdateChildEpoch(int(key[matchlen]), epoch) + branch.UpdateChildEpoch(int(key[matchlen]), t.currentEpoch) + branch.setEpoch(t.currentEpoch) // Replace this shortNode with the branch if it occurs at index 0. if matchlen == 0 { @@ -570,16 +593,19 @@ func (t *Trie) insertWithEpoch(n node, prefix, key []byte, value node, epoch typ t.tracer.onInsert(append(prefix, key[:matchlen]...)) // Replace it with a short node leading up to the branch. - return true, &shortNode{Key: key[:matchlen], Val: branch, flags: t.newFlag(), epoch: epoch}, nil + return true, &shortNode{Key: key[:matchlen], Val: branch, flags: t.newFlag(), epoch: t.currentEpoch}, nil case *fullNode: dirty, nn, err := t.insertWithEpoch(n.Children[key[0]], append(prefix, key[0]), key[1:], value, n.GetChildEpoch(int(key[0]))) - if !dirty || err != nil { + if !t.renewNode(epoch, dirty, true) || err != nil { return false, n, err } n = n.copy() n.flags = t.newFlag() n.Children[key[0]] = nn + n.setEpoch(t.currentEpoch) + n.UpdateChildEpoch(int(key[0]), t.currentEpoch) + return true, n, nil case nil: @@ -588,7 +614,7 @@ func (t *Trie) insertWithEpoch(n node, prefix, key []byte, value node, epoch typ // since it's always embedded in its parent. t.tracer.onInsert(prefix) - return true, &shortNode{Key: key, Val: value, flags: t.newFlag(), epoch: epoch}, nil + return true, &shortNode{Key: key, Val: value, flags: t.newFlag(), epoch: t.currentEpoch}, nil case hashNode: // We've hit a part of the trie that isn't loaded yet. Load @@ -827,9 +853,9 @@ func (t *Trie) deleteWithEpoch(n node, prefix, key []byte, epoch types.StateEpoc // always creates a new slice) instead of append to // avoid modifying n.Key since it might be shared with // other nodes. - return true, &shortNode{Key: concat(n.Key, child.Key...), Val: child.Val, flags: t.newFlag(), epoch: epoch}, nil + return true, &shortNode{Key: concat(n.Key, child.Key...), Val: child.Val, flags: t.newFlag(), epoch: t.currentEpoch}, nil default: - return true, &shortNode{Key: n.Key, Val: child, flags: t.newFlag(), epoch: epoch}, nil + return true, &shortNode{Key: n.Key, Val: child, flags: t.newFlag(), epoch: t.currentEpoch}, nil } case *fullNode: @@ -888,12 +914,12 @@ func (t *Trie) deleteWithEpoch(n node, prefix, key []byte, epoch types.StateEpoc t.tracer.onDelete(append(prefix, byte(pos))) k := append([]byte{byte(pos)}, cnode.Key...) - return true, &shortNode{Key: k, Val: cnode.Val, flags: t.newFlag()}, nil + return true, &shortNode{Key: k, Val: cnode.Val, flags: t.newFlag(), epoch: t.currentEpoch}, nil } } // Otherwise, n is replaced by a one-nibble short node // containing the child. - return true, &shortNode{Key: []byte{byte(pos)}, Val: n.Children[pos], flags: t.newFlag(), epoch: epoch}, nil + return true, &shortNode{Key: []byte{byte(pos)}, Val: n.Children[pos], flags: t.newFlag(), epoch: t.currentEpoch}, nil } // n still contains at least two values and cannot be reduced. return true, n, nil @@ -1062,7 +1088,7 @@ func (t *Trie) ReviveTrie(key []byte, prefixKeyHex []byte, proofList [][]byte) e key = keybytesToHex(key) // Verify the proof first - revivedNode, revivedHash, err := VerifyPathProof(key, prefixKeyHex, proofList, t.getRootEpoch()) + revivedNode, revivedHash, err := VerifyPathProof(key, prefixKeyHex, proofList, t.currentEpoch) if err != nil { return err } @@ -1116,7 +1142,7 @@ func (t *Trie) revive(n node, key []byte, prefixKeyHex []byte, pos int, revivedN if err == nil && didRevived { n = n.copy() n.Val = newNode - n.setEpoch(t.getRootEpoch()) + n.setEpoch(t.currentEpoch) } return n, didRevived, err case *fullNode: @@ -1126,8 +1152,8 @@ func (t *Trie) revive(n node, key []byte, prefixKeyHex []byte, pos int, revivedN if err == nil && didRevived { n = n.copy() n.Children[key[pos]] = newNode - n.setEpoch(t.getRootEpoch()) - n.UpdateChildEpoch(childIndex, t.getRootEpoch()) + n.setEpoch(t.currentEpoch) + n.UpdateChildEpoch(childIndex, t.currentEpoch) } return n, didRevived, err case hashNode: @@ -1248,5 +1274,5 @@ func (t *Trie) epochExpired(n node, epoch types.StateEpoch) bool { if !t.enableExpiry || n == nil { return false } - return types.EpochExpired(epoch, t.getRootEpoch()) + return types.EpochExpired(epoch, t.currentEpoch) } From 2c7c459c46ed61a3ac76300461d3a2f2aeb7a55f Mon Sep 17 00:00:00 2001 From: asyukii Date: Fri, 25 Aug 2023 19:28:51 +0800 Subject: [PATCH 13/65] feat: add trie prune logic apply comments --- trie/trie.go | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/trie/trie.go b/trie/trie.go index d715544a33..2be3749a8b 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -24,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/trie/trienode" ) @@ -1276,3 +1277,147 @@ func (t *Trie) epochExpired(n node, epoch types.StateEpoch) bool { } return types.EpochExpired(epoch, t.currentEpoch) } + +// PruneExpiredStorageTrie traverses the storage trie and prunes all expired nodes. +func (t *Trie) PruneExpiredStorageTrie(db ethdb.Database) error { + + if !t.enableExpiry { + return nil + } + + if t.owner == (common.Hash{}) { + return fmt.Errorf("cannot prune account trie") + } + + var ( + hasher = newHasher(false) + batch = db.NewBatch() + ) + + defer returnHasherToPool(hasher) + + _, _, err := t.pruneExpiredStorageTrie(t.root, nil, t.getRootEpoch(), false, batch, hasher) + if err != nil { + return err + } + + return nil +} + +func (t *Trie) pruneExpiredStorageTrie(n node, path []byte, curEpoch types.StateEpoch, isExpired bool, batch ethdb.Batch, h *hasher) (node, bool, error) { + + // Upon reaching expired node, it will recursively traverse downwards to all the child nodes + // and collect their hashes. Then, the corresponding key-value pairs will be deleted from the + // database by batches. + if isExpired { + var hashList []common.Hash + cn, err := t.pruneExpired(n, path, h, &hashList) + if err != nil { + return nil, false, err + } + + for _, hash := range hashList { + batch.Delete(hash[:]) + } + + // Handle 1 expired subtree at a time + batch.Write() + batch.Reset() + + return cn, true, nil + } + + switch n := n.(type) { + case *shortNode: + newNode, didExpire, err := t.pruneExpiredStorageTrie(n.Val, append(path, n.Key...), curEpoch, isExpired, batch, h) + if err == nil && didExpire { + n = n.copy() + n.Val = newNode + } + return n, didExpire, err + case *fullNode: + + hasExpired := false + + // Go through every child and recursively delete expired nodes + for i, child := range n.Children { + childExpired, _ := n.ChildExpired(path, i, curEpoch) + newNode, didExpire, err := t.pruneExpiredStorageTrie(child, append(path, byte(i)), curEpoch, childExpired, batch, h) + if err == nil && didExpire { + n = n.copy() + n.Children[byte(i)] = newNode + hasExpired = true + } + } + return n, hasExpired, nil + + case hashNode: + child, err := t.resolveAndTrack(n, path) + if err != nil { + return nil, false, err + } + + // TODO(asyukii): resolve meta + if child, ok := child.(*fullNode); ok { + epochMap, err := t.resolveMeta(child, 0, nil) // TODO(asyukii): handle this + if err != nil { + return nil, false, err + } + child.SetEpochMap(epochMap) + } + + newNode, didExpire, err := t.pruneExpiredStorageTrie(child, path, curEpoch, isExpired, batch, h) + return newNode, didExpire, err + case valueNode: + return n, false, nil + default: + panic(fmt.Sprintf("invalid node type: %T", n)) + } +} + +func (t *Trie) pruneExpired(n node, path []byte, hasher *hasher, hashList *[]common.Hash) (node, error) { + + switch n := n.(type) { + case *shortNode: + cn, err := t.pruneExpired(n.Val, append(path, n.Key...), hasher, hashList) + if err != nil { + return nil, err + } + + n.Val = cn + hn, _ := hasher.hash(n, false) + if hn, ok := hn.(hashNode); ok { + *hashList = append(*hashList, common.BytesToHash(hn)) + return hn, nil + } + + return n, nil + + case *fullNode: + for i, child := range n.Children { + cn, err := t.pruneExpired(child, append(path, byte(i)), hasher, hashList) + if err != nil { + return nil, err + } + n.Children[byte(i)] = cn + } + + hn, _ := hasher.hash(n, false) + if hn, ok := hn.(hashNode); ok { + *hashList = append(*hashList, common.BytesToHash(hn)) + return hn, nil + } + + return n, nil + + case hashNode: + child, err := t.resolveAndTrack(n, path) + if err != nil { + return nil, err + } + newNode, err := t.pruneExpired(child, path, hasher, hashList) + return newNode, err + case valueNode: + return n, nil + } +} From 22fc2ab569556e6628e30979504d1d1911de8747 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Mon, 28 Aug 2023 23:31:29 +0800 Subject: [PATCH 14/65] trie/epochmeta: add trie epoch meta storage; trie/epochmeta: trie integrate epoch meta storage; --- core/blockchain.go | 30 +- core/rawdb/accessors_epoch_meta.go | 54 +++ core/rawdb/schema.go | 10 + core/state/database.go | 5 + core/state/iterator.go | 3 + core/state/snapshot/snapshot_expire.go | 5 - core/state/state_object.go | 87 ++-- core/state/statedb.go | 3 + core/types/meta.go | 16 +- core/types/meta_test.go | 25 ++ eth/backend.go | 7 +- light/trie.go | 4 + trie/database.go | 6 + trie/epochmeta/database.go | 146 +++++++ trie/epochmeta/database_test.go | 133 ++++++ trie/epochmeta/difflayer.go | 568 +++++++++++++++++++++++++ trie/epochmeta/difflayer_test.go | 268 ++++++++++++ trie/proof.go | 3 + trie/proof_test.go | 3 +- trie/secure_trie.go | 4 + trie/trie.go | 110 ++--- trie/trie_reader.go | 40 +- trie/trie_test.go | 3 +- 23 files changed, 1421 insertions(+), 112 deletions(-) create mode 100644 core/rawdb/accessors_epoch_meta.go create mode 100644 core/types/meta_test.go create mode 100644 trie/epochmeta/database.go create mode 100644 trie/epochmeta/database_test.go create mode 100644 trie/epochmeta/difflayer.go create mode 100644 trie/epochmeta/difflayer_test.go diff --git a/core/blockchain.go b/core/blockchain.go index 32cbf1cd95..d3a12e5646 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -158,14 +158,19 @@ type CacheConfig struct { 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 + + // state expiry feature + EnableStateExpiry bool + RemoteEndPoint string } // triedbConfig derives the configures for trie database. func (c *CacheConfig) triedbConfig() *trie.Config { config := &trie.Config{ - Cache: c.TrieCleanLimit, - Preimages: c.Preimages, - NoTries: c.NoTries, + Cache: c.TrieCleanLimit, + Preimages: c.Preimages, + NoTries: c.NoTries, + EnableStateExpiry: c.EnableStateExpiry, } if c.StateScheme == rawdb.HashScheme { config.HashDB = &hashdb.Config{ @@ -318,6 +323,7 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis // Open trie database with provided config triedb := trie.NewDatabase(db, cacheConfig.triedbConfig()) + // Setup the genesis block, commit the provided genesis specification // to database if the genesis block is not present yet, or load the // stored one from database. @@ -542,6 +548,14 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis bc.wg.Add(1) go bc.maintainTxIndex() } + + if cacheConfig.EnableStateExpiry { + bc.enableStateExpiry = true + bc.fullStateDB, err = ethdb.NewFullStateRPCServer(cacheConfig.RemoteEndPoint) + if err != nil { + return nil, err + } + } return bc, nil } @@ -611,16 +625,6 @@ func (bc *BlockChain) FullStateDB() ethdb.FullStateDB { return bc.fullStateDB } -func (bc *BlockChain) InitStateExpiry(endpoint string) error { - rpcServer, err := ethdb.NewFullStateRPCServer(endpoint) - if err != nil { - return err - } - bc.enableStateExpiry = true - bc.fullStateDB = rpcServer - return nil -} - // empty returns an indicator whether the blockchain is empty. // Note, it's a special case that we connect a non-empty ancient // database with an empty node, so that we can plugin the ancient diff --git a/core/rawdb/accessors_epoch_meta.go b/core/rawdb/accessors_epoch_meta.go new file mode 100644 index 0000000000..39c33ad034 --- /dev/null +++ b/core/rawdb/accessors_epoch_meta.go @@ -0,0 +1,54 @@ +package rawdb + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" +) + +func DeleteEpochMetaSnapshotJournal(db ethdb.KeyValueWriter) { + if err := db.Delete(epochMetaSnapshotJournalKey); err != nil { + log.Crit("Failed to remove snapshot journal", "err", err) + } +} + +func ReadEpochMetaSnapshotJournal(db ethdb.KeyValueReader) []byte { + data, _ := db.Get(epochMetaSnapshotJournalKey) + return data +} + +func WriteEpochMetaSnapshotJournal(db ethdb.KeyValueWriter, journal []byte) { + if err := db.Put(epochMetaSnapshotJournalKey, journal); err != nil { + log.Crit("Failed to store snapshot journal", "err", err) + } +} + +func ReadEpochMetaPlainStateMeta(db ethdb.KeyValueReader) []byte { + data, _ := db.Get(epochMetaPlainStateMeta) + return data +} + +func WriteEpochMetaPlainStateMeta(db ethdb.KeyValueWriter, val []byte) error { + return db.Put(epochMetaPlainStateMeta, val) +} + +func ReadEpochMetaPlainState(db ethdb.KeyValueReader, addr common.Hash, path string) []byte { + val, _ := db.Get(epochMetaPlainStateKey(addr, path)) + return val +} + +func WriteEpochMetaPlainState(db ethdb.KeyValueWriter, addr common.Hash, path string, val []byte) error { + return db.Put(epochMetaPlainStateKey(addr, path), val) +} + +func DeleteEpochMetaPlainState(db ethdb.KeyValueWriter, addr common.Hash, path string) error { + return db.Delete(epochMetaPlainStateKey(addr, path)) +} + +func epochMetaPlainStateKey(addr common.Hash, path string) []byte { + key := make([]byte, len(EpochMetaPlainStatePrefix)+len(addr)+len(path)) + copy(key[:], EpochMetaPlainStatePrefix) + copy(key[len(EpochMetaPlainStatePrefix):], addr.Bytes()) + copy(key[len(EpochMetaPlainStatePrefix)+len(addr):], path) + return key +} diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 13a00d795a..779f5570ba 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -103,6 +103,13 @@ var ( // transitionStatusKey tracks the eth2 transition status. transitionStatusKey = []byte("eth2-transition") + // state expiry feature + // epochMetaSnapshotJournalKey tracks the in-memory diff layers across restarts. + epochMetaSnapshotJournalKey = []byte("epochMetaSnapshotJournalKey") + + // epochMetaPlainStateMeta save disk layer meta data + epochMetaPlainStateMeta = []byte("epochMetaPlainStateMeta") + // 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 @@ -144,6 +151,9 @@ var ( CliqueSnapshotPrefix = []byte("clique-") ParliaSnapshotPrefix = []byte("parlia-") + // state expiry feature + EpochMetaPlainStatePrefix = []byte("em") // EpochMetaPlainStatePrefix + addr hash + path -> val + preimageCounter = metrics.NewRegisteredCounter("db/preimage/total", nil) preimageHitCounter = metrics.NewRegisteredCounter("db/preimage/hits", nil) ) diff --git a/core/state/database.go b/core/state/database.go index 31ef1bd75c..ed764f30f8 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -157,9 +157,14 @@ type Trie interface { // with the node that proves the absence of the key. Prove(key []byte, proofDb ethdb.KeyValueWriter) error + // ProvePath generate proof state in trie. ProvePath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error + // ReviveTrie revive expired state from proof. ReviveTrie(key []byte, prefixKeyHex []byte, proofList [][]byte) error + + // SetEpoch set current epoch in trie, it must set in initial period, or it will get error behavior. + SetEpoch(types.StateEpoch) } // NewDatabase creates a backing store for state. The returned database is safe for diff --git a/core/state/iterator.go b/core/state/iterator.go index bb9af08206..7cb212ff15 100644 --- a/core/state/iterator.go +++ b/core/state/iterator.go @@ -127,6 +127,9 @@ func (it *nodeIterator) step() error { if err != nil { return err } + if it.state.enableStateExpiry { + dataTrie.SetEpoch(it.state.epoch) + } it.dataIt, err = dataTrie.NodeIterator(nil) if err != nil { return err diff --git a/core/state/snapshot/snapshot_expire.go b/core/state/snapshot/snapshot_expire.go index 17584e3f56..2b62da24a5 100644 --- a/core/state/snapshot/snapshot_expire.go +++ b/core/state/snapshot/snapshot_expire.go @@ -1,17 +1,12 @@ package snapshot import ( - "errors" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" ) -var ( - ErrStorageExpired = errors.New("access expired account storage kv") -) - // ShrinkExpiredLeaf tool function for snapshot kv prune func ShrinkExpiredLeaf(db ethdb.KeyValueStore, accountHash common.Hash, storageHash common.Hash, epoch types.StateEpoch) error { valWithEpoch := NewValueWithEpoch(epoch, nil) diff --git a/core/state/state_object.go b/core/state/state_object.go index 0afc3628fd..1c1cc53c2c 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -170,6 +170,9 @@ func (s *stateObject) getTrie() (Trie, error) { if err != nil { return nil, err } + if s.db.enableStateExpiry { + tr.SetEpoch(s.db.epoch) + } s.trie = tr } } @@ -259,6 +262,7 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { // If no live objects are available, attempt to use snapshots var ( enc []byte + sv snapshot.SnapValue err error value common.Hash ) @@ -266,24 +270,16 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { start := time.Now() // handle state expiry situation if s.db.EnableExpire() { - enc, err = s.db.snap.Storage(s.addrHash, crypto.Keccak256Hash(key.Bytes())) - // handle from remoteDB, if got err just setError, just return to revert in consensus version. - if err == snapshot.ErrStorageExpired { - enc, err = s.fetchExpiredFromRemote(nil, key) - if err != nil { - s.db.setError(err) - return common.Hash{} - } + var dbError error + sv, err, dbError = s.getExpirySnapStorage(key) + if dbError != nil { + s.db.setError(dbError) + return common.Hash{} } - if len(enc) > 0 { - var sv snapshot.SnapValue - sv, err = snapshot.DecodeValueFromRLPBytes(enc) - if err != nil { - s.db.setError(err) - } else { - value.SetBytes(sv.GetVal()) - s.originStorageEpoch[key] = sv.GetEpoch() - } + // if query success, just set val, otherwise request from trie + if err != nil && sv != nil { + value.SetBytes(sv.GetVal()) + s.originStorageEpoch[key] = sv.GetEpoch() } } else { enc, err = s.db.snap.Storage(s.addrHash, crypto.Keccak256Hash(key.Bytes())) @@ -301,14 +297,14 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { } // If the snapshot is unavailable or reading from it fails, load from the database. - if s.needLoadFromTrie(enc, err) { + if s.needLoadFromTrie(err, sv) { start := time.Now() tr, err := s.getTrie() if err != nil { s.db.setError(err) return common.Hash{} } - val, err := tr.GetStorageAndUpdateEpoch(s.address, key.Bytes()) + val, err := tr.GetStorage(s.address, key.Bytes()) if metrics.EnabledExpensive { s.db.StorageReads += time.Since(start) } @@ -333,7 +329,7 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { } // needLoadFromTrie If not found in snap when EnableExpire(), need check insert duplication from trie. -func (s *stateObject) needLoadFromTrie(enc []byte, err error) bool { +func (s *stateObject) needLoadFromTrie(err error, sv snapshot.SnapValue) bool { if s.db.snap == nil { return true } @@ -341,7 +337,7 @@ func (s *stateObject) needLoadFromTrie(enc []byte, err error) bool { return err != nil } - if err != nil || len(enc) == 0 { + if err != nil || sv == nil { return true } @@ -766,14 +762,17 @@ func (s *stateObject) Nonce() uint64 { return s.data.Nonce } -func (s *stateObject) ReviveStorageTrie(proof types.ReviveStorageProof) error { +func (s *stateObject) ReviveStorageTrie(proof types.ReviveStorageProof, targetPrefix []byte) error { + prefixKey := common.Hex2Bytes(proof.PrefixKey) + if !bytes.Equal(targetPrefix, prefixKey) { + return fmt.Errorf("revive with wrong prefix, target: %#x, actual: %#x", targetPrefix, prefixKey) + } + dr, err := s.getPendingReviveTrie() if err != nil { return err } - key := common.Hex2Bytes(proof.Key) - prefixKey := common.Hex2Bytes(proof.PrefixKey) proofList := make([][]byte, 0, len(proof.Proof)) for _, p := range proof.Proof { @@ -833,13 +832,41 @@ func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash) return nil, err } - var val []byte for _, proof := range proofs { - _ = proof - // TODO(0xbundler): s.ReviveStorageTrie(proof) - // query key: val - // val = + if err := s.ReviveStorageTrie(proof, prefixKey); err != nil { + return nil, err + } + } + val := s.pendingReviveState[key.String()] + return val.Bytes(), nil +} + +func (s *stateObject) getExpirySnapStorage(key common.Hash) (snapshot.SnapValue, error, error) { + enc, err := s.db.snap.Storage(s.addrHash, crypto.Keccak256Hash(key.Bytes())) + if err != nil { + return nil, err, nil + } + var val snapshot.SnapValue + if len(enc) > 0 { + val, err = snapshot.DecodeValueFromRLPBytes(enc) + if err != nil { + return nil, nil, err + } + } + + if val == nil { + return nil, nil, nil + } + + if !types.EpochExpired(val.GetEpoch(), s.db.epoch) { + return val, nil, nil + } + + // handle from remoteDB, if got err just setError, just return to revert in consensus version. + valRaw, err := s.fetchExpiredFromRemote(nil, key) + if err != nil { + return nil, nil, err } - return val, nil + return snapshot.NewValueWithEpoch(s.db.epoch, valRaw), nil, nil } diff --git a/core/state/statedb.go b/core/state/statedb.go index c5c33fb3e5..0d3117ae0d 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -1376,6 +1376,9 @@ func (s *StateDB) deleteStorage(addr common.Address, addrHash common.Hash, root if _, ok := tr.(*trie.EmptyTrie); ok { return false, nil, nil, nil } + if s.enableStateExpiry { + tr.SetEpoch(s.epoch) + } it, err := tr.NodeIterator(nil) if err != nil { return false, nil, nil, fmt.Errorf("failed to open storage iterator, err: %w", err) diff --git a/core/types/meta.go b/core/types/meta.go index 2e6f4912e3..1bd8963909 100644 --- a/core/types/meta.go +++ b/core/types/meta.go @@ -12,7 +12,8 @@ const ( ) var ( - ErrMetaNotSupport = errors.New("the meta type not support now") + ErrMetaNotSupport = errors.New("the meta type not support now") + EmptyMetaNoConsensus = MetaNoConsensus(StateEpoch0) ) type MetaNoConsensus StateEpoch // Represents the epoch number @@ -35,6 +36,10 @@ func (m MetaNoConsensus) Hash() common.Hash { return common.Hash{} } +func (m MetaNoConsensus) Epoch() StateEpoch { + return StateEpoch(m) +} + func (m MetaNoConsensus) EncodeToRLPBytes() ([]byte, error) { enc, err := rlp.EncodeToBytes(m) if err != nil { @@ -42,3 +47,12 @@ func (m MetaNoConsensus) EncodeToRLPBytes() ([]byte, error) { } return enc, nil } + +func DecodeMetaNoConsensusFromRLPBytes(enc []byte) (MetaNoConsensus, error) { + var mc MetaNoConsensus + if err := rlp.DecodeBytes(enc, &mc); err != nil { + return EmptyMetaNoConsensus, err + } + + return mc, nil +} diff --git a/core/types/meta_test.go b/core/types/meta_test.go new file mode 100644 index 0000000000..e03a5b5097 --- /dev/null +++ b/core/types/meta_test.go @@ -0,0 +1,25 @@ +package types + +import ( + "encoding/hex" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestMetaEncodeDecode(t *testing.T) { + tests := []struct { + data MetaNoConsensus + }{ + {data: EmptyMetaNoConsensus}, + {data: MetaNoConsensus(StateEpoch(10000))}, + } + + for _, item := range tests { + enc, err := item.data.EncodeToRLPBytes() + assert.NoError(t, err) + t.Log(hex.EncodeToString(enc)) + mc, err := DecodeMetaNoConsensusFromRLPBytes(enc) + assert.NoError(t, err) + assert.Equal(t, item.data, mc) + } +} diff --git a/eth/backend.go b/eth/backend.go index 6d771adca3..6cca4141fe 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -215,6 +215,8 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { Preimages: config.Preimages, StateHistory: config.StateHistory, StateScheme: config.StateScheme, + EnableStateExpiry: config.StateExpiryEnable, + RemoteEndPoint: config.StateExpiryFullStateEndpoint, } ) bcOps := make([]core.BlockChainOption, 0) @@ -242,11 +244,6 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { if err != nil { return nil, err } - if config.StateExpiryEnable { - if err = eth.blockchain.InitStateExpiry(config.StateExpiryFullStateEndpoint); err != nil { - return nil, err - } - } eth.bloomIndexer.Start(eth.blockchain) if config.BlobPool.Datadir != "" { diff --git a/light/trie.go b/light/trie.go index 5ec3ac965b..779fd80203 100644 --- a/light/trie.go +++ b/light/trie.go @@ -137,6 +137,10 @@ func (t *odrTrie) GetStorageAndUpdateEpoch(_ common.Address, key []byte) ([]byte panic("not implemented") } +func (t *odrTrie) SetEpoch(epoch types.StateEpoch) { + panic("not implemented") +} + func (t *odrTrie) GetAccount(address common.Address) (*types.StateAccount, error) { var ( enc []byte diff --git a/trie/database.go b/trie/database.go index 7bad532dde..ee56027f11 100644 --- a/trie/database.go +++ b/trie/database.go @@ -18,6 +18,8 @@ package trie import ( "errors" + "fmt" + "github.com/ethereum/go-ethereum/trie/epochmeta" "strings" "github.com/ethereum/go-ethereum/common" @@ -40,6 +42,9 @@ type Config struct { // Testing hooks OnCommit func(states *triestate.Set) // Hook invoked when commit is performed + + // state expiry feature + EnableStateExpiry bool } // HashDefaults represents a config for using hash-based scheme with @@ -87,6 +92,7 @@ type Database struct { diskdb ethdb.Database // Persistent database to store the snapshot preimages *preimageStore // The store for caching preimages backend backend // The backend for managing trie nodes + snapTree *epochmeta.SnapshotTree } // prepare initializes the database with provided configs, but the diff --git a/trie/epochmeta/database.go b/trie/epochmeta/database.go new file mode 100644 index 0000000000..122fffb826 --- /dev/null +++ b/trie/epochmeta/database.go @@ -0,0 +1,146 @@ +package epochmeta + +import ( + "bytes" + "errors" + "fmt" + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/log" + + "github.com/ethereum/go-ethereum/rlp" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +const ( + AccountMetadataPath = "m" +) + +type FullNodeEpochMeta struct { + EpochMap [16]types.StateEpoch +} + +func NewFullNodeEpochMeta(epochMap [16]types.StateEpoch) FullNodeEpochMeta { + return FullNodeEpochMeta{EpochMap: epochMap} +} +func (n *FullNodeEpochMeta) Encode(w rlp.EncoderBuffer) { + offset := w.List() + for _, e := range n.EpochMap { + w.WriteUint64(uint64(e)) + } + w.ListEnd(offset) +} + +func DecodeFullNodeEpochMeta(enc []byte) (*FullNodeEpochMeta, error) { + var n FullNodeEpochMeta + + if err := rlp.DecodeBytes(enc, &n.EpochMap); err != nil { + return nil, err + } + + return &n, nil +} + +type Storage interface { + Get(addr common.Hash, path string) ([]byte, error) + Delete(addr common.Hash, path string) error + Put(addr common.Hash, path string, val []byte) error + Commit(number *big.Int, blockRoot common.Hash) error +} + +type StorageRW struct { + snap snapshot + tree *SnapshotTree + dirties map[common.Hash]map[string][]byte + + stale bool + lock sync.RWMutex +} + +// NewEpochMetaDatabase first find snap by blockRoot, if got nil, try using number to instance a read only storage +func NewEpochMetaDatabase(tree *SnapshotTree, number *big.Int, blockRoot common.Hash) (Storage, error) { + snap := tree.Snapshot(blockRoot) + if snap == nil { + // try using default snap + if snap = tree.Snapshot(types.EmptyRootHash); snap == nil { + return nil, fmt.Errorf("cannot find target epoch layer %#x", blockRoot) + } + log.Debug("NewEpochMetaDatabase use default database", "number", number, "root", blockRoot) + } + return &StorageRW{ + snap: snap, + tree: tree, + dirties: make(map[common.Hash]map[string][]byte), + }, nil +} + +func (s *StorageRW) Get(addr common.Hash, path string) ([]byte, error) { + s.lock.RLock() + defer s.lock.RUnlock() + sub, exist := s.dirties[addr] + if exist { + if val, ok := sub[path]; ok { + return val, nil + } + } + + return s.snap.EpochMeta(addr, path) +} + +func (s *StorageRW) Delete(addr common.Hash, path string) error { + s.lock.RLock() + defer s.lock.RUnlock() + if s.stale { + return errors.New("storage has staled") + } + _, ok := s.dirties[addr] + if !ok { + s.dirties[addr] = make(map[string][]byte) + } + + s.dirties[addr][path] = nil + return nil +} + +func (s *StorageRW) Put(addr common.Hash, path string, val []byte) error { + prev, err := s.Get(addr, path) + if err != nil { + return err + } + if bytes.Equal(prev, val) { + return nil + } + + s.lock.RLock() + defer s.lock.RUnlock() + if s.stale { + return errors.New("storage has staled") + } + + _, ok := s.dirties[addr] + if !ok { + s.dirties[addr] = make(map[string][]byte) + } + s.dirties[addr][path] = val + return nil +} + +// Commit if you commit to an unknown parent, like deeper than 128 layers, will get error +func (s *StorageRW) Commit(number *big.Int, blockRoot common.Hash) error { + s.lock.Lock() + defer s.lock.Unlock() + if s.stale { + return errors.New("storage has staled") + } + + s.stale = true + err := s.tree.Update(s.snap.Root(), number, blockRoot, s.dirties) + if err != nil { + return err + } + + return s.tree.Cap(blockRoot) +} diff --git a/trie/epochmeta/database_test.go b/trie/epochmeta/database_test.go new file mode 100644 index 0000000000..b1154cac69 --- /dev/null +++ b/trie/epochmeta/database_test.go @@ -0,0 +1,133 @@ +package epochmeta + +import ( + "bytes" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/core/types" + + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/rlp" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethdb/memorydb" + "github.com/stretchr/testify/assert" +) + +func TestEpochMetaRW_CRUD(t *testing.T) { + diskdb := memorydb.New() + tree, err := NewEpochMetaSnapTree(diskdb) + assert.NoError(t, err) + storageDB, err := NewEpochMetaDatabase(tree, common.Big1, blockRoot1) + assert.NoError(t, err) + + err = storageDB.Put(contract1, "hello", []byte("world")) + assert.NoError(t, err) + err = storageDB.Put(contract1, "hello", []byte("world")) + assert.NoError(t, err) + val, err := storageDB.Get(contract1, "hello") + assert.NoError(t, err) + assert.Equal(t, []byte("world"), val) + err = storageDB.Delete(contract1, "hello") + assert.NoError(t, err) + val, err = storageDB.Get(contract1, "hello") + assert.NoError(t, err) + assert.Equal(t, []byte(nil), val) +} + +func TestEpochMetaRO_Get(t *testing.T) { + diskdb := memorydb.New() + makeDiskLayer(diskdb, common.Big1, blockRoot1, contract1, []string{"k1", "v1"}) + + tree, err := NewEpochMetaSnapTree(diskdb) + assert.NoError(t, err) + storageRO, err := NewEpochMetaDatabase(tree, common.Big1, blockRoot1) + assert.NoError(t, err) + + err = storageRO.Put(contract1, "hello", []byte("world")) + assert.NoError(t, err) + err = storageRO.Delete(contract1, "hello") + assert.NoError(t, err) + err = storageRO.Commit(common.Big2, blockRoot2) + assert.NoError(t, err) + + val, err := storageRO.Get(contract1, "hello") + assert.NoError(t, err) + assert.Equal(t, []byte(nil), val) + val, err = storageRO.Get(contract1, "k1") + assert.NoError(t, err) + assert.Equal(t, []byte("v1"), val) +} + +func makeDiskLayer(diskdb *memorydb.Database, number *big.Int, root common.Hash, addr common.Hash, kv []string) { + if len(kv)%2 != 0 { + panic("wrong kv") + } + meta := epochMetaPlainMeta{ + BlockNumber: number, + BlockRoot: root, + } + enc, _ := rlp.EncodeToBytes(&meta) + rawdb.WriteEpochMetaPlainStateMeta(diskdb, enc) + + for i := 0; i < len(kv); i += 2 { + rawdb.WriteEpochMetaPlainState(diskdb, addr, kv[i], []byte(kv[i+1])) + } +} + +func TestEpochMetaRW_Commit(t *testing.T) { + diskdb := memorydb.New() + tree, err := NewEpochMetaSnapTree(diskdb) + assert.NoError(t, err) + storageDB, err := NewEpochMetaDatabase(tree, common.Big1, blockRoot1) + assert.NoError(t, err) + + err = storageDB.Put(contract1, "hello", []byte("world")) + assert.NoError(t, err) + + err = storageDB.Commit(common.Big1, blockRoot1) + assert.NoError(t, err) + + storageDB, err = NewEpochMetaDatabase(tree, common.Big1, blockRoot1) + assert.NoError(t, err) + val, err := storageDB.Get(contract1, "hello") + assert.NoError(t, err) + assert.Equal(t, []byte("world"), val) +} + +func TestShadowBranchNode_encodeDecode(t *testing.T) { + dt := []struct { + n FullNodeEpochMeta + }{ + { + n: FullNodeEpochMeta{ + EpochMap: [16]types.StateEpoch{}, + }, + }, + { + n: FullNodeEpochMeta{ + EpochMap: [16]types.StateEpoch{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + }, + }, + { + n: FullNodeEpochMeta{ + EpochMap: [16]types.StateEpoch{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + }, + }, + { + n: FullNodeEpochMeta{ + EpochMap: [16]types.StateEpoch{}, + }, + }, + } + for _, item := range dt { + buf := rlp.NewEncoderBuffer(bytes.NewBuffer([]byte{})) + item.n.Encode(buf) + enc := buf.ToBytes() + + rn, err := DecodeFullNodeEpochMeta(enc) + assert.NoError(t, err) + assert.Equal(t, &item.n, rn) + } +} diff --git a/trie/epochmeta/difflayer.go b/trie/epochmeta/difflayer.go new file mode 100644 index 0000000000..c519e3a73f --- /dev/null +++ b/trie/epochmeta/difflayer.go @@ -0,0 +1,568 @@ +package epochmeta + +import ( + "bytes" + "errors" + "fmt" + "github.com/ethereum/go-ethereum/core/types" + "io" + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/log" + + lru "github.com/hashicorp/golang-lru" + + "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" +) + +const ( + // MaxEpochMetaDiffDepth default is 128 layers + MaxEpochMetaDiffDepth = 128 + journalVersion uint64 = 1 + defaultDiskLayerCacheSize = 100000 +) + +// snapshot record diff layer and disk layer of shadow nodes, support mini reorg +type snapshot interface { + // Root block state root + Root() common.Hash + + // EpochMeta query shadow node from db, got RLP format + EpochMeta(addrHash common.Hash, path string) ([]byte, error) + + // Parent parent snap + Parent() snapshot + + // Update create a new diff layer from here + Update(blockNumber *big.Int, blockRoot common.Hash, nodeSet map[common.Hash]map[string][]byte) (snapshot, error) + + // Journal commit self as a journal to buffer + Journal(buffer *bytes.Buffer) (common.Hash, error) +} + +// SnapshotTree maintain all diff layers support reorg, will flush to db when MaxEpochMetaDiffDepth reach +// every layer response to a block state change set, there no flatten layers operation. +type SnapshotTree struct { + diskdb ethdb.KeyValueStore + + // diffLayers + diskLayer, disk layer, always not nil + layers map[common.Hash]snapshot + children map[common.Hash][]common.Hash + + lock sync.RWMutex +} + +func NewEpochMetaSnapTree(diskdb ethdb.KeyValueStore) (*SnapshotTree, error) { + diskLayer, err := loadDiskLayer(diskdb) + if err != nil { + return nil, err + } + layers, children, err := loadDiffLayers(diskdb, diskLayer) + if err != nil { + return nil, err + } + + layers[diskLayer.blockRoot] = diskLayer + // check if continuously after disk layer + if len(layers) > 1 && len(children[diskLayer.blockRoot]) == 0 { + return nil, errors.New("cannot found any diff layers link to disk layer") + } + return &SnapshotTree{ + diskdb: diskdb, + layers: layers, + children: children, + }, nil +} + +// Cap keep tree depth not greater MaxEpochMetaDiffDepth, all forks parent to disk layer will delete +func (s *SnapshotTree) Cap(blockRoot common.Hash) error { + snap := s.Snapshot(blockRoot) + if snap == nil { + return errors.New("snapshot missing") + } + nextDiff, ok := snap.(*diffLayer) + if !ok { + return nil + } + for i := 0; i < MaxEpochMetaDiffDepth-1; i++ { + nextDiff, ok = nextDiff.Parent().(*diffLayer) + // if depth less MaxEpochMetaDiffDepth, just return + if !ok { + return nil + } + } + + flatten := make([]snapshot, 0) + parent := nextDiff.Parent() + for parent != nil { + flatten = append(flatten, parent) + parent = parent.Parent() + } + if len(flatten) <= 1 { + return nil + } + + last, ok := flatten[len(flatten)-1].(*epochMetaDiskLayer) + if !ok { + return errors.New("the diff layers not link to disk layer") + } + + s.lock.Lock() + defer s.lock.Unlock() + newDiskLayer, err := s.flattenDiffs2Disk(flatten[:len(flatten)-1], last) + if err != nil { + return err + } + + // clear forks, but keep latest disk forks + for i := len(flatten) - 1; i > 0; i-- { + var childRoot common.Hash + if i > 0 { + childRoot = flatten[i-1].Root() + } else { + childRoot = nextDiff.Root() + } + root := flatten[i].Root() + s.removeSubLayers(s.children[root], &childRoot) + delete(s.layers, root) + delete(s.children, root) + } + + // reset newDiskLayer and children's parent + s.layers[newDiskLayer.Root()] = newDiskLayer + for _, child := range s.children[newDiskLayer.Root()] { + if diff, exist := s.layers[child].(*diffLayer); exist { + diff.setParent(newDiskLayer) + } + } + return nil +} + +func (s *SnapshotTree) Update(parentRoot common.Hash, blockNumber *big.Int, blockRoot common.Hash, nodeSet map[common.Hash]map[string][]byte) error { + // if there are no changes, just skip + if blockRoot == parentRoot { + return nil + } + + // Generate a new snapshot on top of the parent + parent := s.Snapshot(parentRoot) + if parent == nil { + // just point to fake disk layers + parent = s.Snapshot(types.EmptyRootHash) + if parent == nil { + return errors.New("cannot find any suitable parent") + } + parentRoot = parent.Root() + } + snap, err := parent.Update(blockNumber, blockRoot, nodeSet) + if err != nil { + return err + } + + s.lock.Lock() + defer s.lock.Unlock() + + s.layers[blockRoot] = snap + s.children[parentRoot] = append(s.children[parentRoot], blockRoot) + return nil +} + +func (s *SnapshotTree) Snapshot(blockRoot common.Hash) snapshot { + s.lock.RLock() + defer s.lock.RUnlock() + return s.layers[blockRoot] +} + +func (s *SnapshotTree) DB() ethdb.KeyValueStore { + s.lock.RLock() + defer s.lock.RUnlock() + return s.diskdb +} + +func (s *SnapshotTree) Journal() error { + s.lock.Lock() + defer s.lock.Unlock() + + // Firstly write out the metadata of journal + journal := new(bytes.Buffer) + if err := rlp.Encode(journal, journalVersion); err != nil { + return err + } + for _, snap := range s.layers { + if _, err := snap.Journal(journal); err != nil { + return err + } + } + rawdb.WriteEpochMetaSnapshotJournal(s.diskdb, journal.Bytes()) + return nil +} + +func (s *SnapshotTree) removeSubLayers(layers []common.Hash, skip *common.Hash) { + for _, layer := range layers { + if skip != nil && layer == *skip { + continue + } + s.removeSubLayers(s.children[layer], nil) + delete(s.layers, layer) + delete(s.children, layer) + } +} + +// flattenDiffs2Disk delete all flatten and push them to db +func (s *SnapshotTree) flattenDiffs2Disk(flatten []snapshot, diskLayer *epochMetaDiskLayer) (*epochMetaDiskLayer, error) { + var err error + for i := len(flatten) - 1; i >= 0; i-- { + diskLayer, err = diskLayer.PushDiff(flatten[i].(*diffLayer)) + if err != nil { + return nil, err + } + } + + return diskLayer, nil +} + +// loadDiskLayer load from db, could be nil when none in db +func loadDiskLayer(db ethdb.KeyValueStore) (*epochMetaDiskLayer, error) { + val := rawdb.ReadEpochMetaPlainStateMeta(db) + // if there is no disk layer, will construct a fake disk layer + if len(val) == 0 { + diskLayer, err := newEpochMetaDiskLayer(db, common.Big0, types.EmptyRootHash) + if err != nil { + return nil, err + } + return diskLayer, nil + } + var meta epochMetaPlainMeta + if err := rlp.DecodeBytes(val, &meta); err != nil { + return nil, err + } + + layer, err := newEpochMetaDiskLayer(db, meta.BlockNumber, meta.BlockRoot) + if err != nil { + return nil, err + } + return layer, nil +} + +func loadDiffLayers(db ethdb.KeyValueStore, diskLayer *epochMetaDiskLayer) (map[common.Hash]snapshot, map[common.Hash][]common.Hash, error) { + layers := make(map[common.Hash]snapshot) + children := make(map[common.Hash][]common.Hash) + + journal := rawdb.ReadEpochMetaSnapshotJournal(db) + if len(journal) == 0 { + return layers, children, nil + } + r := rlp.NewStream(bytes.NewReader(journal), 0) + // Firstly, resolve the first element as the journal version + version, err := r.Uint64() + if err != nil { + return nil, nil, errors.New("failed to resolve journal version") + } + if version != journalVersion { + return nil, nil, errors.New("wrong journal version") + } + + parents := make(map[common.Hash]common.Hash) + for { + var ( + parent common.Hash + number big.Int + root common.Hash + js []journalEpochMeta + ) + // Read the next diff journal entry + if err := r.Decode(&number); err != nil { + // The first read may fail with EOF, marking the end of the journal + if errors.Is(err, io.EOF) { + break + } + return nil, nil, fmt.Errorf("load diff number: %v", err) + } + if err := r.Decode(&parent); err != nil { + return nil, nil, fmt.Errorf("load diff parent: %v", err) + } + // Read the next diff journal entry + if err := r.Decode(&root); err != nil { + return nil, nil, fmt.Errorf("load diff root: %v", err) + } + if err := r.Decode(&js); err != nil { + return nil, nil, fmt.Errorf("load diff storage: %v", err) + } + + nodeSet := make(map[common.Hash]map[string][]byte) + for _, entry := range js { + nodes := make(map[string][]byte) + for i, key := range entry.Keys { + if len(entry.Vals[i]) > 0 { // RLP loses nil-ness, but `[]byte{}` is not a valid item, so reinterpret that + nodes[key] = entry.Vals[i] + } else { + nodes[key] = nil + } + } + nodeSet[entry.Hash] = nodes + } + + parents[root] = parent + layers[root] = newEpochMetaDiffLayer(&number, root, nil, nodeSet) + } + + for t, s := range layers { + parent := parents[t] + children[parent] = append(children[parent], t) + if p, ok := layers[parent]; ok { + s.(*diffLayer).parent = p + } else if diskLayer != nil && parent == diskLayer.Root() { + s.(*diffLayer).parent = diskLayer + } else { + return nil, nil, errors.New("cannot find it's parent") + } + } + return layers, children, nil +} + +type diffLayer struct { + blockNumber *big.Int + blockRoot common.Hash + parent snapshot + nodeSet map[common.Hash]map[string][]byte + lock sync.RWMutex +} + +func newEpochMetaDiffLayer(blockNumber *big.Int, blockRoot common.Hash, parent snapshot, nodeSet map[common.Hash]map[string][]byte) *diffLayer { + return &diffLayer{ + blockNumber: blockNumber, + blockRoot: blockRoot, + parent: parent, + nodeSet: nodeSet, + } +} + +func (s *diffLayer) Root() common.Hash { + s.lock.RLock() + defer s.lock.RUnlock() + return s.blockRoot +} + +func (s *diffLayer) EpochMeta(addrHash common.Hash, path string) ([]byte, error) { + s.lock.RLock() + defer s.lock.RUnlock() + cm, exist := s.nodeSet[addrHash] + if exist { + if ret, ok := cm[path]; ok { + return ret, nil + } + } + + return s.parent.EpochMeta(addrHash, path) +} + +func (s *diffLayer) Parent() snapshot { + s.lock.RLock() + defer s.lock.RUnlock() + return s.parent +} + +// Update append new diff layer onto current, nodeChgRecord when val is []byte{}, it delete the kv +func (s *diffLayer) Update(blockNumber *big.Int, blockRoot common.Hash, nodeSet map[common.Hash]map[string][]byte) (snapshot, error) { + s.lock.RLock() + if s.blockNumber.Cmp(blockNumber) >= 0 { + return nil, errors.New("update a unordered diff layer") + } + s.lock.RUnlock() + return newEpochMetaDiffLayer(blockNumber, blockRoot, s, nodeSet), nil +} + +func (s *diffLayer) Journal(buffer *bytes.Buffer) (common.Hash, error) { + s.lock.RLock() + defer s.lock.RUnlock() + + if err := rlp.Encode(buffer, s.blockNumber); err != nil { + return common.Hash{}, err + } + + if s.parent != nil { + if err := rlp.Encode(buffer, s.parent.Root()); err != nil { + return common.Hash{}, err + } + } else { + if err := rlp.Encode(buffer, types.EmptyRootHash); err != nil { + return common.Hash{}, err + } + } + + if err := rlp.Encode(buffer, s.blockRoot); err != nil { + return common.Hash{}, err + } + storage := make([]journalEpochMeta, 0, len(s.nodeSet)) + for hash, nodes := range s.nodeSet { + keys := make([]string, 0, len(nodes)) + vals := make([][]byte, 0, len(nodes)) + for key, val := range nodes { + keys = append(keys, key) + vals = append(vals, val) + } + storage = append(storage, journalEpochMeta{Hash: hash, Keys: keys, Vals: vals}) + } + if err := rlp.Encode(buffer, storage); err != nil { + return common.Hash{}, err + } + return s.blockRoot, nil +} + +func (s *diffLayer) setParent(parent snapshot) { + s.lock.Lock() + defer s.lock.Unlock() + s.parent = parent +} + +func (s *diffLayer) getNodeSet() map[common.Hash]map[string][]byte { + s.lock.Lock() + defer s.lock.Unlock() + return s.nodeSet +} + +type journalEpochMeta struct { + Hash common.Hash + Keys []string + Vals [][]byte +} + +type epochMetaPlainMeta struct { + BlockNumber *big.Int + BlockRoot common.Hash +} + +type epochMetaDiskLayer struct { + diskdb ethdb.KeyValueStore + blockNumber *big.Int + blockRoot common.Hash + cache *lru.Cache + lock sync.RWMutex +} + +func newEpochMetaDiskLayer(diskdb ethdb.KeyValueStore, blockNumber *big.Int, blockRoot common.Hash) (*epochMetaDiskLayer, error) { + cache, err := lru.New(defaultDiskLayerCacheSize) + if err != nil { + return nil, err + } + return &epochMetaDiskLayer{ + diskdb: diskdb, + blockNumber: blockNumber, + blockRoot: blockRoot, + cache: cache, + }, nil +} + +func (s *epochMetaDiskLayer) Root() common.Hash { + s.lock.RLock() + defer s.lock.RUnlock() + return s.blockRoot +} + +func (s *epochMetaDiskLayer) EpochMeta(addr common.Hash, path string) ([]byte, error) { + s.lock.RLock() + defer s.lock.RUnlock() + + cacheKey := cacheKey(addr, path) + cached, exist := s.cache.Get(cacheKey) + if exist { + return cached.([]byte), nil + } + + val := rawdb.ReadEpochMetaPlainState(s.diskdb, addr, path) + s.cache.Add(cacheKey, val) + return val, nil +} + +func (s *epochMetaDiskLayer) Parent() snapshot { + return nil +} + +func (s *epochMetaDiskLayer) Update(blockNumber *big.Int, blockRoot common.Hash, nodeSet map[common.Hash]map[string][]byte) (snapshot, error) { + s.lock.RLock() + if s.blockNumber.Cmp(blockNumber) >= 0 { + return nil, errors.New("update a unordered diff layer") + } + s.lock.RUnlock() + return newEpochMetaDiffLayer(blockNumber, blockRoot, s, nodeSet), nil +} + +func (s *epochMetaDiskLayer) Journal(buffer *bytes.Buffer) (common.Hash, error) { + return common.Hash{}, nil +} + +func (s *epochMetaDiskLayer) PushDiff(diff *diffLayer) (*epochMetaDiskLayer, error) { + s.lock.Lock() + defer s.lock.Unlock() + + number := diff.blockNumber + if s.blockNumber.Cmp(number) >= 0 { + return nil, errors.New("push a lower block to disk") + } + batch := s.diskdb.NewBatch() + nodeSet := diff.getNodeSet() + if err := s.writeHistory(number, batch, diff.getNodeSet()); err != nil { + return nil, err + } + + // update meta + meta := epochMetaPlainMeta{ + BlockNumber: number, + BlockRoot: diff.blockRoot, + } + enc, err := rlp.EncodeToBytes(meta) + if err != nil { + return nil, err + } + if err = rawdb.WriteEpochMetaPlainStateMeta(batch, enc); err != nil { + return nil, err + } + + if err = batch.Write(); err != nil { + return nil, err + } + diskLayer := &epochMetaDiskLayer{ + diskdb: s.diskdb, + blockNumber: number, + blockRoot: diff.blockRoot, + cache: s.cache, + } + + // reuse cache + for addr, nodes := range nodeSet { + for path, val := range nodes { + diskLayer.cache.Add(cacheKey(addr, path), val) + } + } + return diskLayer, nil +} + +func (s *epochMetaDiskLayer) writeHistory(number *big.Int, batch ethdb.Batch, nodeSet map[common.Hash]map[string][]byte) error { + for addr, subSet := range nodeSet { + for path, val := range subSet { + // refresh plain state + if len(val) == 0 { + if err := rawdb.DeleteEpochMetaPlainState(batch, addr, path); err != nil { + return err + } + } else { + if err := rawdb.WriteEpochMetaPlainState(batch, addr, path, val); err != nil { + return err + } + } + } + } + log.Info("shadow node history pruned, only keep plainState", "number", number, "count", len(nodeSet)) + return nil +} + +func cacheKey(addr common.Hash, path string) string { + key := make([]byte, len(addr)+len(path)) + copy(key[:], addr.Bytes()) + copy(key[len(addr):], path) + return string(key) +} diff --git a/trie/epochmeta/difflayer_test.go b/trie/epochmeta/difflayer_test.go new file mode 100644 index 0000000000..52c134272f --- /dev/null +++ b/trie/epochmeta/difflayer_test.go @@ -0,0 +1,268 @@ +package epochmeta + +import ( + "github.com/ethereum/go-ethereum/core/types" + "math/big" + "strconv" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethdb/memorydb" + "github.com/stretchr/testify/assert" +) + +const hashLen = len(common.Hash{}) + +var ( + blockRoot0 = makeHash("b0") + blockRoot1 = makeHash("b1") + blockRoot2 = makeHash("b2") + blockRoot3 = makeHash("b3") + storageRoot0 = makeHash("s0") + storageRoot1 = makeHash("s1") + storageRoot2 = makeHash("s2") + storageRoot3 = makeHash("s3") + contract1 = makeHash("c1") + contract2 = makeHash("c2") + contract3 = makeHash("c3") +) + +func TestEpochMetaDiffLayer_whenGenesis(t *testing.T) { + diskdb := memorydb.New() + // create empty tree + tree, err := NewEpochMetaSnapTree(diskdb) + assert.NoError(t, err) + snap := tree.Snapshot(blockRoot0) + assert.Nil(t, snap) + snap = tree.Snapshot(blockRoot1) + assert.Nil(t, snap) + err = tree.Update(blockRoot0, common.Big1, blockRoot1, makeNodeSet(contract1, []string{"hello", "world"})) + assert.NoError(t, err) + err = tree.Update(blockRoot1, common.Big2, blockRoot2, makeNodeSet(contract1, []string{"hello2", "world2"})) + assert.NoError(t, err) + err = tree.Cap(blockRoot1) + assert.NoError(t, err) + err = tree.Journal() + assert.NoError(t, err) + + // reload + tree, err = NewEpochMetaSnapTree(diskdb) + assert.NoError(t, err) + diskLayer := tree.Snapshot(types.EmptyRootHash) + assert.NotNil(t, diskLayer) + snap = tree.Snapshot(blockRoot0) + assert.Nil(t, snap) + snap1 := tree.Snapshot(blockRoot1) + n, err := snap1.EpochMeta(contract1, "hello") + assert.NoError(t, err) + assert.Equal(t, []byte("world"), n) + assert.Equal(t, diskLayer, snap1.Parent()) + assert.Equal(t, blockRoot1, snap1.Root()) + + // read from child + snap2 := tree.Snapshot(blockRoot2) + assert.Equal(t, snap1, snap2.Parent()) + assert.Equal(t, blockRoot2, snap2.Root()) + n, err = snap2.EpochMeta(contract1, "hello") + assert.NoError(t, err) + assert.Equal(t, []byte("world"), n) + n, err = snap2.EpochMeta(contract1, "hello2") + assert.NoError(t, err) + assert.Equal(t, []byte("world2"), n) +} + +func TestEpochMetaDiffLayer_crud(t *testing.T) { + diskdb := memorydb.New() + // create empty tree + tree, err := NewEpochMetaSnapTree(diskdb) + assert.NoError(t, err) + set1 := makeNodeSet(contract1, []string{"hello", "world", "h1", "w1"}) + appendNodeSet(set1, contract3, []string{"h3", "w3"}) + err = tree.Update(blockRoot0, common.Big1, blockRoot1, set1) + assert.NoError(t, err) + set2 := makeNodeSet(contract1, []string{"hello", "", "h1", ""}) + appendNodeSet(set2, contract2, []string{"hello", "", "h2", "w2"}) + err = tree.Update(blockRoot1, common.Big2, blockRoot2, set2) + assert.NoError(t, err) + snap := tree.Snapshot(blockRoot1) + assert.NotNil(t, snap) + val, err := snap.EpochMeta(contract1, "hello") + assert.NoError(t, err) + assert.Equal(t, []byte("world"), val) + val, err = snap.EpochMeta(contract1, "h1") + assert.NoError(t, err) + assert.Equal(t, []byte("w1"), val) + val, err = snap.EpochMeta(contract3, "h3") + assert.NoError(t, err) + assert.Equal(t, []byte("w3"), val) + + snap = tree.Snapshot(blockRoot2) + assert.NotNil(t, snap) + val, err = snap.EpochMeta(contract1, "hello") + assert.NoError(t, err) + assert.Equal(t, []byte{}, val) + val, err = snap.EpochMeta(contract1, "h1") + assert.NoError(t, err) + assert.Equal(t, []byte{}, val) + val, err = snap.EpochMeta(contract2, "hello") + assert.NoError(t, err) + assert.Equal(t, []byte{}, val) + val, err = snap.EpochMeta(contract2, "h2") + assert.NoError(t, err) + assert.Equal(t, []byte("w2"), val) + val, err = snap.EpochMeta(contract3, "h3") + assert.NoError(t, err) + assert.Equal(t, []byte("w3"), val) +} + +func TestEpochMetaDiffLayer_capDiffLayers(t *testing.T) { + diskdb := memorydb.New() + // create empty tree + tree, err := NewEpochMetaSnapTree(diskdb) + assert.NoError(t, err) + + // push 200 diff layers + count := 1 + for i := 0; i < 200; i++ { + ns := strconv.Itoa(count) + root := makeHash("b" + ns) + parent := makeHash("b" + strconv.Itoa(count-1)) + number := new(big.Int).SetUint64(uint64(count)) + err = tree.Update(parent, number, + root, makeNodeSet(contract1, []string{"hello" + ns, "world" + ns})) + assert.NoError(t, err) + + // add 10 forks + for j := 0; j < 10; j++ { + fs := strconv.Itoa(j) + err = tree.Update(parent, number, + makeHash("b"+ns+"f"+fs), makeNodeSet(contract1, []string{"hello" + ns + "f" + fs, "world" + ns + "f" + fs})) + assert.NoError(t, err) + } + + err = tree.Cap(root) + assert.NoError(t, err) + count++ + } + assert.Equal(t, 1409, len(tree.layers)) + + // push 100 diff layers, and cap + for i := 0; i < 100; i++ { + ns := strconv.Itoa(count) + parent := makeHash("b" + strconv.Itoa(count-1)) + root := makeHash("b" + ns) + number := new(big.Int).SetUint64(uint64(count)) + err = tree.Update(parent, number, root, + makeNodeSet(contract1, []string{"hello" + ns, "world" + ns})) + assert.NoError(t, err) + + // add 20 forks + for j := 0; j < 10; j++ { + fs := strconv.Itoa(j) + err = tree.Update(parent, number, + makeHash("b"+ns+"f"+fs), makeNodeSet(contract1, []string{"hello" + ns + "f" + fs, "world" + ns + "f" + fs})) + assert.NoError(t, err) + } + for j := 0; j < 10; j++ { + fs := strconv.Itoa(j) + err = tree.Update(makeHash("b"+strconv.Itoa(count-1)+"f"+fs), number, + makeHash("b"+ns+"f"+fs), makeNodeSet(contract1, []string{"hello" + ns + "f" + fs, "world" + ns + "f" + fs})) + assert.NoError(t, err) + } + count++ + } + lastRoot := makeHash("b" + strconv.Itoa(count-1)) + err = tree.Cap(lastRoot) + assert.NoError(t, err) + assert.Equal(t, 1409, len(tree.layers)) + + // push 100 diff layers, and cap + for i := 0; i < 129; i++ { + ns := strconv.Itoa(count) + parent := makeHash("b" + strconv.Itoa(count-1)) + root := makeHash("b" + ns) + number := new(big.Int).SetUint64(uint64(count)) + err = tree.Update(parent, number, root, + makeNodeSet(contract1, []string{"hello" + ns, "world" + ns})) + assert.NoError(t, err) + + count++ + } + lastRoot = makeHash("b" + strconv.Itoa(count-1)) + err = tree.Cap(lastRoot) + assert.NoError(t, err) + + assert.Equal(t, 129, len(tree.layers)) + assert.Equal(t, 128, len(tree.children)) + for parent, children := range tree.children { + if tree.layers[parent] == nil { + t.Log(tree.layers[parent]) + } + assert.NotNil(t, tree.layers[parent]) + for _, child := range children { + if tree.layers[child] == nil { + t.Log(tree.layers[child]) + } + assert.NotNil(t, tree.layers[child]) + } + } + + snap := tree.Snapshot(lastRoot) + assert.NotNil(t, snap) + for i := 1; i < count; i++ { + ns := strconv.Itoa(i) + n, err := snap.EpochMeta(contract1, "hello"+ns) + assert.NoError(t, err) + assert.Equal(t, []byte("world"+ns), n) + } + + // store + err = tree.Journal() + assert.NoError(t, err) +} + +func makeHash(s string) common.Hash { + var ret common.Hash + if len(s) >= 32 { + copy(ret[:], []byte(s)[:hashLen]) + return ret + } + for i := 0; i < hashLen; i++ { + ret[i] = '0' + } + copy(ret[hashLen-len(s):hashLen], s) + return ret +} + +func makeNodeSet(addr common.Hash, kvs []string) map[common.Hash]map[string][]byte { + if len(kvs)%2 != 0 { + panic("makeNodeSet: wrong params") + } + ret := make(map[common.Hash]map[string][]byte) + ret[addr] = make(map[string][]byte) + for i := 0; i < len(kvs); i += 2 { + if len(kvs) == 0 { + ret[addr][kvs[i]] = nil + continue + } + ret[addr][kvs[i]] = []byte(kvs[i+1]) + } + + return ret +} + +func appendNodeSet(ret map[common.Hash]map[string][]byte, addr common.Hash, kvs []string) { + if len(kvs)%2 != 0 { + panic("makeNodeSet: wrong params") + } + if _, ok := ret[addr]; !ok { + ret[addr] = make(map[string][]byte) + } + for i := 0; i < len(kvs); i += 2 { + if len(kvs) == 0 { + ret[addr][kvs[i]] = nil + continue + } + ret[addr][kvs[i]] = []byte(kvs[i+1]) + } +} diff --git a/trie/proof.go b/trie/proof.go index e4268f60c4..9bd62d309e 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -156,6 +156,9 @@ func (t *Trie) traverseNodes(tn node, key []byte, nodes *[]node, epoch types.Sta // clean cache or the database, they are all in their own // copy and safe to use unsafe decoder. tn = mustDecodeNodeUnsafe(n, blob) + if err = t.resolveEpochMeta(tn, epoch, prefix); err != nil { + return nil, err + } default: panic(fmt.Sprintf("%T: invalid node: %v", tn, tn)) } diff --git a/trie/proof_test.go b/trie/proof_test.go index f10a38d931..913237bc49 100644 --- a/trie/proof_test.go +++ b/trie/proof_test.go @@ -1109,7 +1109,8 @@ func nonRandomTrie(n int) (*Trie, map[string]*kv) { func nonRandomTrieWithExpiry(n int) (*Trie, map[string]*kv) { db := NewDatabase(rawdb.NewMemoryDatabase()) - trie := NewEmptyWithExpiry(db, 10) + trie := NewEmpty(db) + trie.rootEpoch = 10 vals := make(map[string]*kv) max := uint64(0xffffffffffffffff) for i := uint64(0); i < uint64(n); i++ { diff --git a/trie/secure_trie.go b/trie/secure_trie.go index d4cbce873e..2098c463ac 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -254,6 +254,10 @@ func (t *StateTrie) Hash() common.Hash { return t.trie.Hash() } +func (t *StateTrie) SetEpoch(epoch types.StateEpoch) { + t.trie.currentEpoch = epoch +} + // Copy returns a copy of StateTrie. func (t *StateTrie) Copy() *StateTrie { return &StateTrie{ diff --git a/trie/trie.go b/trie/trie.go index 2be3749a8b..663399f08b 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -21,6 +21,7 @@ import ( "bytes" "errors" "fmt" + "github.com/ethereum/go-ethereum/trie/epochmeta" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -104,43 +105,26 @@ func New(id *ID, db *Database) (*Trie, error) { } trie.root = rootnode } + + // resolve root epoch + if db.snapTree != nil { + meta, err := reader.accountMeta() + if err != nil { + return nil, err + } + trie.rootEpoch = meta.Epoch() + trie.enableExpiry = true + } return trie, nil } // NewEmpty is a shortcut to create empty tree. It's mostly used in tests. func NewEmpty(db *Database) *Trie { tr, _ := New(TrieID(types.EmptyRootHash), db) - return tr -} - -func NewEmptyWithExpiry(db *Database, rootEpoch types.StateEpoch) *Trie { - tr, _ := New(TrieID(types.EmptyRootHash), db) - tr.enableExpiry = true - tr.rootEpoch = rootEpoch - return tr -} - -// TODO (asyukii): handle meta storage later -func NewWithExpiry(id *ID, db *Database, rootEpoch types.StateEpoch) (*Trie, error) { - reader, err := newTrieReader(id.StateRoot, id.Owner, db) - if err != nil { - return nil, err - } - trie := &Trie{ - owner: id.Owner, - reader: reader, - tracer: newTracer(), - rootEpoch: rootEpoch, - enableExpiry: true, - } - if id.Root != (common.Hash{}) && id.Root != types.EmptyRootHash { - rootnode, err := trie.resolveAndTrack(id.Root[:], nil) - if err != nil { - return nil, err - } - trie.root = rootnode + if db.snapTree != nil { + tr.enableExpiry = true } - return trie, nil + return tr } // MustNodeIterator is a wrapper of NodeIterator and will omit any encountered @@ -292,11 +276,9 @@ func (t *Trie) getWithEpoch(origNode node, key []byte, pos int, epoch types.Stat } if child, ok := child.(*fullNode); ok { - epochMap, err := t.resolveMeta(child, epoch, key[:pos]) - if err != nil { + if err = t.resolveEpochMeta(child, epoch, key[:pos]); err != nil { return nil, n, true, err } - child.SetEpochMap(epochMap) } value, newnode, _, err := t.getWithEpoch(child, key, pos, epoch, updateEpoch) return value, newnode, true, err @@ -626,13 +608,10 @@ func (t *Trie) insertWithEpoch(n node, prefix, key []byte, value node, epoch typ return false, nil, err } - // TODO(asyukii): if resolved node is a full node, then resolve epochMap as well if child, ok := rn.(*fullNode); ok { - epochMap, err := t.resolveMeta(child, epoch, prefix) - if err != nil { + if err = t.resolveEpochMeta(child, epoch, prefix); err != nil { return false, nil, err } - child.SetEpochMap(epochMap) } dirty, nn, err := t.insertWithEpoch(rn, prefix, key, value, epoch) @@ -941,11 +920,9 @@ func (t *Trie) deleteWithEpoch(n node, prefix, key []byte, epoch types.StateEpoc } if child, ok := rn.(*fullNode); ok { - epochMap, err := t.resolveMeta(child, epoch, prefix) - if err != nil { + if err = t.resolveEpochMeta(child, epoch, prefix); err != nil { return false, nil, err } - child.SetEpochMap(epochMap) } dirty, nn, err := t.deleteWithEpoch(rn, prefix, key, epoch) @@ -987,10 +964,39 @@ func (t *Trie) resolveAndTrack(n hashNode, prefix []byte) (node, error) { return mustDecodeNode(n, blob), nil } -// TODO(asyukii): implement resolve full node's epoch map. -func (t *Trie) resolveMeta(n node, epoch types.StateEpoch, prefix []byte) ([16]types.StateEpoch, error) { +// resolveEpochMeta resolve full node's epoch map. +func (t *Trie) resolveEpochMeta(n node, epoch types.StateEpoch, prefix []byte) error { + if !t.enableExpiry { + return nil + } // 1. Check if the node is a full node - panic("implement me!") + switch n := n.(type) { + case *shortNode: + n.setEpoch(epoch) + return nil + case *fullNode: + n.setEpoch(epoch) + blob, err := t.reader.epochMeta(prefix) + if err != nil { + return err + } + if len(blob) == 0 { + // set default epoch map + n.EpochMap = [16]types.StateEpoch{} + } else { + meta, decErr := epochmeta.DecodeFullNodeEpochMeta(blob) + if decErr != nil { + return decErr + } + n.EpochMap = meta.EpochMap + } + return nil + case valueNode, hashNode, nil: + // just skip + return nil + default: + return errors.New("resolveShadowNode unsupported node type") + } } // Hash returns the root hash of the trie. It does not write to the @@ -1164,11 +1170,9 @@ func (t *Trie) revive(n node, key []byte, prefixKeyHex []byte, pos int, revivedN } if child, ok := child.(*fullNode); ok { - epochMap, err := t.resolveMeta(child, epoch, key[:pos]) - if err != nil { + if err = t.resolveEpochMeta(child, epoch, key[:pos]); err != nil { return nil, false, err } - child.SetEpochMap(epochMap) } newNode, _, err := t.revive(child, key, prefixKeyHex, pos, revivedNode, revivedHash, epoch, isExpired) @@ -1256,7 +1260,7 @@ func (t *Trie) getRootEpoch() types.StateEpoch { // renewNode check if renew node, according to trie node epoch and childDirty, // childDirty or updateEpoch need copy for prevent reuse trie cache func (t *Trie) renewNode(epoch types.StateEpoch, childDirty bool, updateEpoch bool) bool { - // when !updateEpoch, it same as !t.withShadowNodes + // when !updateEpoch, it same as !t.withEpochMeta if !t.enableExpiry || !updateEpoch { return childDirty } @@ -1356,14 +1360,8 @@ func (t *Trie) pruneExpiredStorageTrie(n node, path []byte, curEpoch types.State if err != nil { return nil, false, err } - - // TODO(asyukii): resolve meta - if child, ok := child.(*fullNode); ok { - epochMap, err := t.resolveMeta(child, 0, nil) // TODO(asyukii): handle this - if err != nil { - return nil, false, err - } - child.SetEpochMap(epochMap) + if err = t.resolveEpochMeta(child, curEpoch, path); err != nil { + return nil, false, err } newNode, didExpire, err := t.pruneExpiredStorageTrie(child, path, curEpoch, isExpired, batch, h) @@ -1419,5 +1417,7 @@ func (t *Trie) pruneExpired(n node, path []byte, hasher *hasher, hashList *[]com return newNode, err case valueNode: return n, nil + default: + panic(fmt.Sprintf("invalid node type: %T", n)) } } diff --git a/trie/trie_reader.go b/trie/trie_reader.go index 4215964559..c3182c67c1 100644 --- a/trie/trie_reader.go +++ b/trie/trie_reader.go @@ -17,10 +17,14 @@ package trie import ( + "errors" + "fmt" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/trie/epochmeta" "github.com/ethereum/go-ethereum/trie/triestate" + "math/big" ) // Reader wraps the Node method of a backing trie store. @@ -41,6 +45,7 @@ type Reader interface { type trieReader struct { owner common.Hash reader Reader + emdb epochmeta.Storage banned map[string]struct{} // Marker to prevent node from being accessed, for tests } @@ -56,7 +61,14 @@ func newTrieReader(stateRoot, owner common.Hash, db *Database) (*trieReader, err if err != nil { return nil, &MissingNodeError{Owner: owner, NodeHash: stateRoot, err: err} } - return &trieReader{owner: owner, reader: reader}, nil + tr := trieReader{owner: owner, reader: reader} + if db.snapTree != nil { + tr.emdb, err = epochmeta.NewEpochMetaDatabase(db.snapTree, new(big.Int), stateRoot) + if err != nil { + return nil, err + } + } + return &tr, nil } // newEmptyReader initializes the pure in-memory reader. All read operations @@ -99,3 +111,29 @@ func (l *trieLoader) OpenTrie(root common.Hash) (triestate.Trie, error) { func (l *trieLoader) OpenStorageTrie(stateRoot common.Hash, addrHash, root common.Hash) (triestate.Trie, error) { return New(StorageTrieID(stateRoot, addrHash, root), l.db) } + +// epochMeta resolve from epoch meta storage +func (r *trieReader) epochMeta(path []byte) ([]byte, error) { + if r.emdb == nil { + return nil, fmt.Errorf("cannot resolve epochmeta without db, path: %#x", path) + } + + blob, err := r.emdb.Get(r.owner, string(path)) + if err != nil || len(blob) == 0 { + return nil, fmt.Errorf("resolve epoch meta err, path: %#x, err: %v", path, err) + } + return blob, nil +} + +// accountMeta resolve account metadata +func (r *trieReader) accountMeta() (types.MetaNoConsensus, error) { + if r.emdb == nil { + return types.EmptyMetaNoConsensus, errors.New("cannot resolve epoch meta without db for account") + } + + blob, err := r.emdb.Get(r.owner, epochmeta.AccountMetadataPath) + if err != nil || len(blob) == 0 { + return types.EmptyMetaNoConsensus, fmt.Errorf("resolve epoch meta err for account, err: %v", err) + } + return types.DecodeMetaNoConsensusFromRLPBytes(blob) +} diff --git a/trie/trie_test.go b/trie/trie_test.go index 6364499608..6e3cde6d0f 100644 --- a/trie/trie_test.go +++ b/trie/trie_test.go @@ -1069,7 +1069,8 @@ func TestReviveCustom(t *testing.T) { func createCustomTrie(data map[string]string, epoch types.StateEpoch) *Trie { db := NewDatabase(rawdb.NewMemoryDatabase()) - trie := NewEmptyWithExpiry(db, epoch) + trie := NewEmpty(db) + trie.rootEpoch = epoch for k, v := range data { trie.MustUpdate([]byte(k), []byte(v)) } From 19fd26c48cc56ae68c9886ea757eb587b7a08244 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Tue, 29 Aug 2023 22:27:21 +0800 Subject: [PATCH 15/65] trie/committer: support epoch meta commit; --- trie/committer.go | 23 +++++++++----- trie/database.go | 22 ++++++++++++-- trie/epochmeta/database.go | 13 ++++---- trie/epochmeta/database_test.go | 10 +++---- trie/tracer.go | 53 +++++++++++++++++++++++++-------- trie/trie.go | 15 +++++++++- trie/trienode/node.go | 37 ++++++++++++++++++----- 7 files changed, 133 insertions(+), 40 deletions(-) diff --git a/trie/committer.go b/trie/committer.go index 4b222f9710..c028cfc151 100644 --- a/trie/committer.go +++ b/trie/committer.go @@ -18,6 +18,7 @@ package trie import ( "fmt" + "github.com/ethereum/go-ethereum/trie/epochmeta" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/trie/trienode" @@ -27,17 +28,19 @@ import ( // capture all dirty nodes during the commit process and keep them cached in // insertion order. type committer struct { - nodes *trienode.NodeSet - tracer *tracer - collectLeaf bool + nodes *trienode.NodeSet + tracer *tracer + collectLeaf bool + enableStateExpiry bool } // newCommitter creates a new committer or picks one from the pool. -func newCommitter(nodeset *trienode.NodeSet, tracer *tracer, collectLeaf bool) *committer { +func newCommitter(nodeset *trienode.NodeSet, tracer *tracer, collectLeaf bool, enableStateExpiry bool) *committer { return &committer{ - nodes: nodeset, - tracer: tracer, - collectLeaf: collectLeaf, + nodes: nodeset, + tracer: tracer, + collectLeaf: collectLeaf, + enableStateExpiry: enableStateExpiry, } } @@ -140,6 +143,12 @@ func (c *committer) store(path []byte, n node) node { // Collect the dirty node to nodeset for return. nhash := common.BytesToHash(hash) c.nodes.AddNode(path, trienode.New(nhash, nodeToBytes(n))) + if c.enableStateExpiry { + switch n := n.(type) { + case *fullNode: + c.nodes.AddBranchNodeEpochMeta(path, epochmeta.NewBranchNodeEpochMeta(n.EpochMap)) + } + } // Collect the corresponding leaf node if it's required. We don't check // full node since it's impossible to store value in fullNode. The key diff --git a/trie/database.go b/trie/database.go index ee56027f11..a12b140457 100644 --- a/trie/database.go +++ b/trie/database.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "github.com/ethereum/go-ethereum/trie/epochmeta" + "math/big" "strings" "github.com/ethereum/go-ethereum/common" @@ -198,7 +199,16 @@ func (db *Database) Update(root common.Hash, parent common.Hash, block uint64, n if db.preimages != nil { db.preimages.commit(false) } - return db.backend.Update(root, parent, block, nodes, states) + if err := db.backend.Update(root, parent, block, nodes, states); err != nil { + return err + } + if db.snapTree != nil { + err := db.snapTree.Update(parent, new(big.Int).SetUint64(block), root, nodes.FlattenEpochMeta()) + if err != nil { + return err + } + } + return nil } // Commit iterates over all the children of a particular node, writes them out @@ -208,7 +218,15 @@ func (db *Database) Commit(root common.Hash, report bool) error { if db.preimages != nil { db.preimages.commit(true) } - return db.backend.Commit(root, report) + if err := db.backend.Commit(root, report); err != nil { + return err + } + if db.snapTree != nil { + if err := db.snapTree.Cap(root); err != nil { + return err + } + } + return nil } // Size returns the storage size of dirty trie nodes in front of the persistent diff --git a/trie/epochmeta/database.go b/trie/epochmeta/database.go index 122fffb826..1fbd581f00 100644 --- a/trie/epochmeta/database.go +++ b/trie/epochmeta/database.go @@ -19,14 +19,15 @@ const ( AccountMetadataPath = "m" ) -type FullNodeEpochMeta struct { +type BranchNodeEpochMeta struct { EpochMap [16]types.StateEpoch } -func NewFullNodeEpochMeta(epochMap [16]types.StateEpoch) FullNodeEpochMeta { - return FullNodeEpochMeta{EpochMap: epochMap} +func NewBranchNodeEpochMeta(epochMap [16]types.StateEpoch) *BranchNodeEpochMeta { + return &BranchNodeEpochMeta{EpochMap: epochMap} } -func (n *FullNodeEpochMeta) Encode(w rlp.EncoderBuffer) { + +func (n *BranchNodeEpochMeta) Encode(w rlp.EncoderBuffer) { offset := w.List() for _, e := range n.EpochMap { w.WriteUint64(uint64(e)) @@ -34,8 +35,8 @@ func (n *FullNodeEpochMeta) Encode(w rlp.EncoderBuffer) { w.ListEnd(offset) } -func DecodeFullNodeEpochMeta(enc []byte) (*FullNodeEpochMeta, error) { - var n FullNodeEpochMeta +func DecodeFullNodeEpochMeta(enc []byte) (*BranchNodeEpochMeta, error) { + var n BranchNodeEpochMeta if err := rlp.DecodeBytes(enc, &n.EpochMap); err != nil { return nil, err diff --git a/trie/epochmeta/database_test.go b/trie/epochmeta/database_test.go index b1154cac69..e7ba2d5867 100644 --- a/trie/epochmeta/database_test.go +++ b/trie/epochmeta/database_test.go @@ -98,25 +98,25 @@ func TestEpochMetaRW_Commit(t *testing.T) { func TestShadowBranchNode_encodeDecode(t *testing.T) { dt := []struct { - n FullNodeEpochMeta + n BranchNodeEpochMeta }{ { - n: FullNodeEpochMeta{ + n: BranchNodeEpochMeta{ EpochMap: [16]types.StateEpoch{}, }, }, { - n: FullNodeEpochMeta{ + n: BranchNodeEpochMeta{ EpochMap: [16]types.StateEpoch{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, }, }, { - n: FullNodeEpochMeta{ + n: BranchNodeEpochMeta{ EpochMap: [16]types.StateEpoch{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, }, }, { - n: FullNodeEpochMeta{ + n: BranchNodeEpochMeta{ EpochMap: [16]types.StateEpoch{}, }, }, diff --git a/trie/tracer.go b/trie/tracer.go index 5786af4d3e..993869db52 100644 --- a/trie/tracer.go +++ b/trie/tracer.go @@ -40,17 +40,19 @@ import ( // Note tracer is not thread-safe, callers should be responsible for handling // the concurrency issues by themselves. type tracer struct { - inserts map[string]struct{} - deletes map[string]struct{} - accessList map[string][]byte + inserts map[string]struct{} + deletes map[string]struct{} + deleteBranchNodes map[string]struct{} // record for epoch meta + accessList map[string][]byte } // newTracer initializes the tracer for capturing trie changes. func newTracer() *tracer { return &tracer{ - inserts: make(map[string]struct{}), - deletes: make(map[string]struct{}), - accessList: make(map[string][]byte), + inserts: make(map[string]struct{}), + deletes: make(map[string]struct{}), + deleteBranchNodes: make(map[string]struct{}), + accessList: make(map[string][]byte), } } @@ -72,6 +74,13 @@ func (t *tracer) onInsert(path []byte) { t.inserts[string(path)] = struct{}{} } +// onExpandToBranchNode tracks the newly inserted trie branch node. +func (t *tracer) onExpandToBranchNode(path []byte) { + if _, present := t.deleteBranchNodes[string(path)]; present { + delete(t.deleteBranchNodes, string(path)) + } +} + // onDelete tracks the newly deleted trie node. If it's already // in the addition set, then just wipe it from the addition set // as it's untouched. @@ -83,19 +92,26 @@ func (t *tracer) onDelete(path []byte) { t.deletes[string(path)] = struct{}{} } +// onDeleteBranchNode tracks the newly deleted trie branch node. +func (t *tracer) onDeleteBranchNode(path []byte) { + t.deleteBranchNodes[string(path)] = struct{}{} +} + // reset clears the content tracked by tracer. func (t *tracer) reset() { t.inserts = make(map[string]struct{}) t.deletes = make(map[string]struct{}) + t.deleteBranchNodes = make(map[string]struct{}) t.accessList = make(map[string][]byte) } // copy returns a deep copied tracer instance. func (t *tracer) copy() *tracer { var ( - inserts = make(map[string]struct{}) - deletes = make(map[string]struct{}) - accessList = make(map[string][]byte) + inserts = make(map[string]struct{}) + deletes = make(map[string]struct{}) + deleteBranchNodes = make(map[string]struct{}) + accessList = make(map[string][]byte) ) for path := range t.inserts { inserts[path] = struct{}{} @@ -103,13 +119,17 @@ func (t *tracer) copy() *tracer { for path := range t.deletes { deletes[path] = struct{}{} } + for path := range t.deleteBranchNodes { + deleteBranchNodes[path] = struct{}{} + } for path, blob := range t.accessList { accessList[path] = common.CopyBytes(blob) } return &tracer{ - inserts: inserts, - deletes: deletes, - accessList: accessList, + inserts: inserts, + deletes: deletes, + deleteBranchNodes: deleteBranchNodes, + accessList: accessList, } } @@ -128,3 +148,12 @@ func (t *tracer) deletedNodes() []string { } return paths } + +// deletedBranchNodes returns a list of branch node paths which are deleted from the trie. +func (t *tracer) deletedBranchNodes() []string { + var paths []string + for path := range t.deleteBranchNodes { + paths = append(paths, path) + } + return paths +} diff --git a/trie/trie.go b/trie/trie.go index 663399f08b..5484e5356f 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -480,12 +480,14 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error } // Replace this shortNode with the branch if it occurs at index 0. if matchlen == 0 { + t.tracer.onExpandToBranchNode(prefix) return true, branch, nil } // New branch node is created as a child of the original short node. // Track the newly inserted node in the tracer. The node identifier // passed is the path from the root node. t.tracer.onInsert(append(prefix, key[:matchlen]...)) + t.tracer.onExpandToBranchNode(append(prefix, key[:matchlen]...)) // Replace it with a short node leading up to the branch. return true, &shortNode{Key: key[:matchlen], Val: branch, flags: t.newFlag()}, nil @@ -757,6 +759,7 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) { // Mark the original short node as deleted since the // value is embedded into the parent now. t.tracer.onDelete(append(prefix, byte(pos))) + t.tracer.onDeleteBranchNode(prefix) k := append([]byte{byte(pos)}, cnode.Key...) return true, &shortNode{Key: k, Val: cnode.Val, flags: t.newFlag()}, nil @@ -764,6 +767,7 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) { } // Otherwise, n is replaced by a one-nibble short node // containing the child. + t.tracer.onDeleteBranchNode(prefix) return true, &shortNode{Key: []byte{byte(pos)}, Val: n.Children[pos], flags: t.newFlag()}, nil } // n still contains at least two values and cannot be reduced. @@ -892,6 +896,7 @@ func (t *Trie) deleteWithEpoch(n node, prefix, key []byte, epoch types.StateEpoc // Mark the original short node as deleted since the // value is embedded into the parent now. t.tracer.onDelete(append(prefix, byte(pos))) + t.tracer.onDeleteBranchNode(prefix) k := append([]byte{byte(pos)}, cnode.Key...) return true, &shortNode{Key: k, Val: cnode.Val, flags: t.newFlag(), epoch: t.currentEpoch}, nil @@ -899,6 +904,7 @@ func (t *Trie) deleteWithEpoch(n node, prefix, key []byte, epoch types.StateEpoc } // Otherwise, n is replaced by a one-nibble short node // containing the child. + t.tracer.onDeleteBranchNode(prefix) return true, &shortNode{Key: []byte{byte(pos)}, Val: n.Children[pos], flags: t.newFlag(), epoch: t.currentEpoch}, nil } // n still contains at least two values and cannot be reduced. @@ -1034,6 +1040,10 @@ func (t *Trie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet, error) for _, path := range paths { nodes.AddNode([]byte(path), trienode.NewDeleted()) } + paths = t.tracer.deletedBranchNodes() + for _, path := range paths { + nodes.AddBranchNodeEpochMeta([]byte(path), nil) + } return types.EmptyRootHash, nodes, nil // case (b) } // Derive the hash for all dirty nodes first. We hold the assumption @@ -1052,7 +1062,10 @@ func (t *Trie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet, error) for _, path := range t.tracer.deletedNodes() { nodes.AddNode([]byte(path), trienode.NewDeleted()) } - t.root = newCommitter(nodes, t.tracer, collectLeaf).Commit(t.root) + for _, path := range t.tracer.deletedBranchNodes() { + nodes.AddBranchNodeEpochMeta([]byte(path), nil) + } + t.root = newCommitter(nodes, t.tracer, collectLeaf, t.enableExpiry).Commit(t.root) return rootHash, nodes, nil } diff --git a/trie/trienode/node.go b/trie/trienode/node.go index 98d5588b6d..1127a511df 100644 --- a/trie/trienode/node.go +++ b/trie/trienode/node.go @@ -18,6 +18,8 @@ package trienode import ( "fmt" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie/epochmeta" "sort" "strings" @@ -59,19 +61,21 @@ type leaf struct { // NodeSet contains a set of nodes collected during the commit operation. // Each node is keyed by path. It's not thread-safe to use. type NodeSet struct { - Owner common.Hash - Leaves []*leaf - Nodes map[string]*Node - updates int // the count of updated and inserted nodes - deletes int // the count of deleted nodes + Owner common.Hash + Leaves []*leaf + Nodes map[string]*Node + BranchNodeEpochMetas map[string][]byte + updates int // the count of updated and inserted nodes + deletes int // the count of deleted nodes } // NewNodeSet initializes a node set. The owner is zero for the account trie and // the owning account address hash for storage tries. func NewNodeSet(owner common.Hash) *NodeSet { return &NodeSet{ - Owner: owner, - Nodes: make(map[string]*Node), + Owner: owner, + Nodes: make(map[string]*Node), + BranchNodeEpochMetas: make(map[string][]byte), } } @@ -99,6 +103,17 @@ func (set *NodeSet) AddNode(path []byte, n *Node) { set.Nodes[string(path)] = n } +// AddBranchNodeEpochMeta adds the provided epoch meta into set. +func (set *NodeSet) AddBranchNodeEpochMeta(path []byte, meta *epochmeta.BranchNodeEpochMeta) { + if meta == nil || *meta == (epochmeta.BranchNodeEpochMeta{}) { + set.BranchNodeEpochMetas[string(path)] = []byte{} + return + } + buf := rlp.NewEncoderBuffer(nil) + meta.Encode(buf) + set.BranchNodeEpochMetas[string(path)] = buf.ToBytes() +} + // Merge adds a set of nodes into the set. func (set *NodeSet) Merge(owner common.Hash, nodes map[string]*Node) error { if set.Owner != owner { @@ -197,3 +212,11 @@ func (set *MergedNodeSet) Flatten() map[common.Hash]map[string]*Node { } return nodes } + +func (set *MergedNodeSet) FlattenEpochMeta() map[common.Hash]map[string][]byte { + nodes := make(map[common.Hash]map[string][]byte) + for owner, set := range set.Sets { + nodes[owner] = set.BranchNodeEpochMetas + } + return nodes +} From 2350ed4f697d8357d4a2070b22203e749ba87296 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Wed, 30 Aug 2023 22:42:18 +0800 Subject: [PATCH 16/65] pruner: support prune expired trie/snapshot kv; --- core/state/pruner/pruner.go | 69 +++++++++++- core/state/snapshot/conversion.go | 24 +++-- core/state/snapshot/generate_test.go | 4 +- core/state/snapshot/snapshot.go | 4 +- trie/iterator.go | 2 +- trie/secure_trie.go | 2 +- trie/sync.go | 10 +- trie/trie.go | 156 +++++++++++++-------------- 8 files changed, 173 insertions(+), 98 deletions(-) diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go index 1555852d03..6b88a72372 100644 --- a/core/state/pruner/pruner.go +++ b/core/state/pruner/pruner.go @@ -21,6 +21,7 @@ import ( "encoding/binary" "errors" "fmt" + "github.com/ethereum/go-ethereum/params" "math" "os" "path/filepath" @@ -84,6 +85,7 @@ type Pruner struct { stateBloom *stateBloom snaptree *snapshot.Tree triesInMemory uint64 + chainConfig *params.ChainConfig } type BlockPruner struct { @@ -103,6 +105,10 @@ func NewPruner(db ethdb.Database, config Config, triesInMemory uint64) (*Pruner, // Offline pruning is only supported in legacy hash based scheme. triedb := trie.NewDatabase(db, trie.HashDefaults) + chainConfig := rawdb.ReadChainConfig(db, headBlock.Hash()) + if chainConfig == nil { + return nil, errors.New("failed to load chainConfig") + } snapconfig := snapshot.Config{ CacheSize: 256, Recovery: false, @@ -129,6 +135,7 @@ func NewPruner(db ethdb.Database, config Config, triesInMemory uint64) (*Pruner, stateBloom: stateBloom, snaptree: snaptree, triesInMemory: triesInMemory, + chainConfig: chainConfig, }, nil } @@ -633,10 +640,14 @@ func (p *Pruner) Prune(root common.Hash) error { } middleRoots[layer.Root()] = struct{}{} } + + pruneExpiredCh := make(chan *snapshot.ContractItem, 100000) + epoch := types.GetStateEpoch(p.chainConfig, p.chainHeader.Number) + go asyncPruneExpired(p.db, root, epoch, pruneExpiredCh) // Traverse the target state, re-construct the whole state trie and // commit to the given bloom filter. start := time.Now() - if err := snapshot.GenerateTrie(p.snaptree, root, p.db, p.stateBloom); err != nil { + if err := snapshot.GenerateTrie(p.snaptree, root, p.db, p.stateBloom, pruneExpiredCh); err != nil { return err } // Traverse the genesis, put all genesis state entries into the @@ -654,6 +665,62 @@ func (p *Pruner) Prune(root common.Hash) error { return prune(p.snaptree, root, p.db, p.stateBloom, filterName, middleRoots, start) } +// asyncPruneExpired prune trie expired state +// TODO(0xbundler): here are some issues when just delete it from hash-based storage, because it's shared kv in hbss +// but it's ok for pbss. +func asyncPruneExpired(diskdb ethdb.Database, stateRoot common.Hash, currentEpoch types.StateEpoch, expireRootCh chan *snapshot.ContractItem) { + db := trie.NewDatabaseWithConfig(diskdb, &trie.Config{ + EnableStateExpiry: true, + PathDB: nil, // TODO(0xbundler): support later + }) + + pruneItemCh := make(chan *trie.NodeInfo, 100000) + go asyncPruneExpiredStorageInDisk(diskdb, pruneItemCh) + for item := range expireRootCh { + tr, err := trie.New(&trie.ID{ + StateRoot: stateRoot, + Owner: item.Addr, + Root: item.Root, + }, db) + if err != nil { + log.Error("asyncPruneExpired, trie.New err", "id", item, "err", err) + continue + } + tr.SetEpoch(currentEpoch) + if err = tr.PruneExpired(pruneItemCh); err != nil { + log.Error("asyncPruneExpired, PruneExpired err", "id", item, "err", err) + } + } +} + +func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, expiredCh chan *trie.NodeInfo) { + batch := diskdb.NewBatch() + for info := range expiredCh { + addr := info.Addr + // delete trie kv + rawdb.DeleteTrieNode(batch, addr, info.Path, info.Hash, rawdb.HashScheme) + // delete epoch meta + if info.IsBranch { + rawdb.DeleteEpochMetaPlainState(batch, addr, string(info.Path)) + } + // replace snapshot kv only epoch + if info.IsLeaf { + sv := snapshot.NewValueWithEpoch(info.Epoch, nil) + buf := rlp.NewEncoderBuffer(nil) + sv.EncodeToRLPBytes(&buf) + rawdb.WriteStorageSnapshot(batch, addr, info.Key, buf.ToBytes()) + } + if batch.ValueSize() >= ethdb.IdealBatchSize { + batch.Write() + batch.Reset() + } + } + if batch.ValueSize() > 0 { + batch.Write() + batch.Reset() + } +} + // RecoverPruning will resume the pruning procedure during the system restart. // This function is used in this case: user tries to prune state data, but the // system was interrupted midway because of crash or manual-kill. In this case diff --git a/core/state/snapshot/conversion.go b/core/state/snapshot/conversion.go index 5c5d6a8b48..6c4d77990b 100644 --- a/core/state/snapshot/conversion.go +++ b/core/state/snapshot/conversion.go @@ -42,6 +42,11 @@ type trieKV struct { value []byte } +type ContractItem struct { + Addr common.Hash + Root common.Hash +} + type ( // trieGeneratorFn is the interface of trie generation which can // be implemented by different trie algorithm. @@ -54,18 +59,18 @@ type ( // GenerateAccountTrieRoot takes an account iterator and reproduces the root hash. func GenerateAccountTrieRoot(it AccountIterator) (common.Hash, error) { - return generateTrieRoot(nil, "", it, common.Hash{}, stackTrieGenerate, nil, newGenerateStats(), true) + return generateTrieRoot(nil, "", it, common.Hash{}, stackTrieGenerate, nil, newGenerateStats(), true, nil) } // GenerateStorageTrieRoot takes a storage iterator and reproduces the root hash. func GenerateStorageTrieRoot(account common.Hash, it StorageIterator) (common.Hash, error) { - return generateTrieRoot(nil, "", it, account, stackTrieGenerate, nil, newGenerateStats(), true) + return generateTrieRoot(nil, "", it, account, stackTrieGenerate, nil, newGenerateStats(), true, nil) } // GenerateTrie takes the whole snapshot tree as the input, traverses all the // accounts as well as the corresponding storages and regenerate the whole state // (account trie + all storage tries). -func GenerateTrie(snaptree *Tree, root common.Hash, src ethdb.Database, dst ethdb.KeyValueWriter) error { +func GenerateTrie(snaptree *Tree, root common.Hash, src ethdb.Database, dst ethdb.KeyValueWriter, pruneExpiredCh chan *ContractItem) error { // Traverse all state by snapshot, re-generate the whole state trie acctIt, err := snaptree.AccountIterator(root, common.Hash{}) if err != nil { @@ -90,12 +95,12 @@ func GenerateTrie(snaptree *Tree, root common.Hash, src ethdb.Database, dst ethd } defer storageIt.Release() - hash, err := generateTrieRoot(dst, scheme, storageIt, accountHash, stackTrieGenerate, nil, stat, false) + hash, err := generateTrieRoot(dst, scheme, storageIt, accountHash, stackTrieGenerate, nil, stat, false, nil) if err != nil { return common.Hash{}, err } return hash, nil - }, newGenerateStats(), true) + }, newGenerateStats(), true, pruneExpiredCh) if err != nil { return err @@ -245,7 +250,7 @@ func runReport(stats *generateStats, stop chan bool) { // generateTrieRoot generates the trie hash based on the snapshot iterator. // It can be used for generating account trie, storage trie or even the // whole state which connects the accounts and the corresponding storages. -func generateTrieRoot(db ethdb.KeyValueWriter, scheme string, it Iterator, account common.Hash, generatorFn trieGeneratorFn, leafCallback leafCallbackFn, stats *generateStats, report bool) (common.Hash, error) { +func generateTrieRoot(db ethdb.KeyValueWriter, scheme string, it Iterator, account common.Hash, generatorFn trieGeneratorFn, leafCallback leafCallbackFn, stats *generateStats, report bool, pruneExpiredCh chan *ContractItem) (common.Hash, error) { var ( in = make(chan trieKV) // chan to pass leaves out = make(chan common.Hash, 1) // chan to collect result @@ -329,6 +334,13 @@ func generateTrieRoot(db ethdb.KeyValueWriter, scheme string, it Iterator, accou results <- fmt.Errorf("invalid subroot(path %x), want %x, have %x", hash, account.Root, subroot) return } + // async prune trie expired states + if pruneExpiredCh != nil { + pruneExpiredCh <- &ContractItem{ + Addr: it.Hash(), + Root: account.Root, + } + } results <- nil }) fullData, err = rlp.EncodeToBytes(account) diff --git a/core/state/snapshot/generate_test.go b/core/state/snapshot/generate_test.go index 07016b675c..7d9cdc0ace 100644 --- a/core/state/snapshot/generate_test.go +++ b/core/state/snapshot/generate_test.go @@ -136,12 +136,12 @@ func checkSnapRoot(t *testing.T, snap *diskLayer, trieRoot common.Hash) { storageIt, _ := snap.StorageIterator(accountHash, common.Hash{}) defer storageIt.Release() - hash, err := generateTrieRoot(nil, "", storageIt, accountHash, stackTrieGenerate, nil, stat, false) + hash, err := generateTrieRoot(nil, "", storageIt, accountHash, stackTrieGenerate, nil, stat, false, nil) if err != nil { return common.Hash{}, err } return hash, nil - }, newGenerateStats(), true) + }, newGenerateStats(), true, nil) if err != nil { t.Fatal(err) } diff --git a/core/state/snapshot/snapshot.go b/core/state/snapshot/snapshot.go index f18b6faabe..6de5755ce2 100644 --- a/core/state/snapshot/snapshot.go +++ b/core/state/snapshot/snapshot.go @@ -813,12 +813,12 @@ func (t *Tree) Verify(root common.Hash) error { } defer storageIt.Release() - hash, err := generateTrieRoot(nil, "", storageIt, accountHash, stackTrieGenerate, nil, stat, false) + hash, err := generateTrieRoot(nil, "", storageIt, accountHash, stackTrieGenerate, nil, stat, false, nil) if err != nil { return common.Hash{}, err } return hash, nil - }, newGenerateStats(), true) + }, newGenerateStats(), true, nil) if err != nil { return err diff --git a/trie/iterator.go b/trie/iterator.go index 6f054a7245..4fb626e522 100644 --- a/trie/iterator.go +++ b/trie/iterator.go @@ -735,7 +735,7 @@ func (it *unionIterator) AddResolver(resolver NodeResolver) { // // In the case that descend=false - eg, we're asked to ignore all subnodes of the // current node - we also advance any iterators in the heap that have the current -// path as a prefix. +// Path as a prefix. func (it *unionIterator) Next(descend bool) bool { if len(*it.items) == 0 { return false diff --git a/trie/secure_trie.go b/trie/secure_trie.go index 2098c463ac..5382b2a1f6 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -255,7 +255,7 @@ func (t *StateTrie) Hash() common.Hash { } func (t *StateTrie) SetEpoch(epoch types.StateEpoch) { - t.trie.currentEpoch = epoch + t.trie.SetEpoch(epoch) } // Copy returns a copy of StateTrie. diff --git a/trie/sync.go b/trie/sync.go index 4f55845991..35d36f6e04 100644 --- a/trie/sync.go +++ b/trie/sync.go @@ -61,7 +61,7 @@ const maxFetchesPerDepth = 16384 // - Path 0x012345678901234567890123456789010123456789012345678901234567890199 -> {0x0123456789012345678901234567890101234567890123456789012345678901, 0x0099} type SyncPath [][]byte -// NewSyncPath converts an expanded trie path from nibble form into a compact +// NewSyncPath converts an expanded trie Path from nibble form into a compact // version that can be sent over the network. func NewSyncPath(path []byte) SyncPath { // If the hash is from the account trie, append a single item, if it @@ -82,8 +82,8 @@ func NewSyncPath(path []byte) SyncPath { // trie (account) or a layered trie (account -> storage). Each key in the tuple // is in the raw format(32 bytes). // -// The path is a composite hexary path identifying the trie node. All the key -// bytes are converted to the hexary nibbles and composited with the parent path +// The Path is a composite hexary Path identifying the trie node. All the key +// bytes are converted to the hexary nibbles and composited with the parent Path // if the trie node is in a layered trie. // // It's used by state sync and commit to allow handling external references @@ -140,7 +140,7 @@ func newSyncMemBatch() *syncMemBatch { } } -// hasNode reports the trie node with specific path is already cached. +// hasNode reports the trie node with specific Path is already cached. func (batch *syncMemBatch) hasNode(path []byte) bool { _, ok := batch.nodes[string(path)] return ok @@ -510,7 +510,7 @@ func (s *Sync) commitNodeRequest(req *nodeRequest) error { s.membatch.nodes[string(req.path)] = req.data s.membatch.hashes[string(req.path)] = req.hash // The size tracking refers to the db-batch, not the in-memory data. - // Therefore, we ignore the req.path, and account only for the hash+data + // Therefore, we ignore the req.Path, and account only for the hash+data // which eventually is written to db. s.membatch.size += common.HashLength + uint64(len(req.data)) delete(s.nodeReqs, string(req.path)) diff --git a/trie/trie.go b/trie/trie.go index 5484e5356f..9b5cd001bd 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -25,7 +25,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/trie/trienode" ) @@ -1295,8 +1294,22 @@ func (t *Trie) epochExpired(n node, epoch types.StateEpoch) bool { return types.EpochExpired(epoch, t.currentEpoch) } -// PruneExpiredStorageTrie traverses the storage trie and prunes all expired nodes. -func (t *Trie) PruneExpiredStorageTrie(db ethdb.Database) error { +func (t *Trie) SetEpoch(epoch types.StateEpoch) { + t.currentEpoch = epoch +} + +type NodeInfo struct { + Addr common.Hash + Path []byte + Hash common.Hash + Epoch types.StateEpoch + Key common.Hash // only leaf has right Key. + IsLeaf bool + IsBranch bool +} + +// PruneExpired traverses the storage trie and prunes all expired nodes. +func (t *Trie) PruneExpired(pruneItemCh chan *NodeInfo) error { if !t.enableExpiry { return nil @@ -1306,14 +1319,11 @@ func (t *Trie) PruneExpiredStorageTrie(db ethdb.Database) error { return fmt.Errorf("cannot prune account trie") } - var ( - hasher = newHasher(false) - batch = db.NewBatch() - ) - - defer returnHasherToPool(hasher) - - _, _, err := t.pruneExpiredStorageTrie(t.root, nil, t.getRootEpoch(), false, batch, hasher) + err := t.findExpiredSubTree(t.root, nil, t.getRootEpoch(), func(n node, path []byte, epoch types.StateEpoch) { + if pruneErr := t.recursePruneExpiredNode(n, path, epoch, pruneItemCh); pruneErr != nil { + log.Error("recursePruneExpiredNode err", "Path", path, "err", pruneErr) + } + }) if err != nil { return err } @@ -1321,115 +1331,101 @@ func (t *Trie) PruneExpiredStorageTrie(db ethdb.Database) error { return nil } -func (t *Trie) pruneExpiredStorageTrie(n node, path []byte, curEpoch types.StateEpoch, isExpired bool, batch ethdb.Batch, h *hasher) (node, bool, error) { - +func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, pruner func(n node, path []byte, epoch types.StateEpoch)) error { // Upon reaching expired node, it will recursively traverse downwards to all the child nodes // and collect their hashes. Then, the corresponding key-value pairs will be deleted from the // database by batches. - if isExpired { - var hashList []common.Hash - cn, err := t.pruneExpired(n, path, h, &hashList) - if err != nil { - return nil, false, err - } - - for _, hash := range hashList { - batch.Delete(hash[:]) - } - - // Handle 1 expired subtree at a time - batch.Write() - batch.Reset() - - return cn, true, nil + if t.epochExpired(n, epoch) { + pruner(n, path, epoch) + return nil } switch n := n.(type) { case *shortNode: - newNode, didExpire, err := t.pruneExpiredStorageTrie(n.Val, append(path, n.Key...), curEpoch, isExpired, batch, h) - if err == nil && didExpire { - n = n.copy() - n.Val = newNode + err := t.findExpiredSubTree(n.Val, append(path, n.Key...), epoch, pruner) + if err != nil { + return err } - return n, didExpire, err + return nil case *fullNode: - - hasExpired := false - + var err error // Go through every child and recursively delete expired nodes for i, child := range n.Children { - childExpired, _ := n.ChildExpired(path, i, curEpoch) - newNode, didExpire, err := t.pruneExpiredStorageTrie(child, append(path, byte(i)), curEpoch, childExpired, batch, h) - if err == nil && didExpire { - n = n.copy() - n.Children[byte(i)] = newNode - hasExpired = true + err = t.findExpiredSubTree(child, append(path, byte(i)), epoch, pruner) + if err != nil { + return err } } - return n, hasExpired, nil + return nil case hashNode: - child, err := t.resolveAndTrack(n, path) + resolve, err := t.resolveAndTrack(n, path) if err != nil { - return nil, false, err + return err } - if err = t.resolveEpochMeta(child, curEpoch, path); err != nil { - return nil, false, err + if err = t.resolveEpochMeta(resolve, epoch, path); err != nil { + return err } - newNode, didExpire, err := t.pruneExpiredStorageTrie(child, path, curEpoch, isExpired, batch, h) - return newNode, didExpire, err + return t.findExpiredSubTree(resolve, path, epoch, pruner) case valueNode: - return n, false, nil + return nil default: panic(fmt.Sprintf("invalid node type: %T", n)) } } -func (t *Trie) pruneExpired(n node, path []byte, hasher *hasher, hashList *[]common.Hash) (node, error) { - +func (t *Trie) recursePruneExpiredNode(n node, path []byte, epoch types.StateEpoch, pruneItemCh chan *NodeInfo) error { switch n := n.(type) { case *shortNode: - cn, err := t.pruneExpired(n.Val, append(path, n.Key...), hasher, hashList) + key := common.Hash{} + _, ok := n.Val.(valueNode) + if ok { + key = common.BytesToHash(hexToKeybytes(append(path, n.Key...))) + } + err := t.recursePruneExpiredNode(n.Val, append(path, n.Key...), epoch, pruneItemCh) if err != nil { - return nil, err + return err } - - n.Val = cn - hn, _ := hasher.hash(n, false) - if hn, ok := hn.(hashNode); ok { - *hashList = append(*hashList, common.BytesToHash(hn)) - return hn, nil + // prune child first + pruneItemCh <- &NodeInfo{ + Addr: t.owner, + Hash: common.BytesToHash(n.flags.hash), + Path: path, + Key: key, + Epoch: epoch, + IsLeaf: ok, } - - return n, nil - + return nil case *fullNode: for i, child := range n.Children { - cn, err := t.pruneExpired(child, append(path, byte(i)), hasher, hashList) + err := t.recursePruneExpiredNode(child, append(path, byte(i)), n.EpochMap[i], pruneItemCh) if err != nil { - return nil, err + return err } - n.Children[byte(i)] = cn } - - hn, _ := hasher.hash(n, false) - if hn, ok := hn.(hashNode); ok { - *hashList = append(*hashList, common.BytesToHash(hn)) - return hn, nil + // prune child first + pruneItemCh <- &NodeInfo{ + Addr: t.owner, + Hash: common.BytesToHash(n.flags.hash), + Path: path, + Epoch: epoch, + IsBranch: true, } - - return n, nil - + return nil case hashNode: - child, err := t.resolveAndTrack(n, path) + // hashNode is a index of trie node storage, need not prune. + resolve, err := t.resolveAndTrack(n, path) if err != nil { - return nil, err + return err } - newNode, err := t.pruneExpired(child, path, hasher, hashList) - return newNode, err + if err = t.resolveEpochMeta(resolve, epoch, path); err != nil { + return err + } + return t.recursePruneExpiredNode(resolve, path, epoch, pruneItemCh) case valueNode: - return n, nil + // value node is not a single storage uint, so pass to prune. + return nil default: panic(fmt.Sprintf("invalid node type: %T", n)) } From cca3319678b3a547e99de87e3bc07eed7bf2db59 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Wed, 30 Aug 2023 22:42:18 +0800 Subject: [PATCH 17/65] pruner: support prune expired trie/snapshot kv; --- core/state/pruner/pruner.go | 7 +++---- core/state/snapshot/snapshot_expire.go | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go index 6b88a72372..ce00cd1240 100644 --- a/core/state/pruner/pruner.go +++ b/core/state/pruner/pruner.go @@ -705,10 +705,9 @@ func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, expiredCh chan *trie. } // replace snapshot kv only epoch if info.IsLeaf { - sv := snapshot.NewValueWithEpoch(info.Epoch, nil) - buf := rlp.NewEncoderBuffer(nil) - sv.EncodeToRLPBytes(&buf) - rawdb.WriteStorageSnapshot(batch, addr, info.Key, buf.ToBytes()) + if err := snapshot.ShrinkExpiredLeaf(batch, addr, info.Key, info.Epoch); err != nil { + log.Error("ShrinkExpiredLeaf err", "addr", addr, "key", info.Key, "err", err) + } } if batch.ValueSize() >= ethdb.IdealBatchSize { batch.Write() diff --git a/core/state/snapshot/snapshot_expire.go b/core/state/snapshot/snapshot_expire.go index 2b62da24a5..7c53e7dde5 100644 --- a/core/state/snapshot/snapshot_expire.go +++ b/core/state/snapshot/snapshot_expire.go @@ -8,7 +8,7 @@ import ( ) // ShrinkExpiredLeaf tool function for snapshot kv prune -func ShrinkExpiredLeaf(db ethdb.KeyValueStore, accountHash common.Hash, storageHash common.Hash, epoch types.StateEpoch) error { +func ShrinkExpiredLeaf(db ethdb.KeyValueWriter, accountHash common.Hash, storageHash common.Hash, epoch types.StateEpoch) error { valWithEpoch := NewValueWithEpoch(epoch, nil) enc, err := EncodeValueToRLPBytes(valWithEpoch) if err != nil { From b8ce0569f791deb42094ad6957803d752591a601 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Fri, 1 Sep 2023 15:49:01 +0800 Subject: [PATCH 18/65] state/trieprefetcher: support handle expired state; ethdb/fullstatedb: support proof cache; trie/epochmeta: opt query & commit logic; bugfix: fix remote full state db params; --- accounts/abi/bind/backends/simulated.go | 2 +- common/big.go | 4 ++ core/blockchain.go | 24 ++++----- core/blockchain_reader.go | 8 +-- core/blockchain_test.go | 2 +- core/state/pruner/pruner.go | 5 ++ core/state/state_expiry.go | 63 +++++++++++++++++++++++ core/state/state_object.go | 63 ++++------------------- core/state/statedb.go | 22 ++++---- core/state/trie_prefetcher.go | 58 ++++++++++++++++++++- core/txpool/blobpool/blobpool.go | 4 +- core/txpool/blobpool/blobpool_test.go | 2 +- core/txpool/blobpool/interface.go | 2 +- core/txpool/legacypool/legacypool.go | 4 +- core/txpool/legacypool/legacypool_test.go | 2 +- core/types/meta.go | 3 ++ core/types/state_epoch.go | 3 +- eth/api_backend.go | 4 +- eth/api_debug.go | 6 +-- eth/protocols/eth/handler_test.go | 2 +- eth/state_accessor.go | 8 +-- eth/tracers/api_test.go | 2 +- ethdb/fullstatedb.go | 43 +++++++++++++--- internal/ethapi/api_test.go | 2 +- miner/miner_test.go | 2 +- miner/worker.go | 2 +- trie/epochmeta/difflayer.go | 38 +++++++------- trie/proof.go | 2 +- trie/trie.go | 39 +++++--------- trie/trie_reader.go | 17 ++++-- 30 files changed, 277 insertions(+), 161 deletions(-) create mode 100644 core/state/state_expiry.go diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go index acc1143497..b54a766dcf 100644 --- a/accounts/abi/bind/backends/simulated.go +++ b/accounts/abi/bind/backends/simulated.go @@ -188,7 +188,7 @@ func (b *SimulatedBackend) stateByBlockNumber(ctx context.Context, blockNumber * if err != nil { return nil, err } - return b.blockchain.StateAt(block.Root(), blockNumber) + return b.blockchain.StateAt(block.Root(), block.Hash(), block.Number()) } // CodeAt returns the code associated with a certain account in the blockchain. diff --git a/common/big.go b/common/big.go index 65d4377bf7..4d32622092 100644 --- a/common/big.go +++ b/common/big.go @@ -28,3 +28,7 @@ var ( Big256 = big.NewInt(256) Big257 = big.NewInt(257) ) + +func AddBig1(tmp *big.Int) *big.Int { + return new(big.Int).Add(tmp, Big1) +} diff --git a/core/blockchain.go b/core/blockchain.go index d3a12e5646..d9d1202b36 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -374,6 +374,14 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis bc.processor = NewStateProcessor(chainConfig, bc, engine) var err error + if cacheConfig.EnableStateExpiry { + log.Info("enable state expiry feature", "RemoteEndPoint", cacheConfig.RemoteEndPoint) + bc.enableStateExpiry = true + bc.fullStateDB, err = ethdb.NewFullStateRPCServer(cacheConfig.RemoteEndPoint) + if err != nil { + return nil, err + } + } bc.hc, err = NewHeaderChain(db, chainConfig, engine, bc.insertStopped) if err != nil { return nil, err @@ -548,14 +556,6 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis bc.wg.Add(1) go bc.maintainTxIndex() } - - if cacheConfig.EnableStateExpiry { - bc.enableStateExpiry = true - bc.fullStateDB, err = ethdb.NewFullStateRPCServer(cacheConfig.RemoteEndPoint) - if err != nil { - return nil, err - } - } return bc, nil } @@ -1041,13 +1041,13 @@ func (bc *BlockChain) SnapSyncCommitHead(hash common.Hash) error { } // StateAtWithSharedPool returns a new mutable state based on a particular point in time with sharedStorage -func (bc *BlockChain) StateAtWithSharedPool(root common.Hash, height *big.Int) (*state.StateDB, error) { +func (bc *BlockChain) StateAtWithSharedPool(root, startAtBlockHash common.Hash, height *big.Int) (*state.StateDB, error) { stateDB, err := state.NewWithSharedPool(root, bc.stateCache, bc.snaps) if err != nil { return nil, err } if bc.enableStateExpiry { - stateDB.InitStateExpiry(bc.chainConfig, height, bc.fullStateDB) + stateDB.InitStateExpiryFeature(bc.chainConfig, bc.fullStateDB, startAtBlockHash, height) } return stateDB, err } @@ -2049,7 +2049,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) } bc.updateHighestVerifiedHeader(block.Header()) if bc.enableStateExpiry { - statedb.InitStateExpiry(bc.chainConfig, block.Number(), bc.fullStateDB) + statedb.InitStateExpiryFeature(bc.chainConfig, bc.fullStateDB, parent.Hash(), block.Number()) } // Enable prefetching to pull in trie node paths while processing transactions @@ -2061,7 +2061,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) // 1.do state prefetch for snapshot cache throwaway := statedb.CopyDoPrefetch() if throwaway != nil && bc.enableStateExpiry { - throwaway.InitStateExpiry(bc.chainConfig, block.Number(), bc.fullStateDB) + throwaway.InitStateExpiryFeature(bc.chainConfig, bc.fullStateDB, parent.Hash(), block.Number()) } go bc.prefetcher.Prefetch(block, throwaway, &bc.vmConfig, interruptCh) diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index 1626fe46fa..540a70ad00 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -347,17 +347,17 @@ func (bc *BlockChain) ContractCodeWithPrefix(hash common.Hash) ([]byte, error) { // State returns a new mutable state based on the current HEAD block. func (bc *BlockChain) State() (*state.StateDB, error) { - return bc.StateAt(bc.CurrentBlock().Root, bc.CurrentBlock().Number) + return bc.StateAt(bc.CurrentBlock().Root, bc.CurrentBlock().Hash(), bc.CurrentBlock().Number) } // StateAt returns a new mutable state based on a particular point in time. -func (bc *BlockChain) StateAt(root common.Hash, height *big.Int) (*state.StateDB, error) { - sdb, err := state.New(root, bc.stateCache, bc.snaps) +func (bc *BlockChain) StateAt(startAtRoot common.Hash, startAtBlockHash common.Hash, expectHeight *big.Int) (*state.StateDB, error) { + sdb, err := state.New(startAtRoot, bc.stateCache, bc.snaps) if err != nil { return nil, err } if bc.enableStateExpiry { - sdb.InitStateExpiry(bc.chainConfig, height, bc.fullStateDB) + sdb.InitStateExpiryFeature(bc.chainConfig, bc.fullStateDB, startAtBlockHash, expectHeight) } return sdb, err } diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 98015411f0..750e6518c9 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -4346,7 +4346,7 @@ func TestTransientStorageReset(t *testing.T) { t.Fatalf("failed to insert into chain: %v", err) } // Check the storage - state, err := chain.StateAt(chain.CurrentHeader().Root, chain.CurrentHeader().Number) + state, err := chain.StateAt(chain.CurrentHeader().Root, chain.CurrentHeader().Hash(), chain.CurrentHeader().Number) if err != nil { t.Fatalf("Failed to load state %v", err) } diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go index ce00cd1240..828dd96a46 100644 --- a/core/state/pruner/pruner.go +++ b/core/state/pruner/pruner.go @@ -19,6 +19,7 @@ package pruner import ( "bytes" "encoding/binary" + "encoding/hex" "errors" "fmt" "github.com/ethereum/go-ethereum/params" @@ -677,6 +678,7 @@ func asyncPruneExpired(diskdb ethdb.Database, stateRoot common.Hash, currentEpoc pruneItemCh := make(chan *trie.NodeInfo, 100000) go asyncPruneExpiredStorageInDisk(diskdb, pruneItemCh) for item := range expireRootCh { + log.Info("start scan trie expired state", "addr", item.Addr, "root", item.Root) tr, err := trie.New(&trie.ID{ StateRoot: stateRoot, Owner: item.Addr, @@ -696,6 +698,9 @@ func asyncPruneExpired(diskdb ethdb.Database, stateRoot common.Hash, currentEpoc func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, expiredCh chan *trie.NodeInfo) { batch := diskdb.NewBatch() for info := range expiredCh { + log.Info("found expired state", "addr", info.Addr, "path", + hex.EncodeToString(info.Path), "epoch", info.Epoch, "isBranch", + info.IsBranch, "isLeaf", info.IsLeaf) addr := info.Addr // delete trie kv rawdb.DeleteTrieNode(batch, addr, info.Path, info.Hash, rawdb.HashScheme) diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go new file mode 100644 index 0000000000..6c1964efc5 --- /dev/null +++ b/core/state/state_expiry.go @@ -0,0 +1,63 @@ +package state + +import ( + "bytes" + "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/trie" +) + +// fetchExpiredStorageFromRemote request expired state from remote full state node; +func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, blockHash common.Hash, addr common.Address, tr Trie, prefixKey []byte, key common.Hash) ([]byte, error) { + // if no prefix, query from revive trie, got the newest expired info + if len(prefixKey) == 0 { + _, err := tr.GetStorage(addr, key.Bytes()) + if enErr, ok := err.(*trie.ExpiredNodeError); ok { + prefixKey = enErr.Path + } + } + proofs, err := fullDB.GetStorageReviveProof(blockHash, addr, []string{common.Bytes2Hex(prefixKey)}, []string{common.Bytes2Hex(key[:])}) + if err != nil { + return nil, err + } + + if len(proofs) == 0 { + log.Error("cannot find any revive proof from remoteDB", "addr", addr, "prefix", prefixKey, "key", key) + return nil, fmt.Errorf("cannot find any revive proof from remoteDB") + } + + return reviveStorageTrie(addr, tr, proofs[0], prefixKey) +} + +// reviveStorageTrie revive trie's expired state from proof +func reviveStorageTrie(addr common.Address, tr Trie, proof types.ReviveStorageProof, targetPrefix []byte) ([]byte, error) { + prefixKey := common.Hex2Bytes(proof.PrefixKey) + if !bytes.Equal(targetPrefix, prefixKey) { + return nil, fmt.Errorf("revive with wrong prefix, target: %#x, actual: %#x", targetPrefix, prefixKey) + } + + key := common.Hex2Bytes(proof.Key) + proofs := make([][]byte, 0, len(proof.Proof)) + + for _, p := range proof.Proof { + proofs = append(proofs, common.Hex2Bytes(p)) + } + + // TODO(asyukii): support proofs merge, revive in nubs + err := tr.ReviveTrie(crypto.Keccak256(key), prefixKey, proofs) + if err != nil { + return nil, fmt.Errorf("revive storage trie failed, err: %v", err) + } + + // Update pending revive state + val, err := tr.GetStorage(addr, key) // TODO(asyukii): may optimize this, return value when revive trie + if err != nil { + return nil, fmt.Errorf("get storage value failed, err: %v", err) + } + + return val, nil +} diff --git a/core/state/state_object.go b/core/state/state_object.go index 1c1cc53c2c..38392657e1 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -20,6 +20,7 @@ import ( "bytes" "fmt" "github.com/ethereum/go-ethereum/core/state/snapshot" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/trie" "io" "math/big" @@ -277,7 +278,7 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { return common.Hash{} } // if query success, just set val, otherwise request from trie - if err != nil && sv != nil { + if err == nil && sv != nil { value.SetBytes(sv.GetVal()) s.originStorageEpoch[key] = sv.GetEpoch() } @@ -762,39 +763,6 @@ func (s *stateObject) Nonce() uint64 { return s.data.Nonce } -func (s *stateObject) ReviveStorageTrie(proof types.ReviveStorageProof, targetPrefix []byte) error { - prefixKey := common.Hex2Bytes(proof.PrefixKey) - if !bytes.Equal(targetPrefix, prefixKey) { - return fmt.Errorf("revive with wrong prefix, target: %#x, actual: %#x", targetPrefix, prefixKey) - } - - dr, err := s.getPendingReviveTrie() - if err != nil { - return err - } - key := common.Hex2Bytes(proof.Key) - proofList := make([][]byte, 0, len(proof.Proof)) - - for _, p := range proof.Proof { - proofList = append(proofList, common.Hex2Bytes(p)) - } - - // TODO(asyukii): support proofs merge, revive in nubs - err = dr.ReviveTrie(crypto.Keccak256(key), prefixKey, proofList) - if err != nil { - return fmt.Errorf("revive storage trie failed, err: %v", err) - } - - // Update pending revive state - val, err := dr.GetStorageAndUpdateEpoch(s.address, key) // TODO(asyukii): may optimize this, return value when revive trie - if err != nil { - return fmt.Errorf("get storage value failed, err: %v", err) - } - - s.pendingReviveState[proof.Key] = common.BytesToHash(val) - return nil -} - // accessState record all access states, now in pendingAccessedStateEpoch without consensus func (s *stateObject) accessState(key common.Hash) { if !s.db.EnableExpire() { @@ -814,31 +782,20 @@ func (s *stateObject) queryFromReviveState(reviveState map[string]common.Hash, k return val, ok } -// fetchExpiredFromRemote request expired state from remote full state node; +// fetchExpiredStorageFromRemote request expired state from remote full state node; func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash) ([]byte, error) { - // if no prefix, query from revive trie, got the newest expired info - if len(prefixKey) == 0 { - tr, err := s.getPendingReviveTrie() - if err != nil { - return nil, err - } - _, err = tr.GetStorage(s.address, key.Bytes()) - if enErr, ok := err.(*trie.ExpiredNodeError); ok { - prefixKey = enErr.Path - } - } - proofs, err := s.db.fullStateDB.GetStorageReviveProof(s.db.originalRoot, s.address, []string{common.Bytes2Hex(prefixKey)}, []string{common.Bytes2Hex(key[:])}) + log.Info("fetchExpiredStorageFromRemote in stateDB", "addr", s.address, "prefixKey", prefixKey, "key", key) + tr, err := s.getPendingReviveTrie() if err != nil { return nil, err } - for _, proof := range proofs { - if err := s.ReviveStorageTrie(proof, prefixKey); err != nil { - return nil, err - } + val, err := fetchExpiredStorageFromRemote(s.db.fullStateDB, s.db.originalHash, s.address, tr, prefixKey, key) + if err != nil { + return nil, err } - val := s.pendingReviveState[key.String()] - return val.Bytes(), nil + s.pendingReviveState[key.String()] = common.BytesToHash(val) + return val, nil } func (s *stateObject) getExpirySnapStorage(key common.Hash) (snapshot.SnapValue, error, error) { diff --git a/core/state/statedb.go b/core/state/statedb.go index 0d3117ae0d..67f74c2e5a 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -142,9 +142,10 @@ type StateDB struct { nextRevisionId int // state expiry feature - enableStateExpiry bool + enableStateExpiry bool // default disable epoch types.StateEpoch // epoch indicate stateDB start at which block's target epoch - fullStateDB ethdb.FullStateDB //RemoteFullStateNode + fullStateDB ethdb.FullStateDB // RemoteFullStateNode + originalHash common.Hash // Measurements gathered during execution for debugging purposes // MetricsMux should be used in more places, but will affect on performance, so following meteration is not accruate @@ -238,14 +239,17 @@ func (s *StateDB) TransferPrefetcher(prev *StateDB) { s.prefetcherLock.Unlock() } -// InitStateExpiry it must set in initial, reset later will cause wrong result -func (s *StateDB) InitStateExpiry(config *params.ChainConfig, height *big.Int, remote ethdb.FullStateDB) *StateDB { - if config == nil || height == nil || remote == nil { +// InitStateExpiryFeature it must set in initial, reset later will cause wrong result +// Attention: startAtBlockHash corresponding to stateDB's originalRoot, expectHeight is the epoch indicator. +func (s *StateDB) InitStateExpiryFeature(config *params.ChainConfig, remote ethdb.FullStateDB, startAtBlockHash common.Hash, expectHeight *big.Int) *StateDB { + if config == nil || expectHeight == nil || remote == nil { panic("cannot init state expiry stateDB with nil config/height/remote") } s.enableStateExpiry = true s.fullStateDB = remote - s.epoch = types.GetStateEpoch(config, height) + s.epoch = types.GetStateEpoch(config, expectHeight) + s.originalHash = startAtBlockHash + log.Info("StateDB enable state expiry feature", "expectHeight", expectHeight, "startAtBlockHash", startAtBlockHash, "epoch", s.epoch) return s } @@ -269,6 +273,7 @@ func (s *StateDB) StartPrefetcher(namespace string) { } else { s.prefetcher = newTriePrefetcher(s.db, s.originalRoot, common.Hash{}, namespace) } + s.prefetcher.InitStateExpiryFeature(s.epoch, s.originalHash, s.fullStateDB) } } @@ -1912,10 +1917,7 @@ func (s *StateDB) AddSlotToAccessList(addr common.Address, slot common.Hash) { } func (s *StateDB) EnableExpire() bool { - if !s.enableStateExpiry { - return false - } - return types.EpochExpired(types.StateEpoch0, s.epoch) + return s.enableStateExpiry } // AddressInAccessList returns true if the given address is in the access list. diff --git a/core/state/trie_prefetcher.go b/core/state/trie_prefetcher.go index 4184369d9c..c29b1ef297 100644 --- a/core/state/trie_prefetcher.go +++ b/core/state/trie_prefetcher.go @@ -17,12 +17,15 @@ package state import ( + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb" "sync" "sync/atomic" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" + trie2 "github.com/ethereum/go-ethereum/trie" ) const ( @@ -63,6 +66,12 @@ type triePrefetcher struct { fetchersMutex sync.RWMutex prefetchChan chan *prefetchMsg // no need to wait for return + // state expiry feature + enableStateExpiry bool // default disable + epoch types.StateEpoch // epoch indicate stateDB start at which block's target epoch + fullStateDB ethdb.FullStateDB // RemoteFullStateNode + blockHash common.Hash + deliveryMissMeter metrics.Meter accountLoadMeter metrics.Meter accountDupMeter metrics.Meter @@ -123,6 +132,9 @@ func (p *triePrefetcher) mainLoop() { fetcher := p.fetchers[id] if fetcher == nil { fetcher = newSubfetcher(p.db, p.root, pMsg.owner, pMsg.root, pMsg.addr) + if p.enableStateExpiry { + fetcher.initStateExpiryFeature(p.epoch, p.blockHash, p.fullStateDB) + } p.fetchersMutex.Lock() p.fetchers[id] = fetcher p.fetchersMutex.Unlock() @@ -204,6 +216,14 @@ func (p *triePrefetcher) mainLoop() { } } +// InitStateExpiryFeature it must call in initial period. +func (p *triePrefetcher) InitStateExpiryFeature(epoch types.StateEpoch, blockHash common.Hash, fullStateDB ethdb.FullStateDB) { + p.enableStateExpiry = true + p.epoch = epoch + p.fullStateDB = fullStateDB + p.blockHash = blockHash +} + // close iterates over all the subfetchers, aborts any that were left spinning // and reports the stats to the metrics subsystem. func (p *triePrefetcher) close() { @@ -354,6 +374,12 @@ type subfetcher struct { addr common.Address // Address of the account that the trie belongs to trie Trie // Trie being populated with nodes + // state expiry feature + enableStateExpiry bool // default disable + epoch types.StateEpoch // epoch indicate stateDB start at which block's target epoch + fullStateDB ethdb.FullStateDB // RemoteFullStateNode + blockHash common.Hash + tasks [][]byte // Items queued up for retrieval lock sync.Mutex // Lock protecting the task queue @@ -385,10 +411,21 @@ func newSubfetcher(db Database, state common.Hash, owner common.Hash, root commo copy: make(chan chan Trie), seen: make(map[string]struct{}), } - go sf.loop() return sf } +func (sf *subfetcher) start() { + go sf.loop() +} + +// InitStateExpiryFeature it must call in initial period. +func (sf *subfetcher) initStateExpiryFeature(epoch types.StateEpoch, blockHash common.Hash, fullStateDB ethdb.FullStateDB) { + sf.enableStateExpiry = true + sf.epoch = epoch + sf.fullStateDB = fullStateDB + sf.blockHash = blockHash +} + // schedule adds a batch of trie keys to the queue to prefetch. func (sf *subfetcher) schedule(keys [][]byte) { atomic.AddUint32(&sf.pendingSize, uint32(len(keys))) @@ -432,6 +469,9 @@ func (sf *subfetcher) scheduleParallel(keys [][]byte) { keysLeftSize := len(keysLeft) for i := 0; i*parallelTriePrefetchCapacity < keysLeftSize; i++ { child := newSubfetcher(sf.db, sf.state, sf.owner, sf.root, sf.addr) + if sf.enableStateExpiry { + child.initStateExpiryFeature(sf.epoch, sf.blockHash, sf.fullStateDB) + } sf.paraChildren = append(sf.paraChildren, child) endIndex := (i + 1) * parallelTriePrefetchCapacity if endIndex >= keysLeftSize { @@ -484,6 +524,9 @@ func (sf *subfetcher) loop() { trie, err = sf.db.OpenTrie(sf.root) } else { trie, err = sf.db.OpenStorageTrie(sf.state, sf.addr, sf.root) + if err == nil && sf.enableStateExpiry { + trie.SetEpoch(sf.epoch) + } } if err != nil { log.Debug("Trie prefetcher failed opening trie", "root", sf.root, "err", err) @@ -535,7 +578,18 @@ func (sf *subfetcher) loop() { if len(task) == common.AddressLength { sf.trie.GetAccount(common.BytesToAddress(task)) } else { - sf.trie.GetStorage(sf.addr, task) + _, err := sf.trie.GetStorage(sf.addr, task) + // handle expired state + if sf.enableStateExpiry { + if exErr, match := err.(*trie2.ExpiredNodeError); match { + key := common.BytesToHash(task) + log.Info("fetchExpiredStorageFromRemote in trie prefetcher", "addr", sf.addr, "prefixKey", exErr.Path, "key", key) + _, err = fetchExpiredStorageFromRemote(sf.fullStateDB, sf.blockHash, sf.addr, sf.trie, exErr.Path, key) + if err != nil { + log.Error("subfetcher fetchExpiredStorageFromRemote err", "addr", sf.addr, "path", exErr.Path, "err", err) + } + } + } } sf.seen[string(task)] = struct{}{} } diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index a36ed15d3a..b363d7d5a0 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -365,7 +365,7 @@ func (p *BlobPool) Init(gasTip *big.Int, head *types.Header, reserve txpool.Addr return err } } - state, err := p.chain.StateAt(head.Root, head.Number) + state, err := p.chain.StateAt(head.Root, head.Hash(), head.Number) if err != nil { return err } @@ -746,7 +746,7 @@ func (p *BlobPool) Reset(oldHead, newHead *types.Header) { resettimeHist.Update(time.Since(start).Nanoseconds()) }(time.Now()) - statedb, err := p.chain.StateAt(newHead.Root, newHead.Number) + statedb, err := p.chain.StateAt(newHead.Root, newHead.Hash(), newHead.Number) if err != nil { log.Error("Failed to reset blobpool state", "err", err) return diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go index 3db5336c48..dd348f9c2a 100644 --- a/core/txpool/blobpool/blobpool_test.go +++ b/core/txpool/blobpool/blobpool_test.go @@ -158,7 +158,7 @@ func (bt *testBlockChain) GetBlock(hash common.Hash, number uint64) *types.Block return nil } -func (bc *testBlockChain) StateAt(common.Hash, *big.Int) (*state.StateDB, error) { +func (bc *testBlockChain) StateAt(common.Hash, common.Hash, *big.Int) (*state.StateDB, error) { return bc.statedb, nil } diff --git a/core/txpool/blobpool/interface.go b/core/txpool/blobpool/interface.go index 98fe44d3a4..f3c0658fc2 100644 --- a/core/txpool/blobpool/interface.go +++ b/core/txpool/blobpool/interface.go @@ -41,5 +41,5 @@ type BlockChain interface { GetBlock(hash common.Hash, number uint64) *types.Block // StateAt returns a state database for a given root hash (generally the head). - StateAt(root common.Hash, height *big.Int) (*state.StateDB, error) + StateAt(root common.Hash, blockHash common.Hash, height *big.Int) (*state.StateDB, error) } diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index b8df608f8c..88f1c839a2 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -125,7 +125,7 @@ type BlockChain interface { GetBlock(hash common.Hash, number uint64) *types.Block // StateAt returns a state database for a given root hash (generally the head). - StateAt(root common.Hash, height *big.Int) (*state.StateDB, error) + StateAt(root common.Hash, blockHash common.Hash, height *big.Int) (*state.StateDB, error) } // Config are the configuration parameters of the transaction pool. @@ -1470,7 +1470,7 @@ func (pool *LegacyPool) reset(oldHead, newHead *types.Header) { if newHead == nil { newHead = pool.chain.CurrentBlock() // Special case during testing } - statedb, err := pool.chain.StateAt(newHead.Root, newHead.Number) + statedb, err := pool.chain.StateAt(newHead.Root, newHead.Hash(), newHead.Number) if err != nil { log.Error("Failed to reset txpool state", "err", err) return diff --git a/core/txpool/legacypool/legacypool_test.go b/core/txpool/legacypool/legacypool_test.go index 4c95d50f8c..6efcdf8dac 100644 --- a/core/txpool/legacypool/legacypool_test.go +++ b/core/txpool/legacypool/legacypool_test.go @@ -88,7 +88,7 @@ func (bc *testBlockChain) GetBlock(hash common.Hash, number uint64) *types.Block return types.NewBlock(bc.CurrentBlock(), nil, nil, nil, trie.NewStackTrie(nil)) } -func (bc *testBlockChain) StateAt(common.Hash, *big.Int) (*state.StateDB, error) { +func (bc *testBlockChain) StateAt(common.Hash, common.Hash, *big.Int) (*state.StateDB, error) { return bc.statedb, nil } diff --git a/core/types/meta.go b/core/types/meta.go index 1bd8963909..afd6f8655b 100644 --- a/core/types/meta.go +++ b/core/types/meta.go @@ -49,6 +49,9 @@ func (m MetaNoConsensus) EncodeToRLPBytes() ([]byte, error) { } func DecodeMetaNoConsensusFromRLPBytes(enc []byte) (MetaNoConsensus, error) { + if len(enc) == 0 { + return EmptyMetaNoConsensus, nil + } var mc MetaNoConsensus if err := rlp.DecodeBytes(enc, &mc); err != nil { return EmptyMetaNoConsensus, err diff --git a/core/types/state_epoch.go b/core/types/state_epoch.go index 741367fb30..0df9d7f562 100644 --- a/core/types/state_epoch.go +++ b/core/types/state_epoch.go @@ -8,7 +8,8 @@ import ( ) const ( - DefaultStateEpochPeriod = uint64(7_008_000) + //DefaultStateEpochPeriod = uint64(7_008_000) + DefaultStateEpochPeriod = uint64(50) StateEpoch0 = StateEpoch(0) StateEpoch1 = StateEpoch(1) StateEpochKeepLiveNum = StateEpoch(2) diff --git a/eth/api_backend.go b/eth/api_backend.go index 13a6793a6f..ada7c3f944 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -204,7 +204,7 @@ func (b *EthAPIBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.B if header == nil { return nil, nil, errors.New("header not found") } - stateDb, err := b.eth.BlockChain().StateAt(header.Root, header.Number) + stateDb, err := b.eth.BlockChain().StateAt(header.Root, header.Hash(), header.Number) return stateDb, header, err } @@ -223,7 +223,7 @@ func (b *EthAPIBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockN if blockNrOrHash.RequireCanonical && b.eth.blockchain.GetCanonicalHash(header.Number.Uint64()) != hash { return nil, nil, errors.New("hash is not currently canonical") } - stateDb, err := b.eth.BlockChain().StateAt(header.Root, header.Number) + stateDb, err := b.eth.BlockChain().StateAt(header.Root, header.Hash(), header.Number) return stateDb, header, err } return nil, nil, errors.New("invalid arguments; neither block nor hash specified") diff --git a/eth/api_debug.go b/eth/api_debug.go index 66b7c6e6f1..a3868faf0b 100644 --- a/eth/api_debug.go +++ b/eth/api_debug.go @@ -79,7 +79,7 @@ func (api *DebugAPI) DumpBlock(blockNr rpc.BlockNumber) (state.Dump, error) { if header == nil { return state.Dump{}, fmt.Errorf("block #%d not found", blockNr) } - stateDb, err := api.eth.BlockChain().StateAt(header.Root, header.Number) + stateDb, err := api.eth.BlockChain().StateAt(header.Root, header.Hash(), header.Number) if err != nil { return state.Dump{}, err } @@ -164,7 +164,7 @@ func (api *DebugAPI) AccountRange(blockNrOrHash rpc.BlockNumberOrHash, start hex if header == nil { return state.IteratorDump{}, fmt.Errorf("block #%d not found", number) } - stateDb, err = api.eth.BlockChain().StateAt(header.Root, header.Number) + stateDb, err = api.eth.BlockChain().StateAt(header.Root, header.Hash(), header.Number) if err != nil { return state.IteratorDump{}, err } @@ -174,7 +174,7 @@ func (api *DebugAPI) AccountRange(blockNrOrHash rpc.BlockNumberOrHash, start hex if block == nil { return state.IteratorDump{}, fmt.Errorf("block %s not found", hash.Hex()) } - stateDb, err = api.eth.BlockChain().StateAt(block.Root(), block.Number()) + stateDb, err = api.eth.BlockChain().StateAt(block.Root(), block.Hash(), block.Number()) if err != nil { return state.IteratorDump{}, err } diff --git a/eth/protocols/eth/handler_test.go b/eth/protocols/eth/handler_test.go index d254f96d71..3d82823406 100644 --- a/eth/protocols/eth/handler_test.go +++ b/eth/protocols/eth/handler_test.go @@ -538,7 +538,7 @@ func testGetNodeData(t *testing.T, protocol uint, drop bool) { root := block.Root() reconstructed, _ := state.New(root, state.NewDatabase(reconstructDB), nil) for j, acc := range accounts { - state, _ := backend.chain.StateAt(root, block.Number()) + state, _ := backend.chain.StateAt(root, block.Hash(), block.Number()) bw := state.GetBalance(acc) bh := reconstructed.GetBalance(acc) diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 18c9fb14aa..dd4aefa9bc 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -72,7 +72,7 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u database = state.NewDatabaseWithConfig(eth.chainDb, trie.HashDefaults) if statedb, err = state.New(block.Root(), database, nil); err == nil { if eth.blockchain.EnableStateExpiry() { - statedb.InitStateExpiry(eth.blockchain.Config(), block.Number(), eth.blockchain.FullStateDB()) + statedb.InitStateExpiryFeature(eth.blockchain.Config(), eth.blockchain.FullStateDB(), block.Hash(), block.Number()) } log.Info("Found disk backend for state trie", "root", block.Root(), "number", block.Number()) return statedb, noopReleaser, nil @@ -102,7 +102,7 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u statedb, err = state.New(current.Root(), database, nil) if err == nil { if eth.blockchain.EnableStateExpiry() { - statedb.InitStateExpiry(eth.blockchain.Config(), block.Number(), eth.blockchain.FullStateDB()) + statedb.InitStateExpiryFeature(eth.blockchain.Config(), eth.blockchain.FullStateDB(), current.Hash(), block.Number()) } return statedb, noopReleaser, nil } @@ -124,7 +124,7 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u statedb, err = state.New(current.Root(), database, nil) if err == nil { if eth.blockchain.EnableStateExpiry() { - statedb.InitStateExpiry(eth.blockchain.Config(), block.Number(), eth.blockchain.FullStateDB()) + statedb.InitStateExpiryFeature(eth.blockchain.Config(), eth.blockchain.FullStateDB(), current.Hash(), block.Number()) } break } @@ -176,7 +176,7 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u return nil, nil, fmt.Errorf("state reset after block %d failed: %v", current.NumberU64(), err) } if eth.blockchain.EnableStateExpiry() { - statedb.InitStateExpiry(eth.blockchain.Config(), block.Number(), eth.blockchain.FullStateDB()) + statedb.InitStateExpiryFeature(eth.blockchain.Config(), eth.blockchain.FullStateDB(), current.Hash(), new(big.Int).Add(current.Number(), common.Big1)) } // Hold the state reference and also drop the parent state // to prevent accumulating too many nodes in memory. diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go index 661aab83e7..44b3595215 100644 --- a/eth/tracers/api_test.go +++ b/eth/tracers/api_test.go @@ -141,7 +141,7 @@ func (b *testBackend) teardown() { } func (b *testBackend) StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, StateReleaseFunc, error) { - statedb, err := b.chain.StateAt(block.Root(), block.Number()) + statedb, err := b.chain.StateAt(block.Root(), block.Hash(), block.Number()) if err != nil { return nil, nil, errStateNotFound } diff --git a/ethdb/fullstatedb.go b/ethdb/fullstatedb.go index 91af886af4..7f800e047e 100644 --- a/ethdb/fullstatedb.go +++ b/ethdb/fullstatedb.go @@ -6,6 +6,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rpc" + lru "github.com/hashicorp/golang-lru" "strings" "time" ) @@ -13,12 +14,13 @@ import ( // FullStateDB expired state could fetch from it type FullStateDB interface { // GetStorageReviveProof fetch target proof according to specific params - GetStorageReviveProof(root common.Hash, account common.Address, prefixKeys, keys []string) ([]types.ReviveStorageProof, error) + GetStorageReviveProof(blockHash common.Hash, account common.Address, prefixKeys, keys []string) ([]types.ReviveStorageProof, error) } type FullStateRPCServer struct { endpoint string client *rpc.Client + cache *lru.Cache } func NewFullStateRPCServer(endpoint string) (FullStateDB, error) { @@ -30,25 +32,52 @@ func NewFullStateRPCServer(endpoint string) (FullStateDB, error) { // these prefixes. endpoint = endpoint[4:] } - // TODO(0xbundler): add more opts, like auth? - client, err := rpc.DialOptions(context.Background(), endpoint, nil) + // TODO(0xbundler): add more opts, like auth, cache size? + client, err := rpc.DialOptions(context.Background(), endpoint) + if err != nil { + return nil, err + } + + cache, err := lru.New(10000) if err != nil { return nil, err } return &FullStateRPCServer{ endpoint: endpoint, client: client, + cache: cache, }, nil } -func (f *FullStateRPCServer) GetStorageReviveProof(root common.Hash, account common.Address, prefixKeys, keys []string) ([]types.ReviveStorageProof, error) { +func (f *FullStateRPCServer) GetStorageReviveProof(blockHash common.Hash, account common.Address, prefixKeys, keys []string) ([]types.ReviveStorageProof, error) { + // find from lru cache, now it cache key proof + uncahcedPrefixKeys := make([]string, 0, len(prefixKeys)) + uncahcedKeys := make([]string, 0, len(keys)) + ret := make([]types.ReviveStorageProof, 0, len(keys)) + for i, key := range keys { + val, ok := f.cache.Get(key) + if !ok { + uncahcedPrefixKeys = append(uncahcedPrefixKeys, prefixKeys[i]) + uncahcedKeys = append(uncahcedKeys, keys[i]) + continue + } + ret = append(ret, val.(types.ReviveStorageProof)) + } + // TODO(0xbundler): add timeout in flags? ctx, cancelFunc := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancelFunc() - proofs := make([]types.ReviveStorageProof, 0, len(keys)) - err := f.client.CallContext(ctx, &proofs, "eth_getStorageReviveProof", account, prefixKeys, keys, root) + proofs := make([]types.ReviveStorageProof, 0, len(uncahcedKeys)) + err := f.client.CallContext(ctx, &proofs, "eth_getStorageReviveProof", account, uncahcedKeys, uncahcedPrefixKeys, blockHash) if err != nil { return nil, err } - return proofs, err + + // add to cache + for _, proof := range proofs { + f.cache.Add(proof.Key, proof) + } + + ret = append(ret, proofs...) + return ret, err } diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 57a5914409..5b1b52f567 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -454,7 +454,7 @@ func (b testBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.Bloc if header == nil { return nil, nil, errors.New("header not found") } - stateDb, err := b.chain.StateAt(header.Root, header.Number) + stateDb, err := b.chain.StateAt(header.Root, header.Hash(), header.Number) return stateDb, header, err } func (b testBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*state.StateDB, *types.Header, error) { diff --git a/miner/miner_test.go b/miner/miner_test.go index 20e9969fa5..fa8941a076 100644 --- a/miner/miner_test.go +++ b/miner/miner_test.go @@ -85,7 +85,7 @@ func (bc *testBlockChain) GetBlock(hash common.Hash, number uint64) *types.Block return types.NewBlock(bc.CurrentBlock(), nil, nil, nil, trie.NewStackTrie(nil)) } -func (bc *testBlockChain) StateAt(common.Hash, *big.Int) (*state.StateDB, error) { +func (bc *testBlockChain) StateAt(common.Hash, common.Hash, *big.Int) (*state.StateDB, error) { return bc.statedb, nil } diff --git a/miner/worker.go b/miner/worker.go index 326f94e531..75580e1d9d 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -650,7 +650,7 @@ func (w *worker) makeEnv(parent *types.Header, header *types.Header, coinbase co prevEnv *environment) (*environment, error) { // Retrieve the parent state to execute on top and start a prefetcher for // the miner to speed block sealing up a bit - state, err := w.chain.StateAtWithSharedPool(parent.Root, header.Number) + state, err := w.chain.StateAtWithSharedPool(parent.Root, parent.Hash(), header.Number) if err != nil { return nil, err } diff --git a/trie/epochmeta/difflayer.go b/trie/epochmeta/difflayer.go index c519e3a73f..85a4cb5608 100644 --- a/trie/epochmeta/difflayer.go +++ b/trie/epochmeta/difflayer.go @@ -106,7 +106,7 @@ func (s *SnapshotTree) Cap(blockRoot common.Hash) error { return nil } - last, ok := flatten[len(flatten)-1].(*epochMetaDiskLayer) + last, ok := flatten[len(flatten)-1].(*diskLayer) if !ok { return errors.New("the diff layers not link to disk layer") } @@ -213,7 +213,7 @@ func (s *SnapshotTree) removeSubLayers(layers []common.Hash, skip *common.Hash) } // flattenDiffs2Disk delete all flatten and push them to db -func (s *SnapshotTree) flattenDiffs2Disk(flatten []snapshot, diskLayer *epochMetaDiskLayer) (*epochMetaDiskLayer, error) { +func (s *SnapshotTree) flattenDiffs2Disk(flatten []snapshot, diskLayer *diskLayer) (*diskLayer, error) { var err error for i := len(flatten) - 1; i >= 0; i-- { diskLayer, err = diskLayer.PushDiff(flatten[i].(*diffLayer)) @@ -226,7 +226,7 @@ func (s *SnapshotTree) flattenDiffs2Disk(flatten []snapshot, diskLayer *epochMet } // loadDiskLayer load from db, could be nil when none in db -func loadDiskLayer(db ethdb.KeyValueStore) (*epochMetaDiskLayer, error) { +func loadDiskLayer(db ethdb.KeyValueStore) (*diskLayer, error) { val := rawdb.ReadEpochMetaPlainStateMeta(db) // if there is no disk layer, will construct a fake disk layer if len(val) == 0 { @@ -248,7 +248,7 @@ func loadDiskLayer(db ethdb.KeyValueStore) (*epochMetaDiskLayer, error) { return layer, nil } -func loadDiffLayers(db ethdb.KeyValueStore, diskLayer *epochMetaDiskLayer) (map[common.Hash]snapshot, map[common.Hash][]common.Hash, error) { +func loadDiffLayers(db ethdb.KeyValueStore, diskLayer *diskLayer) (map[common.Hash]snapshot, map[common.Hash][]common.Hash, error) { layers := make(map[common.Hash]snapshot) children := make(map[common.Hash][]common.Hash) @@ -369,8 +369,8 @@ func (s *diffLayer) Parent() snapshot { // Update append new diff layer onto current, nodeChgRecord when val is []byte{}, it delete the kv func (s *diffLayer) Update(blockNumber *big.Int, blockRoot common.Hash, nodeSet map[common.Hash]map[string][]byte) (snapshot, error) { s.lock.RLock() - if s.blockNumber.Cmp(blockNumber) >= 0 { - return nil, errors.New("update a unordered diff layer") + if s.blockNumber.Int64() != 0 && s.blockNumber.Cmp(blockNumber) >= 0 { + return nil, errors.New("update a unordered diff layer in diff layer") } s.lock.RUnlock() return newEpochMetaDiffLayer(blockNumber, blockRoot, s, nodeSet), nil @@ -436,7 +436,7 @@ type epochMetaPlainMeta struct { BlockRoot common.Hash } -type epochMetaDiskLayer struct { +type diskLayer struct { diskdb ethdb.KeyValueStore blockNumber *big.Int blockRoot common.Hash @@ -444,12 +444,12 @@ type epochMetaDiskLayer struct { lock sync.RWMutex } -func newEpochMetaDiskLayer(diskdb ethdb.KeyValueStore, blockNumber *big.Int, blockRoot common.Hash) (*epochMetaDiskLayer, error) { +func newEpochMetaDiskLayer(diskdb ethdb.KeyValueStore, blockNumber *big.Int, blockRoot common.Hash) (*diskLayer, error) { cache, err := lru.New(defaultDiskLayerCacheSize) if err != nil { return nil, err } - return &epochMetaDiskLayer{ + return &diskLayer{ diskdb: diskdb, blockNumber: blockNumber, blockRoot: blockRoot, @@ -457,13 +457,13 @@ func newEpochMetaDiskLayer(diskdb ethdb.KeyValueStore, blockNumber *big.Int, blo }, nil } -func (s *epochMetaDiskLayer) Root() common.Hash { +func (s *diskLayer) Root() common.Hash { s.lock.RLock() defer s.lock.RUnlock() return s.blockRoot } -func (s *epochMetaDiskLayer) EpochMeta(addr common.Hash, path string) ([]byte, error) { +func (s *diskLayer) EpochMeta(addr common.Hash, path string) ([]byte, error) { s.lock.RLock() defer s.lock.RUnlock() @@ -478,24 +478,24 @@ func (s *epochMetaDiskLayer) EpochMeta(addr common.Hash, path string) ([]byte, e return val, nil } -func (s *epochMetaDiskLayer) Parent() snapshot { +func (s *diskLayer) Parent() snapshot { return nil } -func (s *epochMetaDiskLayer) Update(blockNumber *big.Int, blockRoot common.Hash, nodeSet map[common.Hash]map[string][]byte) (snapshot, error) { +func (s *diskLayer) Update(blockNumber *big.Int, blockRoot common.Hash, nodeSet map[common.Hash]map[string][]byte) (snapshot, error) { s.lock.RLock() - if s.blockNumber.Cmp(blockNumber) >= 0 { - return nil, errors.New("update a unordered diff layer") + if s.blockNumber.Int64() != 0 && s.blockNumber.Cmp(blockNumber) >= 0 { + return nil, errors.New("update a unordered diff layer in disk layer") } s.lock.RUnlock() return newEpochMetaDiffLayer(blockNumber, blockRoot, s, nodeSet), nil } -func (s *epochMetaDiskLayer) Journal(buffer *bytes.Buffer) (common.Hash, error) { +func (s *diskLayer) Journal(buffer *bytes.Buffer) (common.Hash, error) { return common.Hash{}, nil } -func (s *epochMetaDiskLayer) PushDiff(diff *diffLayer) (*epochMetaDiskLayer, error) { +func (s *diskLayer) PushDiff(diff *diffLayer) (*diskLayer, error) { s.lock.Lock() defer s.lock.Unlock() @@ -525,7 +525,7 @@ func (s *epochMetaDiskLayer) PushDiff(diff *diffLayer) (*epochMetaDiskLayer, err if err = batch.Write(); err != nil { return nil, err } - diskLayer := &epochMetaDiskLayer{ + diskLayer := &diskLayer{ diskdb: s.diskdb, blockNumber: number, blockRoot: diff.blockRoot, @@ -541,7 +541,7 @@ func (s *epochMetaDiskLayer) PushDiff(diff *diffLayer) (*epochMetaDiskLayer, err return diskLayer, nil } -func (s *epochMetaDiskLayer) writeHistory(number *big.Int, batch ethdb.Batch, nodeSet map[common.Hash]map[string][]byte) error { +func (s *diskLayer) writeHistory(number *big.Int, batch ethdb.Batch, nodeSet map[common.Hash]map[string][]byte) error { for addr, subSet := range nodeSet { for path, val := range subSet { // refresh plain state diff --git a/trie/proof.go b/trie/proof.go index 9bd62d309e..19a67e9474 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -251,7 +251,7 @@ func ConstructTrieFromProof(keyHex []byte, prefixKeyHex []byte, proofList [][]by node := proofList[i] n, err := decodeNode(nil, node) if err != nil { - return nil, fmt.Errorf("decode proof item %064x, err: %x", node, err) + return nil, fmt.Errorf("decode proof item %#x, err: %v", node, err) } if parentNode == nil { diff --git a/trie/trie.go b/trie/trie.go index 9b5cd001bd..a9f7334c31 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -21,8 +21,6 @@ import ( "bytes" "errors" "fmt" - "github.com/ethereum/go-ethereum/trie/epochmeta" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" @@ -93,9 +91,10 @@ func New(id *ID, db *Database) (*Trie, error) { return nil, err } trie := &Trie{ - owner: id.Owner, - reader: reader, - tracer: newTracer(), + owner: id.Owner, + reader: reader, + tracer: newTracer(), + enableExpiry: db.snapTree != nil, } if id.Root != (common.Hash{}) && id.Root != types.EmptyRootHash { rootnode, err := trie.resolveAndTrack(id.Root[:], nil) @@ -103,17 +102,16 @@ func New(id *ID, db *Database) (*Trie, error) { return nil, err } trie.root = rootnode - } - - // resolve root epoch - if db.snapTree != nil { - meta, err := reader.accountMeta() - if err != nil { - return nil, err + // resolve root epoch + if trie.enableExpiry { + meta, err := reader.accountMeta() + if err != nil { + return nil, err + } + trie.rootEpoch = meta.Epoch() } - trie.rootEpoch = meta.Epoch() - trie.enableExpiry = true } + return trie, nil } @@ -981,20 +979,11 @@ func (t *Trie) resolveEpochMeta(n node, epoch types.StateEpoch, prefix []byte) e return nil case *fullNode: n.setEpoch(epoch) - blob, err := t.reader.epochMeta(prefix) + meta, err := t.reader.epochMeta(prefix) if err != nil { return err } - if len(blob) == 0 { - // set default epoch map - n.EpochMap = [16]types.StateEpoch{} - } else { - meta, decErr := epochmeta.DecodeFullNodeEpochMeta(blob) - if decErr != nil { - return decErr - } - n.EpochMap = meta.EpochMap - } + n.EpochMap = meta.EpochMap return nil case valueNode, hashNode, nil: // just skip diff --git a/trie/trie_reader.go b/trie/trie_reader.go index c3182c67c1..924d379bbf 100644 --- a/trie/trie_reader.go +++ b/trie/trie_reader.go @@ -113,16 +113,25 @@ func (l *trieLoader) OpenStorageTrie(stateRoot common.Hash, addrHash, root commo } // epochMeta resolve from epoch meta storage -func (r *trieReader) epochMeta(path []byte) ([]byte, error) { +func (r *trieReader) epochMeta(path []byte) (*epochmeta.BranchNodeEpochMeta, error) { if r.emdb == nil { return nil, fmt.Errorf("cannot resolve epochmeta without db, path: %#x", path) } + // epoch meta cloud be empty, because epoch0 or delete? blob, err := r.emdb.Get(r.owner, string(path)) - if err != nil || len(blob) == 0 { + if err != nil { return nil, fmt.Errorf("resolve epoch meta err, path: %#x, err: %v", path, err) } - return blob, nil + if len(blob) == 0 { + // set default epoch map + return epochmeta.NewBranchNodeEpochMeta([16]types.StateEpoch{}), nil + } + meta, err := epochmeta.DecodeFullNodeEpochMeta(blob) + if err != nil { + return nil, err + } + return meta, nil } // accountMeta resolve account metadata @@ -132,7 +141,7 @@ func (r *trieReader) accountMeta() (types.MetaNoConsensus, error) { } blob, err := r.emdb.Get(r.owner, epochmeta.AccountMetadataPath) - if err != nil || len(blob) == 0 { + if err != nil { return types.EmptyMetaNoConsensus, fmt.Errorf("resolve epoch meta err for account, err: %v", err) } return types.DecodeMetaNoConsensusFromRLPBytes(blob) From 794273c1a2a806d32081c1e072c23082ae806a6a Mon Sep 17 00:00:00 2001 From: asyukii Date: Sat, 2 Sep 2023 02:39:33 +0800 Subject: [PATCH 19/65] fix: revive storage trie from remoteDB error more fixes minor --- core/state/state_expiry.go | 13 +++++++++---- core/state/state_object.go | 8 +++++++- trie/errors.go | 14 ++++++++++++++ trie/proof.go | 2 +- trie/proof_test.go | 2 ++ trie/trie.go | 32 +++++++++++++++++++++----------- 6 files changed, 54 insertions(+), 17 deletions(-) diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index 6c1964efc5..bea6d0d3c6 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" @@ -35,26 +36,30 @@ func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, blockHash common.Ha // reviveStorageTrie revive trie's expired state from proof func reviveStorageTrie(addr common.Address, tr Trie, proof types.ReviveStorageProof, targetPrefix []byte) ([]byte, error) { - prefixKey := common.Hex2Bytes(proof.PrefixKey) + // prefixKey := common.Hex2Bytes(proof.PrefixKey) + + prefixKey, _ := hexutil.Decode(proof.PrefixKey) + if !bytes.Equal(targetPrefix, prefixKey) { return nil, fmt.Errorf("revive with wrong prefix, target: %#x, actual: %#x", targetPrefix, prefixKey) } - key := common.Hex2Bytes(proof.Key) + key := hexutil.MustDecode(proof.Key) proofs := make([][]byte, 0, len(proof.Proof)) for _, p := range proof.Proof { proofs = append(proofs, common.Hex2Bytes(p)) + proofs = append(proofs, hexutil.MustDecode(p)) } // TODO(asyukii): support proofs merge, revive in nubs err := tr.ReviveTrie(crypto.Keccak256(key), prefixKey, proofs) if err != nil { - return nil, fmt.Errorf("revive storage trie failed, err: %v", err) + return nil, err } // Update pending revive state - val, err := tr.GetStorage(addr, key) // TODO(asyukii): may optimize this, return value when revive trie + val, err := tr.GetStorageAndUpdateEpoch(addr, key) // TODO(asyukii): may optimize this, return value when revive trie if err != nil { return nil, fmt.Errorf("get storage value failed, err: %v", err) } diff --git a/core/state/state_object.go b/core/state/state_object.go index 38392657e1..f33ba8690f 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -791,10 +791,16 @@ func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash) } val, err := fetchExpiredStorageFromRemote(s.db.fullStateDB, s.db.originalHash, s.address, tr, prefixKey, key) + if err != nil { - return nil, err + // Keys may not exist in the trie, so they can't be revived. + if _, ok := err.(*trie.KeyDoesNotExistError); ok { + return nil, nil + } + return nil, fmt.Errorf("revive storage trie failed, err: %v", err) } s.pendingReviveState[key.String()] = common.BytesToHash(val) + return val, nil } diff --git a/trie/errors.go b/trie/errors.go index 199856a585..33f114ade3 100644 --- a/trie/errors.go +++ b/trie/errors.go @@ -67,3 +67,17 @@ func NewExpiredNodeError(path []byte, epoch types.StateEpoch) error { func (err *ExpiredNodeError) Error() string { return "expired trie node" } + +type KeyDoesNotExistError struct { + Key []byte +} + +func NewKeyDoesNotExistError(key []byte) error { + return &KeyDoesNotExistError{ + Key: key, + } +} + +func (err *KeyDoesNotExistError) Error() string { + return "key does not exist" +} diff --git a/trie/proof.go b/trie/proof.go index 19a67e9474..d94898a5e3 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -262,7 +262,7 @@ func ConstructTrieFromProof(keyHex []byte, prefixKeyHex []byte, proofList [][]by keyrest, cld := get(n, keyHex, false) switch cld := cld.(type) { case nil: - return nil, fmt.Errorf("the trie doesn't contain the key") + return nil, NewKeyDoesNotExistError(keyHex) case hashNode: keyHex = keyrest // Verify that the child node is a hashNode and matches the hash in the proof diff --git a/trie/proof_test.go b/trie/proof_test.go index 913237bc49..220200e821 100644 --- a/trie/proof_test.go +++ b/trie/proof_test.go @@ -1110,7 +1110,9 @@ func nonRandomTrie(n int) (*Trie, map[string]*kv) { func nonRandomTrieWithExpiry(n int) (*Trie, map[string]*kv) { db := NewDatabase(rawdb.NewMemoryDatabase()) trie := NewEmpty(db) + trie.currentEpoch = 10 trie.rootEpoch = 10 + trie.enableExpiry = true vals := make(map[string]*kv) max := uint64(0xffffffffffffffff) for i := uint64(0); i < uint64(n); i++ { diff --git a/trie/trie.go b/trie/trie.go index a9f7334c31..da192090ec 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -75,6 +75,7 @@ func (t *Trie) Copy() *Trie { reader: t.reader, tracer: t.tracer.copy(), rootEpoch: t.rootEpoch, + currentEpoch: t.currentEpoch, enableExpiry: t.enableExpiry, } } @@ -442,6 +443,7 @@ func (t *Trie) updateWithEpoch(key, value []byte, epoch types.StateEpoch) error } t.root = n } + t.rootEpoch = t.currentEpoch return nil } @@ -648,6 +650,7 @@ func (t *Trie) Delete(key []byte) error { if t.enableExpiry { _, n, err = t.deleteWithEpoch(t.root, nil, k, t.getRootEpoch()) + t.rootEpoch = t.currentEpoch } else { _, n, err = t.delete(t.root, nil, k) } @@ -1101,12 +1104,22 @@ func (t *Trie) ReviveTrie(key []byte, prefixKeyHex []byte, proofList [][]byte) e return err } - newRoot, _, err := t.revive(t.root, key, prefixKeyHex, 0, revivedNode, common.BytesToHash(revivedHash), t.getRootEpoch(), false) + newRoot, didRevive, err := t.revive(t.root, key, prefixKeyHex, 0, revivedNode, common.BytesToHash(revivedHash), t.getRootEpoch(), types.EpochExpired(t.getRootEpoch(), t.currentEpoch)) if err != nil { return err } - t.root = newRoot + if didRevive { + switch n := newRoot.(type) { + case *shortNode: + n.setEpoch(t.currentEpoch) + case *fullNode: + n.setEpoch(t.currentEpoch) + n.UpdateChildEpoch(int(key[0]), t.currentEpoch) + } + t.root = newRoot + t.rootEpoch = t.currentEpoch + } return nil } @@ -1123,14 +1136,11 @@ func (t *Trie) revive(n node, key []byte, prefixKeyHex []byte, pos int, revivedN return nil, false, fmt.Errorf("target revive node is not expired") } - hn, ok := n.(hashNode) - if !ok { - return nil, false, fmt.Errorf("prefix key path does not lead to a hash node") - } - - // Compare the hash of the revived node with the hash of the hash node - if revivedHash != common.BytesToHash(hn) { - return nil, false, fmt.Errorf("revived node hash does not match the hash node hash") + if hn, ok := n.(hashNode); ok { + // Compare the hash of the revived node with the hash of the hash node + if revivedHash != common.BytesToHash(hn) { + return nil, false, fmt.Errorf("revived node hash does not match the hash node hash") + } } return revivedNode, true, nil @@ -1155,7 +1165,7 @@ func (t *Trie) revive(n node, key []byte, prefixKeyHex []byte, pos int, revivedN return n, didRevived, err case *fullNode: childIndex := int(key[pos]) - childExpired, _ := n.ChildExpired(key[:pos], childIndex, t.getRootEpoch()) + childExpired, _ := n.ChildExpired(key[:pos], childIndex, t.currentEpoch) newNode, didRevived, err := t.revive(n.Children[childIndex], key, prefixKeyHex, pos+1, revivedNode, revivedHash, n.GetChildEpoch(childIndex), childExpired) if err == nil && didRevived { n = n.copy() From 76336070ac7e5b914a880f01a65df4ea582647a6 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Mon, 4 Sep 2023 11:50:49 +0800 Subject: [PATCH 20/65] trie/proof: fix child hash validate bug; bugfix: fix state copy issues, remote cache; --- common/bytes.go | 7 +++ core/state/state_expiry.go | 21 ++++----- core/state/statedb.go | 1 + core/types/state_epoch.go | 2 +- ethdb/fullstatedb.go | 18 +++++++- trie/proof.go | 87 ++++++++++++++++++-------------------- trie/trie.go | 12 +++++- 7 files changed, 84 insertions(+), 64 deletions(-) diff --git a/common/bytes.go b/common/bytes.go index d1f5c6c995..eaaa29c6cf 100644 --- a/common/bytes.go +++ b/common/bytes.go @@ -36,6 +36,13 @@ func FromHex(s string) []byte { return Hex2Bytes(s) } +func No0xPrefix(s string) string { + if has0xPrefix(s) { + return s[2:] + } + return s +} + // CopyBytes returns an exact copy of the provided bytes. func CopyBytes(b []byte) (copiedBytes []byte) { if b == nil { diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index bea6d0d3c6..b0ebfd85ef 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" @@ -31,25 +30,21 @@ func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, blockHash common.Ha return nil, fmt.Errorf("cannot find any revive proof from remoteDB") } - return reviveStorageTrie(addr, tr, proofs[0], prefixKey) + return reviveStorageTrie(addr, tr, proofs[0], key) } // reviveStorageTrie revive trie's expired state from proof -func reviveStorageTrie(addr common.Address, tr Trie, proof types.ReviveStorageProof, targetPrefix []byte) ([]byte, error) { - // prefixKey := common.Hex2Bytes(proof.PrefixKey) - - prefixKey, _ := hexutil.Decode(proof.PrefixKey) - - if !bytes.Equal(targetPrefix, prefixKey) { - return nil, fmt.Errorf("revive with wrong prefix, target: %#x, actual: %#x", targetPrefix, prefixKey) +func reviveStorageTrie(addr common.Address, tr Trie, proof types.ReviveStorageProof, targetKey common.Hash) ([]byte, error) { + key := common.FromHex(proof.Key) + if !bytes.Equal(targetKey[:], key) { + return nil, fmt.Errorf("revive with wrong key, target: %#x, actual: %#x", targetKey, key) } - key := hexutil.MustDecode(proof.Key) + prefixKey := common.FromHex(proof.PrefixKey) proofs := make([][]byte, 0, len(proof.Proof)) for _, p := range proof.Proof { - proofs = append(proofs, common.Hex2Bytes(p)) - proofs = append(proofs, hexutil.MustDecode(p)) + proofs = append(proofs, common.FromHex(p)) } // TODO(asyukii): support proofs merge, revive in nubs @@ -59,7 +54,7 @@ func reviveStorageTrie(addr common.Address, tr Trie, proof types.ReviveStoragePr } // Update pending revive state - val, err := tr.GetStorageAndUpdateEpoch(addr, key) // TODO(asyukii): may optimize this, return value when revive trie + val, err := tr.GetStorage(addr, key) // TODO(asyukii): may optimize this, return value when revive trie if err != nil { return nil, fmt.Errorf("get storage value failed, err: %v", err) } diff --git a/core/state/statedb.go b/core/state/statedb.go index 67f74c2e5a..25209d5fe2 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -991,6 +991,7 @@ func (s *StateDB) copyInternal(doPrefetch bool) *StateDB { epoch: s.epoch, enableStateExpiry: s.enableStateExpiry, fullStateDB: s.fullStateDB, + originalHash: s.originalHash, // In order for the block producer to be able to use and make additions // to the snapshot tree, we need to copy that as well. Otherwise, any diff --git a/core/types/state_epoch.go b/core/types/state_epoch.go index 0df9d7f562..82192d6433 100644 --- a/core/types/state_epoch.go +++ b/core/types/state_epoch.go @@ -9,7 +9,7 @@ import ( const ( //DefaultStateEpochPeriod = uint64(7_008_000) - DefaultStateEpochPeriod = uint64(50) + DefaultStateEpochPeriod = uint64(30) StateEpoch0 = StateEpoch(0) StateEpoch1 = StateEpoch(1) StateEpochKeepLiveNum = StateEpoch(2) diff --git a/ethdb/fullstatedb.go b/ethdb/fullstatedb.go index 7f800e047e..8121f0c01f 100644 --- a/ethdb/fullstatedb.go +++ b/ethdb/fullstatedb.go @@ -1,10 +1,12 @@ package ethdb import ( + "bytes" "context" "errors" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" lru "github.com/hashicorp/golang-lru" "strings" @@ -55,7 +57,8 @@ func (f *FullStateRPCServer) GetStorageReviveProof(blockHash common.Hash, accoun uncahcedKeys := make([]string, 0, len(keys)) ret := make([]types.ReviveStorageProof, 0, len(keys)) for i, key := range keys { - val, ok := f.cache.Get(key) + val, ok := f.cache.Get(proofCacheKey(blockHash, account, key)) + log.Info("GetStorageReviveProof hit cache", "account", account, "key", key, "ok", ok) if !ok { uncahcedPrefixKeys = append(uncahcedPrefixKeys, prefixKeys[i]) uncahcedKeys = append(uncahcedKeys, keys[i]) @@ -75,9 +78,20 @@ func (f *FullStateRPCServer) GetStorageReviveProof(blockHash common.Hash, accoun // add to cache for _, proof := range proofs { - f.cache.Add(proof.Key, proof) + log.Info("GetStorageReviveProof cache", "account", account, "key", proof.Key) + f.cache.Add(proofCacheKey(blockHash, account, proof.Key), proof) } ret = append(ret, proofs...) return ret, err } + +func proofCacheKey(blockHash common.Hash, account common.Address, key string) string { + buf := bytes.NewBuffer(make([]byte, 0, 66+len(key))) + buf.Write(blockHash[:]) + buf.WriteByte('$') + buf.Write(account[:]) + buf.WriteByte('$') + buf.WriteString(common.No0xPrefix(key)) + return buf.String() +} diff --git a/trie/proof.go b/trie/proof.go index d94898a5e3..171111e00b 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -239,74 +239,67 @@ func VerifyPathProof(keyHex []byte, prefixKeyHex []byte, proofList [][]byte, epo // ConstructTrieFromProof constructs a trie from the given proof. It returns the root node of the trie. func ConstructTrieFromProof(keyHex []byte, prefixKeyHex []byte, proofList [][]byte, epoch types.StateEpoch) (node, error) { - var parentNode node - var root node - + if len(proofList) == 0 { + return nil, nil + } + h := newHasher(false) + defer returnHasherToPool(h) keyHex = keyHex[len(prefixKeyHex):] - for i := 0; i < len(proofList); i++ { - - var n node + root, err := decodeNode(nil, proofList[0]) + if err != nil { + return nil, fmt.Errorf("decode proof root %#x, err: %v", proofList[0], err) + } + // update epoch + switch n := root.(type) { + case *shortNode: + n.setEpoch(epoch) + case *fullNode: + n.setEpoch(epoch) + } - node := proofList[i] - n, err := decodeNode(nil, node) + parentNode := root + for i := 1; i < len(proofList); i++ { + n, err := decodeNode(nil, proofList[i]) if err != nil { - return nil, fmt.Errorf("decode proof item %#x, err: %v", node, err) - } - - if parentNode == nil { - parentNode = n - root = parentNode + return nil, fmt.Errorf("decode proof item %#x, err: %v", proofList[i], err) } - keyrest, cld := get(n, keyHex, false) - switch cld := cld.(type) { + // verify proof continuous + keyrest, child := get(parentNode, keyHex, false) + switch cld := child.(type) { case nil: return nil, NewKeyDoesNotExistError(keyHex) case hashNode: - keyHex = keyrest - // Verify that the child node is a hashNode and matches the hash in the proof - switch sn := parentNode.(type) { - case *shortNode: - if hash, ok := sn.Val.(hashNode); ok && !bytes.Equal(hash, cld) { - return nil, fmt.Errorf("the child node of shortNode is not a hashNode or doesn't match the hash in the proof") - } - case *fullNode: - if hash, ok := sn.Children[keyHex[0]].(hashNode); ok && !bytes.Equal(hash, cld) { - return nil, fmt.Errorf("the child node of fullNode is not a hashNode or doesn't match the hash in the proof") - } - } - case valueNode: - switch sn := parentNode.(type) { - case *shortNode: - sn.Val = cld - sn.setEpoch(epoch) - return root, nil - case *fullNode: - sn.Children[keyHex[0]] = cld - sn.setEpoch(epoch) - return root, nil + hashed, _ := h.hash(n, false) + if !bytes.Equal(cld, hashed.(hashNode)) { + return nil, fmt.Errorf("the child node of shortNode is not a hashNode or doesn't match the hash in the proof") } + default: + // proof's child cannot contain valueNode/shortNode/fullNode + return nil, fmt.Errorf("worng proof, got unexpect node, fstr: %v", child.fstring("")) + } + + // update epoch + switch n := n.(type) { + case *shortNode: + n.setEpoch(epoch) + case *fullNode: + n.setEpoch(epoch) } // Link the parent and child. switch sn := parentNode.(type) { case *shortNode: sn.Val = n - sn.setEpoch(epoch) - parentNode = n case *fullNode: sn.Children[keyHex[0]] = n - sn.setEpoch(epoch) sn.UpdateChildEpoch(int(keyHex[0]), epoch) - parentNode = n } - } - // Continue traverse down the trie to update the epoch of the child nodes - err := updateEpochInChildNodes(&parentNode, keyHex, epoch) - if err != nil { - return nil, err + // reset + parentNode = n + keyHex = keyrest } return root, nil diff --git a/trie/trie.go b/trie/trie.go index da192090ec..52b45843c0 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -95,7 +95,7 @@ func New(id *ID, db *Database) (*Trie, error) { owner: id.Owner, reader: reader, tracer: newTracer(), - enableExpiry: db.snapTree != nil, + enableExpiry: enableStateExpiry(id, db), } if id.Root != (common.Hash{}) && id.Root != types.EmptyRootHash { rootnode, err := trie.resolveAndTrack(id.Root[:], nil) @@ -116,6 +116,16 @@ func New(id *ID, db *Database) (*Trie, error) { return trie, nil } +func enableStateExpiry(id *ID, db *Database) bool { + if db.snapTree == nil { + return false + } + if id.Owner == (common.Hash{}) { + return false + } + return true +} + // NewEmpty is a shortcut to create empty tree. It's mostly used in tests. func NewEmpty(db *Database) *Trie { tr, _ := New(TrieID(types.EmptyRootHash), db) From a6e4e303ddfa17b78298e88c441a8fe4dcac1f5b Mon Sep 17 00:00:00 2001 From: asyukii Date: Mon, 4 Sep 2023 17:32:12 +0800 Subject: [PATCH 21/65] fix: revive trie use nubs method more fixes Squashed commit of the following: commit 45e7deacede8ebecd2625cc09c609d0b92f96515 Author: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Mon Sep 4 11:50:49 2023 +0800 trie/proof: fix child hash validate bug; bugfix: fix state copy issues, remote cache; --- core/state/database.go | 2 +- core/state/state_expiry.go | 33 +++-- light/trie.go | 2 +- trie/node.go | 33 +++++ trie/proof.go | 274 ++++++++++++++++++++++++++++++++++++- trie/secure_trie.go | 4 +- trie/trie.go | 157 +++++++++++++++------ trie/trie_test.go | 31 ++++- 8 files changed, 470 insertions(+), 66 deletions(-) diff --git a/core/state/database.go b/core/state/database.go index ed764f30f8..acc099ffb6 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -161,7 +161,7 @@ type Trie interface { ProvePath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error // ReviveTrie revive expired state from proof. - ReviveTrie(key []byte, prefixKeyHex []byte, proofList [][]byte) error + ReviveTrie(key []byte, proof []*trie.MPTProofNub) []*trie.MPTProofNub // SetEpoch set current epoch in trie, it must set in initial period, or it will get error behavior. SetEpoch(types.StateEpoch) diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index b0ebfd85ef..41c5556972 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/trie" @@ -35,29 +34,35 @@ func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, blockHash common.Ha // reviveStorageTrie revive trie's expired state from proof func reviveStorageTrie(addr common.Address, tr Trie, proof types.ReviveStorageProof, targetKey common.Hash) ([]byte, error) { + + // Decode keys and proofs key := common.FromHex(proof.Key) if !bytes.Equal(targetKey[:], key) { return nil, fmt.Errorf("revive with wrong key, target: %#x, actual: %#x", targetKey, key) } - prefixKey := common.FromHex(proof.PrefixKey) - proofs := make([][]byte, 0, len(proof.Proof)) - + innerProofs := make([][]byte, 0, len(proof.Proof)) for _, p := range proof.Proof { - proofs = append(proofs, common.FromHex(p)) + innerProofs = append(innerProofs, common.FromHex(p)) } - // TODO(asyukii): support proofs merge, revive in nubs - err := tr.ReviveTrie(crypto.Keccak256(key), prefixKey, proofs) - if err != nil { - return nil, err + proofCache := trie.MPTProofCache{ + MPTProof: trie.MPTProof{ + RootKeyHex: prefixKey, + Proof: innerProofs, + }, } - // Update pending revive state - val, err := tr.GetStorage(addr, key) // TODO(asyukii): may optimize this, return value when revive trie - if err != nil { - return nil, fmt.Errorf("get storage value failed, err: %v", err) + if err := proofCache.VerifyProof(); err != nil { + return nil, err } - return val, nil + nubs := tr.ReviveTrie(key, proofCache.CacheNubs()) + for _, nub := range nubs { + val := nub.GetValue() + if val != nil { + return val, nil + } + } + return nil, nil } diff --git a/light/trie.go b/light/trie.go index 779fd80203..afea904709 100644 --- a/light/trie.go +++ b/light/trie.go @@ -115,7 +115,7 @@ type odrTrie struct { trie *trie.Trie } -func (t *odrTrie) ReviveTrie(key []byte, prefixKeyHex []byte, proofList [][]byte) error { +func (t *odrTrie) ReviveTrie(key []byte, proof []*trie.MPTProofNub) []*trie.MPTProofNub { panic("not implemented") } diff --git a/trie/node.go b/trie/node.go index 19fb2562fe..4e36ef0fd7 100644 --- a/trie/node.go +++ b/trie/node.go @@ -26,12 +26,25 @@ import ( "github.com/ethereum/go-ethereum/rlp" ) +const ( + BranchNodeLength = 17 +) + +const ( + shortNodeType = iota + fullNodeType + hashNodeType + valueNodeType + rawNodeType +) + var indices = []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "[17]"} type node interface { cache() (hashNode, bool) encode(w rlp.EncoderBuffer) fstring(string) string + nodeType() int } type ( @@ -161,6 +174,10 @@ type rawNode []byte func (n rawNode) cache() (hashNode, bool) { panic("this should never end up in a live trie") } func (n rawNode) fstring(ind string) string { panic("this should never end up in a live trie") } +func (n rawNode) nodeType() int { + return rawNodeType +} + func (n rawNode) EncodeRLP(w io.Writer) error { _, err := w.Write(n) return err @@ -171,6 +188,22 @@ func NodeString(hash, buf []byte) string { return node.fstring("NodeString: ") } +func (n *shortNode) nodeType() int { + return shortNodeType +} + +func (n *fullNode) nodeType() int { + return fullNodeType +} + +func (n hashNode) nodeType() int { + return hashNodeType +} + +func (n valueNode) nodeType() int { + return valueNodeType +} + // mustDecodeNode is a wrapper of decodeNode and panic if any error is encountered. func mustDecodeNode(hash, buf []byte) node { n, err := decodeNode(hash, buf) diff --git a/trie/proof.go b/trie/proof.go index 171111e00b..fb5fce9372 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -328,9 +328,9 @@ func updateEpochInChildNodes(tn *node, key []byte, epoch types.StateEpoch) error n.setEpoch(epoch) key = key[1:] - case hashNode: - return fmt.Errorf("cannot resolve hash node") - case valueNode: + case nil, hashNode, valueNode: + *tn = startNode + return nil default: panic(fmt.Sprintf("%T: invalid node: %v", tn, tn)) } @@ -847,3 +847,271 @@ func get(tn node, key []byte, skipResolved bool) ([]byte, node) { } } } + +type MPTProof struct { + RootKeyHex []byte // prefix key in nibbles format, max 65 bytes. TODO: optimize witness size + Proof [][]byte // list of RLP-encoded nodes +} + +type MPTProofNub struct { + n1PrefixKey []byte // n1's prefix hex key, max 64bytes + n1 node + n2PrefixKey []byte // n2's prefix hex key, max 64bytes + n2 node +} + +// ResolveKV revive state could revive KV from fullNode[0-15] or fullNode[16] or shortNode.Val, return KVs for cache & snap +func (m *MPTProofNub) ResolveKV() (map[string][]byte, error) { + kvMap := make(map[string][]byte) + if err := resolveKV(m.n1, m.n1PrefixKey, kvMap); err != nil { + return nil, err + } + if err := resolveKV(m.n2, m.n2PrefixKey, kvMap); err != nil { + return nil, err + } + + return kvMap, nil +} + +func (m *MPTProofNub) GetValue() []byte { + if val := getNubValue(m.n1, m.n1PrefixKey); val != nil { + return val + } + + if val := getNubValue(m.n2, m.n2PrefixKey); val != nil { + return val + } + + return nil +} + +func getNubValue(origin node, prefixKey []byte) []byte { + switch n := origin.(type) { + case nil, hashNode: + return nil + case valueNode: + return n + case *shortNode: + return getNubValue(n.Val, append(prefixKey, n.Key...)) + case *fullNode: + for i := 0; i < BranchNodeLength-1; i++ { + if val := getNubValue(n.Children[i], append(prefixKey, byte(i))); val != nil { + return val + } + } + return getNubValue(n.Children[BranchNodeLength-1], prefixKey) + default: + panic(fmt.Sprintf("invalid node: %v", origin)) + } +} + +func resolveKV(origin node, prefixKey []byte, kvWriter map[string][]byte) error { + switch n := origin.(type) { + case nil, hashNode: + return nil + case valueNode: + kvWriter[string(hexToKeybytes(prefixKey))] = n + return nil + case *shortNode: + return resolveKV(n.Val, append(prefixKey, n.Key...), kvWriter) + case *fullNode: + for i := 0; i < BranchNodeLength-1; i++ { + if err := resolveKV(n.Children[i], append(prefixKey, byte(i)), kvWriter); err != nil { + return err + } + } + return resolveKV(n.Children[BranchNodeLength-1], prefixKey, kvWriter) + default: + panic(fmt.Sprintf("invalid node: %v", origin)) + } +} + +type MPTProofCache struct { + MPTProof + + cacheHexPath [][]byte // cache path for performance + cacheHashes [][]byte // cache hash for performance + cacheNodes []node // cache node for performance + cacheNubs []*MPTProofNub // cache proof nubs to check revive duplicate +} + +// VerifyProof verify proof in MPT witness +// 1. calculate hash +// 2. decode trie node +// 3. verify partial merkle proof of the witness +// 4. split to partial witness +func (m *MPTProofCache) VerifyProof() error { + m.cacheHashes = make([][]byte, len(m.Proof)) + m.cacheNodes = make([]node, len(m.Proof)) + m.cacheHexPath = make([][]byte, len(m.Proof)) + hasher := newHasher(false) + defer returnHasherToPool(hasher) + + var child []byte + for i := len(m.Proof) - 1; i >= 0; i-- { + m.cacheHashes[i] = hasher.hashData(m.Proof[i]) + n, err := decodeNode(m.cacheHashes[i], m.Proof[i]) + if err != nil { + return err + } + m.cacheNodes[i] = n + + switch t := n.(type) { + case *shortNode: + m.cacheHexPath[i] = t.Key + if err := matchHashNodeInShortNode(child, t); err != nil { + return err + } + case *fullNode: + index, err := matchHashNodeInFullNode(child, t) + if err != nil { + return err + } + if index >= 0 { + m.cacheHexPath[i] = []byte{byte(index)} + } + case valueNode: + if child != nil { + return errors.New("proof wrong child in valueNode") + } + default: + return fmt.Errorf("proof got wrong trie node: %v", t.nodeType()) + } + + child = m.cacheHashes[i] + } + + // cache proof nubs + m.cacheNubs = make([]*MPTProofNub, 0, len(m.Proof)) + prefix := m.RootKeyHex + for i := 0; i < len(m.cacheNodes); i++ { + if i-1 >= 0 { + prefix = copyNewSlice(prefix, m.cacheHexPath[i-1]) + } + // prefix = append(prefix, m.cacheHexPath[i]...) + n1 := m.cacheNodes[i] + nub := MPTProofNub{ + n1PrefixKey: prefix, + n1: n1, + n2: nil, + n2PrefixKey: nil, + } + + // check if satisfy partial witness rules, + // that short node must with its child, may full node or valueNode + merge, err := mergeNextNode(m.cacheNodes, i) + if err != nil { + return err + } + if merge { + i++ + prefix = copyNewSlice(prefix, m.cacheHexPath[i-1]) + nub.n2 = m.cacheNodes[i] + nub.n2PrefixKey = prefix + } + m.cacheNubs = append(m.cacheNubs, &nub) + } + + return nil +} + +func copyNewSlice(s1, s2 []byte) []byte { + ret := make([]byte, len(s1)+len(s2)) + copy(ret, s1) + copy(ret[len(s1):], s2) + return ret +} + +func (m *MPTProofCache) CacheNubs() []*MPTProofNub { + return m.cacheNubs +} + +// mergeNextNode check short node must with child in same nub +func mergeNextNode(nodes []node, i int) (bool, error) { + if i >= len(nodes) { + return false, errors.New("mergeNextNode input outbound index") + } + + n1 := nodes[i] + switch n := n1.(type) { + case *shortNode: + need, err := needNextProofNode(n, n.Val) + if err != nil { + return false, err + } + if need && i+1 >= len(nodes) { + return false, errors.New("mergeNextNode short node must with its child") + } + return need, nil + case valueNode: + return false, errors.New("mergeNextNode value node need merge with prev node") + } + + if i+1 >= len(nodes) { + return false, nil + } + return nodes[i+1].nodeType() == valueNodeType, nil +} + +// needNextProofNode check if node need merge next node into a proofNub, because TrieExtendNode must with its child to revive together +func needNextProofNode(parent, origin node) (bool, error) { + switch n := origin.(type) { + case *fullNode: + for i := 0; i < BranchNodeLength-1; i++ { + need, err := needNextProofNode(n, n.Children[i]) + if err != nil { + return false, err + } + if need { + return true, nil + } + } + return false, nil + case *shortNode: + if parent.nodeType() == shortNodeType { + return false, errors.New("needNextProofNode cannot short node's child is short node") + } + return needNextProofNode(n, n.Val) + case valueNode: + return false, nil + case hashNode: + if parent.nodeType() == fullNodeType { + return false, nil + } + return true, nil + default: + return false, errors.New("needNextProofNode unsupported node") + } +} + +func matchHashNodeInFullNode(child []byte, n *fullNode) (int, error) { + if child == nil { + return -1, nil + } + + for i := 0; i < BranchNodeLength-1; i++ { + switch v := n.Children[i].(type) { + case hashNode: + if bytes.Equal(child, v) { + return i, nil + } + } + } + return -1, errors.New("proof cannot find target child in fullNode") +} + +func matchHashNodeInShortNode(child []byte, n *shortNode) error { + if child == nil { + return nil + } + + switch v := n.Val.(type) { + case hashNode: + if !bytes.Equal(child, v) { + return errors.New("proof wrong child in shortNode") + } + default: + return errors.New("proof must hashNode when meet shortNode") + } + return nil +} diff --git a/trie/secure_trie.go b/trie/secure_trie.go index 5382b2a1f6..0abe3db64d 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -316,6 +316,6 @@ func (t *StateTrie) getSecKeyCache() map[string][]byte { return t.secKeyCache } -func (t *StateTrie) ReviveTrie(key []byte, prefixKeyHex []byte, proofList [][]byte) error { - return t.trie.ReviveTrie(key, prefixKeyHex, proofList) +func (t *StateTrie) ReviveTrie(key []byte, proof []*MPTProofNub) []*MPTProofNub { + return t.trie.ReviveTrie(key, proof) } diff --git a/trie/trie.go b/trie/trie.go index 52b45843c0..114562d5f0 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -295,6 +295,42 @@ func (t *Trie) getWithEpoch(origNode node, key []byte, pos int, epoch types.Stat } } +// updateChildNodeEpoch traverses the trie and update the node epoch for each accessed trie node. +// Under an expiry scheme where a hash node is accessed, its parent node's epoch will not be updated. +func (t *Trie) updateChildNodeEpoch(origNode node, key []byte, pos int, epoch types.StateEpoch) (newnode node, updateEpoch bool, err error) { + switch n := (origNode).(type) { + case nil: + return nil, false, nil + case valueNode: + return n, true, nil + case *shortNode: + if len(key)-pos < len(n.Key) || !bytes.Equal(n.Key, key[pos:pos+len(n.Key)]) { + return n, false, nil + } + newnode, updateEpoch, err = t.updateChildNodeEpoch(n.Val, key, pos+len(n.Key), epoch) + if err == nil && updateEpoch { + n = n.copy() + n.Val = newnode + n.setEpoch(t.currentEpoch) + } + + return n, true, err + case *fullNode: + newnode, updateEpoch, err = t.updateChildNodeEpoch(n.Children[key[pos]], key, pos+1, epoch) + if err == nil && updateEpoch { + n = n.copy() + n.Children[key[pos]] = newnode + n.setEpoch(t.currentEpoch) + n.UpdateChildEpoch(int(key[pos]), t.currentEpoch) + } + return n, true, err + case hashNode: + return n, false, err + default: + panic(fmt.Sprintf("%T: invalid node: %v", origNode, origNode)) + } +} + // MustGetNode is a wrapper of GetNode and will omit any encountered error but // just print out an error message. func (t *Trie) MustGetNode(path []byte) ([]byte, int) { @@ -1103,57 +1139,81 @@ func (t *Trie) Owner() common.Hash { return t.owner } -// ReviveTrie revives a trie by prefix key with the given proof list. -func (t *Trie) ReviveTrie(key []byte, prefixKeyHex []byte, proofList [][]byte) error { - +// ReviveTrie attempts to revive a trie from a list of MPTProofNubs. +// ReviveTrie performs full or partial revive and returns a list of successful +// nubs. ReviveTrie does not guarantee that a value will be revived completely, +// if the proof is not fully valid. +func (t *Trie) ReviveTrie(key []byte, proof []*MPTProofNub) (successNubs []*MPTProofNub) { key = keybytesToHex(key) - - // Verify the proof first - revivedNode, revivedHash, err := VerifyPathProof(key, prefixKeyHex, proofList, t.currentEpoch) + successNubs, err := t.TryRevive(key, proof) if err != nil { - return err + log.Error(fmt.Sprintf("Failed to revive trie: %v", err)) } + return successNubs +} - newRoot, didRevive, err := t.revive(t.root, key, prefixKeyHex, 0, revivedNode, common.BytesToHash(revivedHash), t.getRootEpoch(), types.EpochExpired(t.getRootEpoch(), t.currentEpoch)) - if err != nil { - return err - } +func (t *Trie) TryRevive(key []byte, proof []*MPTProofNub) (successNubs []*MPTProofNub, err error) { - if didRevive { - switch n := newRoot.(type) { - case *shortNode: - n.setEpoch(t.currentEpoch) - case *fullNode: - n.setEpoch(t.currentEpoch) - n.UpdateChildEpoch(int(key[0]), t.currentEpoch) + // Revive trie with each proof nub + for _, nub := range proof { + rootExpired := types.EpochExpired(t.getRootEpoch(), t.currentEpoch) + newNode, didRevive, err := t.tryRevive(t.root, key, nub.n1PrefixKey, *nub, 0, t.currentEpoch, rootExpired) + if err != nil { + log.Error("tryRevive err", "prefix", nub.n1PrefixKey, "didRevive", didRevive, "err", err) + } + if didRevive && err == nil { + successNubs = append(successNubs, nub) + t.root = newNode + t.rootEpoch = t.currentEpoch } - t.root = newRoot - t.rootEpoch = t.currentEpoch } - return nil + // If no nubs were successful, return error + if len(successNubs) == 0 && len(proof) != 0 { + return successNubs, fmt.Errorf("all nubs failed to revive trie") + } + + return successNubs, nil } -func (t *Trie) revive(n node, key []byte, prefixKeyHex []byte, pos int, revivedNode node, revivedHash common.Hash, epoch types.StateEpoch, isExpired bool) (node, bool, error) { +func (t *Trie) tryRevive(n node, key []byte, targetPrefixKey []byte, nub MPTProofNub, pos int, epoch types.StateEpoch, isExpired bool) (node, bool, error) { - if pos > len(prefixKeyHex) { + if pos > len(targetPrefixKey) { return nil, false, fmt.Errorf("target revive node not found") } - if pos == len(prefixKeyHex) { + if pos == len(targetPrefixKey) { if !isExpired { return nil, false, fmt.Errorf("target revive node is not expired") } - if hn, ok := n.(hashNode); ok { - // Compare the hash of the revived node with the hash of the hash node - if revivedHash != common.BytesToHash(hn) { - return nil, false, fmt.Errorf("revived node hash does not match the hash node hash") + cachedHash, _ := nub.n1.cache() + if hn, ok := n.(hashNode); ok && !bytes.Equal(cachedHash, hn) { + return nil, false, fmt.Errorf("hash values does not match") + } + + if nub.n2 != nil { + n1, ok := nub.n1.(*shortNode) + if !ok { + return nil, false, fmt.Errorf("invalid node type") } + tryUpdateNodeEpoch(nub.n2, t.currentEpoch) + newnode, _, err := t.updateChildNodeEpoch(nub.n2, key, pos+len(n1.Key), t.currentEpoch) + if err != nil { + return nil, false, fmt.Errorf("update child node epoch while reviving failed, err: %v", err) + } + n1.Val = newnode + return n1, true, nil } - return revivedNode, true, nil + tryUpdateNodeEpoch(nub.n1, t.currentEpoch) + newnode, _, err := t.updateChildNodeEpoch(nub.n1, key, pos, t.currentEpoch) + if err != nil { + return nil, false, fmt.Errorf("update child node epoch while reviving failed, err: %v", err) + } + + return newnode, true, nil } if isExpired { @@ -1163,27 +1223,32 @@ func (t *Trie) revive(n node, key []byte, prefixKeyHex []byte, pos int, revivedN switch n := n.(type) { case *shortNode: if len(key)-pos < len(n.Key) || !bytes.Equal(n.Key, key[pos:pos+len(n.Key)]) { - // key not found in trie - return n, false, nil + return nil, false, fmt.Errorf("key %v not found", key) } - newNode, didRevived, err := t.revive(n.Val, key, prefixKeyHex, pos+len(n.Key), revivedNode, revivedHash, epoch, isExpired) - if err == nil && didRevived { + newNode, didRevive, err := t.tryRevive(n.Val, key, targetPrefixKey, nub, pos+len(n.Key), epoch, isExpired) + if didRevive && err == nil { n = n.copy() n.Val = newNode n.setEpoch(t.currentEpoch) } - return n, didRevived, err + return n, didRevive, err case *fullNode: childIndex := int(key[pos]) - childExpired, _ := n.ChildExpired(key[:pos], childIndex, t.currentEpoch) - newNode, didRevived, err := t.revive(n.Children[childIndex], key, prefixKeyHex, pos+1, revivedNode, revivedHash, n.GetChildEpoch(childIndex), childExpired) - if err == nil && didRevived { + isExpired, _ := n.ChildExpired(nil, childIndex, t.currentEpoch) + newNode, didRevive, err := t.tryRevive(n.Children[childIndex], key, targetPrefixKey, nub, pos+1, epoch, isExpired) + if didRevive && err == nil { n = n.copy() - n.Children[key[pos]] = newNode + n.Children[childIndex] = newNode n.setEpoch(t.currentEpoch) n.UpdateChildEpoch(childIndex, t.currentEpoch) } - return n, didRevived, err + + if e, ok := err.(*ExpiredNodeError); ok { + e.Epoch = n.GetChildEpoch(childIndex) + return n, didRevive, e + } + + return n, didRevive, err case hashNode: child, err := t.resolveAndTrack(n, key[:pos]) if err != nil { @@ -1196,7 +1261,7 @@ func (t *Trie) revive(n node, key []byte, prefixKeyHex []byte, pos int, revivedN } } - newNode, _, err := t.revive(child, key, prefixKeyHex, pos, revivedNode, revivedHash, epoch, isExpired) + newNode, _, err := t.tryRevive(child, key, targetPrefixKey, nub, pos, epoch, isExpired) return newNode, true, err case nil: return nil, false, nil @@ -1210,8 +1275,9 @@ func (t *Trie) revive(n node, key []byte, prefixKeyHex []byte, pos int, revivedN // only a child node of a full node is expired, if not an error is returned. func (t *Trie) ExpireByPrefix(prefixKeyHex []byte) error { hn, _, err := t.expireByPrefix(t.root, prefixKeyHex) - if prefixKeyHex == nil && hn != nil { + if len(prefixKeyHex) == 0 && hn != nil { // whole trie is expired t.root = hn + t.rootEpoch = 0 } if err != nil { return err @@ -1219,6 +1285,15 @@ func (t *Trie) ExpireByPrefix(prefixKeyHex []byte) error { return nil } +func tryUpdateNodeEpoch(origin node, epoch types.StateEpoch) { + switch n := origin.(type) { + case *shortNode: + n.setEpoch(epoch) + case *fullNode: + n.setEpoch(epoch) + } +} + func (t *Trie) expireByPrefix(n node, prefixKeyHex []byte) (node, bool, error) { // Loop through prefix key // When prefix key is empty, generate the hash node of the current node diff --git a/trie/trie_test.go b/trie/trie_test.go index 6e3cde6d0f..b9f09dd641 100644 --- a/trie/trie_test.go +++ b/trie/trie_test.go @@ -1014,9 +1014,15 @@ func TestRevive(t *testing.T) { // Expire trie trie.ExpireByPrefix(prefixKey) + proofCache := makeRawMPTProofCache(prefixKey, proof) + err = proofCache.VerifyProof() + assert.NoError(t, err) + // Revive trie - trie.ReviveTrie(key, prefixKey, proof) + _, err = trie.TryRevive(keybytesToHex(key), proofCache.CacheNubs()) + assert.NoError(t, err, "TryRevive failed, key %x, prefixKey %x, val %x", key, prefixKey, val) + // Verifiy value exists after revive v := trie.MustGet(key) assert.Equal(t, val, v, "value mismatch, got %x, exp %x, key %x, prefixKey %x", v, val, key, prefixKey) @@ -1052,10 +1058,16 @@ func TestReviveCustom(t *testing.T) { trie.ExpireByPrefix(prefixKey) - trie.ReviveTrie(key, prefixKey, proofList) + proofCache := makeRawMPTProofCache(prefixKey, proofList) + err = proofCache.VerifyProof() + assert.NoError(t, err) - v := trie.MustGet(key) - assert.Equal(t, val, v, "value mismatch, got %x, exp %x, key %x, prefixKey %x", v, val, key, prefixKey) + // Revive trie + _, err = trie.TryRevive(keybytesToHex(key), proofCache.cacheNubs) + assert.NoError(t, err, "TryRevive failed, key %x, prefixKey %x, val %x", key, prefixKey, val) + + res := trie.MustGet(key) + assert.Equal(t, val, res, "value mismatch, got %x, exp %x, key %x, prefixKey %x", res, val, key, prefixKey) // Verify root hash currRootHash := trie.Hash() @@ -1071,6 +1083,8 @@ func createCustomTrie(data map[string]string, epoch types.StateEpoch) *Trie { db := NewDatabase(rawdb.NewMemoryDatabase()) trie := NewEmpty(db) trie.rootEpoch = epoch + trie.currentEpoch = epoch + trie.enableExpiry = true for k, v := range data { trie.MustUpdate([]byte(k), []byte(v)) } @@ -1298,3 +1312,12 @@ func TestDecodeNode(t *testing.T) { decodeNode(hash, elems) } } + +func makeRawMPTProofCache(rootKeyHex []byte, proof [][]byte) MPTProofCache { + return MPTProofCache{ + MPTProof: MPTProof{ + RootKeyHex: rootKeyHex, + Proof: proof, + }, + } +} From 58e80abf7826962fd2b2b6ae322716292c9eb86e Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:45:20 +0800 Subject: [PATCH 22/65] core/state: fix revive error, trie prefetcher copy bug; trie: commit account meta, revive state bug; ethdb/fullstatedb: fix cache bug, cannot share by key; --- core/state/state_expiry.go | 23 ++++++++++++++------- core/state/state_object.go | 13 +++++++----- core/state/statedb.go | 4 +++- core/state/trie_prefetcher.go | 3 ++- ethdb/fullstatedb.go | 11 +++++----- trie/dummy_trie.go | 14 +++++++++++++ trie/errors.go | 2 +- trie/proof.go | 10 +++++++-- trie/secure_trie.go | 1 + trie/trie.go | 33 +++++++++++++++++++++-------- trie/trienode/node.go | 39 ++++++++++++++++++++++++----------- 11 files changed, 110 insertions(+), 43 deletions(-) diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index 41c5556972..17c3746bae 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -11,7 +11,7 @@ import ( ) // fetchExpiredStorageFromRemote request expired state from remote full state node; -func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, blockHash common.Hash, addr common.Address, tr Trie, prefixKey []byte, key common.Hash) ([]byte, error) { +func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, blockHash common.Hash, addr common.Address, tr Trie, prefixKey []byte, key common.Hash) (map[string][]byte, error) { // if no prefix, query from revive trie, got the newest expired info if len(prefixKey) == 0 { _, err := tr.GetStorage(addr, key.Bytes()) @@ -33,8 +33,7 @@ func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, blockHash common.Ha } // reviveStorageTrie revive trie's expired state from proof -func reviveStorageTrie(addr common.Address, tr Trie, proof types.ReviveStorageProof, targetKey common.Hash) ([]byte, error) { - +func reviveStorageTrie(addr common.Address, tr Trie, proof types.ReviveStorageProof, targetKey common.Hash) (map[string][]byte, error) { // Decode keys and proofs key := common.FromHex(proof.Key) if !bytes.Equal(targetKey[:], key) { @@ -58,11 +57,21 @@ func reviveStorageTrie(addr common.Address, tr Trie, proof types.ReviveStoragePr } nubs := tr.ReviveTrie(key, proofCache.CacheNubs()) + + // check if it could get from trie + if _, err := tr.GetStorage(addr, key); err != nil { + return nil, err + } + + ret := make(map[string][]byte) for _, nub := range nubs { - val := nub.GetValue() - if val != nil { - return val, nil + kvs, err := nub.ResolveKV() + if err != nil { + return nil, err + } + for k, v := range kvs { + ret[k] = v } } - return nil, nil + return ret, nil } diff --git a/core/state/state_object.go b/core/state/state_object.go index f33ba8690f..9412fc6ee0 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -82,7 +82,7 @@ type stateObject struct { // for state expiry feature pendingReviveTrie Trie // pendingReviveTrie it contains pending revive trie nodes, could update & commit later - pendingReviveState map[string]common.Hash // pendingReviveState for block, when R&W, access revive state first + pendingReviveState map[string]common.Hash // pendingReviveState for block, when R&W, access revive state first, saved in hash key pendingAccessedState map[common.Hash]int // pendingAccessedState record which state is accessed(only read now, update/delete/insert will auto update epoch), it will update epoch index late originStorageEpoch map[common.Hash]types.StateEpoch // originStorageEpoch record origin state epoch, prevent frequency epoch update @@ -784,13 +784,13 @@ func (s *stateObject) queryFromReviveState(reviveState map[string]common.Hash, k // fetchExpiredStorageFromRemote request expired state from remote full state node; func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash) ([]byte, error) { - log.Info("fetchExpiredStorageFromRemote in stateDB", "addr", s.address, "prefixKey", prefixKey, "key", key) tr, err := s.getPendingReviveTrie() if err != nil { return nil, err } - val, err := fetchExpiredStorageFromRemote(s.db.fullStateDB, s.db.originalHash, s.address, tr, prefixKey, key) + log.Info("fetchExpiredStorageFromRemote in stateDB", "addr", s.address, "prefixKey", prefixKey, "key", key, "tr", fmt.Sprintf("%p", tr)) + kvs, err := fetchExpiredStorageFromRemote(s.db.fullStateDB, s.db.originalHash, s.address, tr, prefixKey, key) if err != nil { // Keys may not exist in the trie, so they can't be revived. @@ -799,9 +799,12 @@ func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash) } return nil, fmt.Errorf("revive storage trie failed, err: %v", err) } - s.pendingReviveState[key.String()] = common.BytesToHash(val) + for k, v := range kvs { + s.pendingReviveState[k] = common.BytesToHash(v) + } - return val, nil + val := s.pendingReviveState[string(crypto.Keccak256(key[:]))] + return val.Bytes(), nil } func (s *stateObject) getExpirySnapStorage(key common.Hash) (snapshot.SnapValue, error, error) { diff --git a/core/state/statedb.go b/core/state/statedb.go index 25209d5fe2..4e91be2b39 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -1068,12 +1068,14 @@ func (s *StateDB) copyInternal(doPrefetch bool) *StateDB { state.transientStorage = s.transientStorage.Copy() state.prefetcher = s.prefetcher - if s.prefetcher != nil && !doPrefetch { + if !s.enableStateExpiry && s.prefetcher != nil && !doPrefetch { // If there's a prefetcher running, make an inactive copy of it that can // only access data but does not actively preload (since the user will not // know that they need to explicitly terminate an active copy). + // State Expiry cannot use older prefetcher directly. state.prefetcher = state.prefetcher.copy() } + return state } diff --git a/core/state/trie_prefetcher.go b/core/state/trie_prefetcher.go index c29b1ef297..88dccb6a41 100644 --- a/core/state/trie_prefetcher.go +++ b/core/state/trie_prefetcher.go @@ -17,6 +17,7 @@ package state import ( + "fmt" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" "sync" @@ -583,7 +584,7 @@ func (sf *subfetcher) loop() { if sf.enableStateExpiry { if exErr, match := err.(*trie2.ExpiredNodeError); match { key := common.BytesToHash(task) - log.Info("fetchExpiredStorageFromRemote in trie prefetcher", "addr", sf.addr, "prefixKey", exErr.Path, "key", key) + log.Info("fetchExpiredStorageFromRemote in trie prefetcher", "addr", sf.addr, "prefixKey", exErr.Path, "key", key, "tr", fmt.Sprintf("%p", sf.trie)) _, err = fetchExpiredStorageFromRemote(sf.fullStateDB, sf.blockHash, sf.addr, sf.trie, exErr.Path, key) if err != nil { log.Error("subfetcher fetchExpiredStorageFromRemote err", "addr", sf.addr, "path", exErr.Path, "err", err) diff --git a/ethdb/fullstatedb.go b/ethdb/fullstatedb.go index 8121f0c01f..d737a8a764 100644 --- a/ethdb/fullstatedb.go +++ b/ethdb/fullstatedb.go @@ -57,7 +57,7 @@ func (f *FullStateRPCServer) GetStorageReviveProof(blockHash common.Hash, accoun uncahcedKeys := make([]string, 0, len(keys)) ret := make([]types.ReviveStorageProof, 0, len(keys)) for i, key := range keys { - val, ok := f.cache.Get(proofCacheKey(blockHash, account, key)) + val, ok := f.cache.Get(proofCacheKey(blockHash, account, prefixKeys[i], key)) log.Info("GetStorageReviveProof hit cache", "account", account, "key", key, "ok", ok) if !ok { uncahcedPrefixKeys = append(uncahcedPrefixKeys, prefixKeys[i]) @@ -78,20 +78,21 @@ func (f *FullStateRPCServer) GetStorageReviveProof(blockHash common.Hash, accoun // add to cache for _, proof := range proofs { - log.Info("GetStorageReviveProof cache", "account", account, "key", proof.Key) - f.cache.Add(proofCacheKey(blockHash, account, proof.Key), proof) + f.cache.Add(proofCacheKey(blockHash, account, proof.PrefixKey, proof.Key), proof) } ret = append(ret, proofs...) return ret, err } -func proofCacheKey(blockHash common.Hash, account common.Address, key string) string { - buf := bytes.NewBuffer(make([]byte, 0, 66+len(key))) +func proofCacheKey(blockHash common.Hash, account common.Address, prefix, key string) string { + buf := bytes.NewBuffer(make([]byte, 0, 67+len(prefix)+len(key))) buf.Write(blockHash[:]) buf.WriteByte('$') buf.Write(account[:]) buf.WriteByte('$') + buf.WriteString(common.No0xPrefix(prefix)) + buf.WriteByte('$') buf.WriteString(common.No0xPrefix(key)) return buf.String() } diff --git a/trie/dummy_trie.go b/trie/dummy_trie.go index 41478253d9..052c9e6689 100644 --- a/trie/dummy_trie.go +++ b/trie/dummy_trie.go @@ -81,6 +81,20 @@ func (t *EmptyTrie) NodeIterator(startKey []byte) (NodeIterator, error) { func (t *EmptyTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error { return nil } +func (t *EmptyTrie) GetStorageAndUpdateEpoch(addr common.Address, key []byte) ([]byte, error) { + return nil, nil +} + +func (t *EmptyTrie) ProvePath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error { + return nil +} + +func (t *EmptyTrie) ReviveTrie(key []byte, proof []*MPTProofNub) []*MPTProofNub { + return nil +} + +func (t *EmptyTrie) SetEpoch(epoch types.StateEpoch) { +} // Copy returns a copy of SecureTrie. func (t *EmptyTrie) Copy() *EmptyTrie { diff --git a/trie/errors.go b/trie/errors.go index 33f114ade3..024875f0ae 100644 --- a/trie/errors.go +++ b/trie/errors.go @@ -65,7 +65,7 @@ func NewExpiredNodeError(path []byte, epoch types.StateEpoch) error { } func (err *ExpiredNodeError) Error() string { - return "expired trie node" + return fmt.Sprintf("expired trie node, path: %v, epoch: %v", err.Path, err.Epoch) } type KeyDoesNotExistError struct { diff --git a/trie/proof.go b/trie/proof.go index fb5fce9372..a6ba41586b 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -20,6 +20,7 @@ import ( "bytes" "errors" "fmt" + "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -890,7 +891,8 @@ func getNubValue(origin node, prefixKey []byte) []byte { case nil, hashNode: return nil case valueNode: - return n + _, content, _, _ := rlp.Split(n) + return content case *shortNode: return getNubValue(n.Val, append(prefixKey, n.Key...)) case *fullNode: @@ -910,7 +912,11 @@ func resolveKV(origin node, prefixKey []byte, kvWriter map[string][]byte) error case nil, hashNode: return nil case valueNode: - kvWriter[string(hexToKeybytes(prefixKey))] = n + _, content, _, err := rlp.Split(n) + if err != nil { + return err + } + kvWriter[string(hexToKeybytes(prefixKey))] = content return nil case *shortNode: return resolveKV(n.Val, append(prefixKey, n.Key...), kvWriter) diff --git a/trie/secure_trie.go b/trie/secure_trie.go index 0abe3db64d..05852b1c60 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -317,5 +317,6 @@ func (t *StateTrie) getSecKeyCache() map[string][]byte { } func (t *StateTrie) ReviveTrie(key []byte, proof []*MPTProofNub) []*MPTProofNub { + key = t.hashKey(key) return t.trie.ReviveTrie(key, proof) } diff --git a/trie/trie.go b/trie/trie.go index 114562d5f0..d52409cd2c 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -97,20 +97,25 @@ func New(id *ID, db *Database) (*Trie, error) { tracer: newTracer(), enableExpiry: enableStateExpiry(id, db), } + // resolve root epoch + if trie.enableExpiry { + meta, err := reader.accountMeta() + if err != nil { + return nil, err + } + trie.rootEpoch = meta.Epoch() + if id.Root != (common.Hash{}) && id.Root != types.EmptyRootHash { + trie.root = hashNode(id.Root[:]) + } + return trie, nil + } + if id.Root != (common.Hash{}) && id.Root != types.EmptyRootHash { rootnode, err := trie.resolveAndTrack(id.Root[:], nil) if err != nil { return nil, err } trie.root = rootnode - // resolve root epoch - if trie.enableExpiry { - meta, err := reader.accountMeta() - if err != nil { - return nil, err - } - trie.rootEpoch = meta.Epoch() - } } return trie, nil @@ -1102,6 +1107,12 @@ func (t *Trie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet, error) for _, path := range t.tracer.deletedBranchNodes() { nodes.AddBranchNodeEpochMeta([]byte(path), nil) } + // store state expiry account meta + if t.enableExpiry { + if err := nodes.AddAccountMeta(types.NewMetaNoConsensus(t.rootEpoch)); err != nil { + return common.Hash{}, nil, err + } + } t.root = newCommitter(nodes, t.tracer, collectLeaf, t.enableExpiry).Commit(t.root) return rootHash, nodes, nil } @@ -1187,9 +1198,13 @@ func (t *Trie) tryRevive(n node, key []byte, targetPrefixKey []byte, nub MPTProo if !isExpired { return nil, false, fmt.Errorf("target revive node is not expired") } + hn, ok := n.(hashNode) + if !ok { + return nil, false, fmt.Errorf("not match hashNode stub") + } cachedHash, _ := nub.n1.cache() - if hn, ok := n.(hashNode); ok && !bytes.Equal(cachedHash, hn) { + if !bytes.Equal(cachedHash, hn) { return nil, false, fmt.Errorf("hash values does not match") } diff --git a/trie/trienode/node.go b/trie/trienode/node.go index 1127a511df..e23216995e 100644 --- a/trie/trienode/node.go +++ b/trie/trienode/node.go @@ -18,6 +18,7 @@ package trienode import ( "fmt" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie/epochmeta" "sort" @@ -61,21 +62,21 @@ type leaf struct { // NodeSet contains a set of nodes collected during the commit operation. // Each node is keyed by path. It's not thread-safe to use. type NodeSet struct { - Owner common.Hash - Leaves []*leaf - Nodes map[string]*Node - BranchNodeEpochMetas map[string][]byte - updates int // the count of updated and inserted nodes - deletes int // the count of deleted nodes + Owner common.Hash + Leaves []*leaf + Nodes map[string]*Node + EpochMetas map[string][]byte + updates int // the count of updated and inserted nodes + deletes int // the count of deleted nodes } // NewNodeSet initializes a node set. The owner is zero for the account trie and // the owning account address hash for storage tries. func NewNodeSet(owner common.Hash) *NodeSet { return &NodeSet{ - Owner: owner, - Nodes: make(map[string]*Node), - BranchNodeEpochMetas: make(map[string][]byte), + Owner: owner, + Nodes: make(map[string]*Node), + EpochMetas: make(map[string][]byte), } } @@ -106,12 +107,26 @@ func (set *NodeSet) AddNode(path []byte, n *Node) { // AddBranchNodeEpochMeta adds the provided epoch meta into set. func (set *NodeSet) AddBranchNodeEpochMeta(path []byte, meta *epochmeta.BranchNodeEpochMeta) { if meta == nil || *meta == (epochmeta.BranchNodeEpochMeta{}) { - set.BranchNodeEpochMetas[string(path)] = []byte{} + set.EpochMetas[string(path)] = []byte{} return } buf := rlp.NewEncoderBuffer(nil) meta.Encode(buf) - set.BranchNodeEpochMetas[string(path)] = buf.ToBytes() + set.EpochMetas[string(path)] = buf.ToBytes() +} + +// AddAccountMeta adds the provided account into set. +func (set *NodeSet) AddAccountMeta(meta types.StateMeta) error { + if meta == nil { + set.EpochMetas[epochmeta.AccountMetadataPath] = []byte{} + return nil + } + enc, err := meta.EncodeToRLPBytes() + if err != nil { + return err + } + set.EpochMetas[epochmeta.AccountMetadataPath] = enc + return nil } // Merge adds a set of nodes into the set. @@ -216,7 +231,7 @@ func (set *MergedNodeSet) Flatten() map[common.Hash]map[string]*Node { func (set *MergedNodeSet) FlattenEpochMeta() map[common.Hash]map[string][]byte { nodes := make(map[common.Hash]map[string][]byte) for owner, set := range set.Sets { - nodes[owner] = set.BranchNodeEpochMetas + nodes[owner] = set.EpochMetas } return nodes } From e0de4ac87715ea2036c1d491316b21e8867332af Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Thu, 7 Sep 2023 13:53:07 +0800 Subject: [PATCH 23/65] logs: opt some state expiry logs level to debug; pruner: fix some epoch meta db error; metrics: add more metrics for state expiry; --- cmd/geth/snapshot.go | 17 +++++-- core/blockchain.go | 7 +++ core/rawdb/database.go | 14 ++++++ core/state/database.go | 2 +- core/state/pruner/pruner.go | 84 +++++++++++++++++++++++++------ core/state/snapshot/conversion.go | 2 +- core/state/state_expiry.go | 7 ++- core/state/state_object.go | 6 ++- core/state/statedb.go | 9 +++- core/state/trie_prefetcher.go | 2 +- ethdb/fullstatedb.go | 10 +++- light/trie.go | 2 +- trie/database.go | 4 ++ trie/epochmeta/difflayer.go | 18 +++---- trie/epochmeta/difflayer_test.go | 5 ++ trie/errors.go | 16 ++++++ trie/secure_trie.go | 2 +- trie/trie.go | 37 +++++++------- 18 files changed, 188 insertions(+), 56 deletions(-) diff --git a/cmd/geth/snapshot.go b/cmd/geth/snapshot.go index 0d9e583e79..54ef78076a 100644 --- a/cmd/geth/snapshot.go +++ b/cmd/geth/snapshot.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/ethereum/go-ethereum/core" "os" "path/filepath" "time" @@ -29,7 +30,6 @@ import ( "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state/pruner" @@ -61,6 +61,8 @@ var ( Flags: flags.Merge([]cli.Flag{ utils.BloomFilterSizeFlag, utils.TriesInMemoryFlag, + utils.StateExpiryEnableFlag, + configFileFlag, }, utils.NetworkFlags, utils.DatabasePathFlags), Description: ` geth snapshot prune-state @@ -411,18 +413,25 @@ func pruneBlock(ctx *cli.Context) error { // Deprecation: this command should be deprecated once the hash-based // scheme is deprecated. func pruneState(ctx *cli.Context) error { - stack, _ := makeConfigNode(ctx) + stack, cfg := makeConfigNode(ctx) defer stack.Close() chaindb := utils.MakeChainDatabase(ctx, stack, false, false) defer chaindb.Close() + chainConfig, _, err := core.LoadChainConfig(chaindb, cfg.Eth.Genesis) + if err != nil { + return err + } + if rawdb.ReadStateScheme(chaindb) != rawdb.HashScheme { log.Crit("Offline pruning is not required for path scheme") } prunerconfig := pruner.Config{ - Datadir: stack.ResolvePath(""), - BloomSize: ctx.Uint64(utils.BloomFilterSizeFlag.Name), + Datadir: stack.ResolvePath(""), + BloomSize: ctx.Uint64(utils.BloomFilterSizeFlag.Name), + EnableStateExpiry: cfg.Eth.StateExpiryEnable, + ChainConfig: chainConfig, } pruner, err := pruner.NewPruner(chaindb, prunerconfig, ctx.Uint64(utils.TriesInMemoryFlag.Name)) if err != nil { diff --git a/core/blockchain.go b/core/blockchain.go index d9d1202b36..7a918a0195 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1200,6 +1200,13 @@ func (bc *BlockChain) Stop() { log.Error("Failed to journal state snapshot", "err", err) } } + + epochMetaSnapTree := bc.triedb.EpochMetaSnapTree() + if epochMetaSnapTree != nil { + if err := epochMetaSnapTree.Journal(); err != nil { + log.Error("Failed to journal epochMetaSnapTree", "err", err) + } + } if bc.triedb.Scheme() == rawdb.PathScheme { // Ensure that the in-memory trie nodes are journaled to disk properly. if err := bc.triedb.Journal(bc.CurrentBlock().Root); err != nil { diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 40a034923d..d889796b72 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -642,6 +642,11 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { metadata stat unaccounted stat + // state expiry statistics + epochMetaMetaSize stat + epochMetaSnapJournalSize stat + epochMetaPlainStateSize stat + // Totals total common.StorageSize ) @@ -703,6 +708,12 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { bytes.HasPrefix(key, BloomTrieIndexPrefix) || bytes.HasPrefix(key, BloomTriePrefix): // Bloomtrie sub bloomTrieNodes.Add(size) + case bytes.Equal(key, epochMetaPlainStateMeta): + epochMetaMetaSize.Add(size) + case bytes.Equal(key, epochMetaSnapshotJournalKey): + epochMetaSnapJournalSize.Add(size) + case bytes.HasPrefix(key, EpochMetaPlainStatePrefix) && len(key) >= (len(EpochMetaPlainStatePrefix)+common.HashLength): + epochMetaPlainStateSize.Add(size) default: var accounted bool for _, meta := range [][]byte{ @@ -751,6 +762,9 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { {"Key-Value store", "Singleton metadata", metadata.Size(), metadata.Count()}, {"Light client", "CHT trie nodes", chtTrieNodes.Size(), chtTrieNodes.Count()}, {"Light client", "Bloom trie nodes", bloomTrieNodes.Size(), bloomTrieNodes.Count()}, + {"State Expiry", "Epoch Metadata", epochMetaMetaSize.Size(), epochMetaMetaSize.Count()}, + {"State Expiry", "EpochMeta KV", epochMetaPlainStateSize.Size(), epochMetaPlainStateSize.Count()}, + {"State Expiry", "EpochMeta Snap Journal", epochMetaSnapJournalSize.Size(), epochMetaSnapJournalSize.Count()}, } // Inspect all registered append-only file store then. ancients, err := inspectFreezers(db) diff --git a/core/state/database.go b/core/state/database.go index acc099ffb6..e480315e2e 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -161,7 +161,7 @@ type Trie interface { ProvePath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error // ReviveTrie revive expired state from proof. - ReviveTrie(key []byte, proof []*trie.MPTProofNub) []*trie.MPTProofNub + ReviveTrie(key []byte, proof []*trie.MPTProofNub) ([]*trie.MPTProofNub, error) // SetEpoch set current epoch in trie, it must set in initial period, or it will get error behavior. SetEpoch(types.StateEpoch) diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go index 828dd96a46..a8a05b6e97 100644 --- a/core/state/pruner/pruner.go +++ b/core/state/pruner/pruner.go @@ -64,8 +64,10 @@ const ( // Config includes all the configurations for pruning. type Config struct { - Datadir string // The directory of the state database - BloomSize uint64 // The Megabytes of memory allocated to bloom-filter + Datadir string // The directory of the state database + BloomSize uint64 // The Megabytes of memory allocated to bloom-filter + EnableStateExpiry bool + ChainConfig *params.ChainConfig } // Pruner is an offline tool to prune the stale state with the @@ -86,7 +88,6 @@ type Pruner struct { stateBloom *stateBloom snaptree *snapshot.Tree triesInMemory uint64 - chainConfig *params.ChainConfig } type BlockPruner struct { @@ -105,11 +106,8 @@ func NewPruner(db ethdb.Database, config Config, triesInMemory uint64) (*Pruner, } // Offline pruning is only supported in legacy hash based scheme. triedb := trie.NewDatabase(db, trie.HashDefaults) + log.Info("ChainConfig", "headBlock", headBlock.NumberU64(), "config", config.ChainConfig) - chainConfig := rawdb.ReadChainConfig(db, headBlock.Hash()) - if chainConfig == nil { - return nil, errors.New("failed to load chainConfig") - } snapconfig := snapshot.Config{ CacheSize: 256, Recovery: false, @@ -136,7 +134,6 @@ func NewPruner(db ethdb.Database, config Config, triesInMemory uint64) (*Pruner, stateBloom: stateBloom, snaptree: snaptree, triesInMemory: triesInMemory, - chainConfig: chainConfig, }, nil } @@ -642,9 +639,14 @@ func (p *Pruner) Prune(root common.Hash) error { middleRoots[layer.Root()] = struct{}{} } - pruneExpiredCh := make(chan *snapshot.ContractItem, 100000) - epoch := types.GetStateEpoch(p.chainConfig, p.chainHeader.Number) - go asyncPruneExpired(p.db, root, epoch, pruneExpiredCh) + var pruneExpiredCh chan *snapshot.ContractItem + var expireRetCh chan error + if p.config.EnableStateExpiry { + pruneExpiredCh = make(chan *snapshot.ContractItem, 100000) + expireRetCh = make(chan error, 1) + epoch := types.GetStateEpoch(p.config.ChainConfig, p.chainHeader.Number) + go asyncPruneExpired(p.db, root, epoch, pruneExpiredCh, expireRetCh) + } // Traverse the target state, re-construct the whole state trie and // commit to the given bloom filter. start := time.Now() @@ -663,20 +665,30 @@ func (p *Pruner) Prune(root common.Hash) error { return err } log.Info("State bloom filter committed", "name", filterName) - return prune(p.snaptree, root, p.db, p.stateBloom, filterName, middleRoots, start) + err = prune(p.snaptree, root, p.db, p.stateBloom, filterName, middleRoots, start) + + // wait expired result + if expireRetCh != nil { + expireErr := <-expireRetCh + if expireErr != nil { + return expireErr + } + } + return err } // asyncPruneExpired prune trie expired state // TODO(0xbundler): here are some issues when just delete it from hash-based storage, because it's shared kv in hbss // but it's ok for pbss. -func asyncPruneExpired(diskdb ethdb.Database, stateRoot common.Hash, currentEpoch types.StateEpoch, expireRootCh chan *snapshot.ContractItem) { +func asyncPruneExpired(diskdb ethdb.Database, stateRoot common.Hash, currentEpoch types.StateEpoch, expireRootCh chan *snapshot.ContractItem, expirePruneCh chan error) { db := trie.NewDatabaseWithConfig(diskdb, &trie.Config{ EnableStateExpiry: true, PathDB: nil, // TODO(0xbundler): support later }) pruneItemCh := make(chan *trie.NodeInfo, 100000) - go asyncPruneExpiredStorageInDisk(diskdb, pruneItemCh) + pruneDiskRet := make(chan error, 1) + go asyncPruneExpiredStorageInDisk(diskdb, pruneItemCh, pruneDiskRet) for item := range expireRootCh { log.Info("start scan trie expired state", "addr", item.Addr, "root", item.Root) tr, err := trie.New(&trie.ID{ @@ -693,9 +705,35 @@ func asyncPruneExpired(diskdb ethdb.Database, stateRoot common.Hash, currentEpoc log.Error("asyncPruneExpired, PruneExpired err", "id", item, "err", err) } } + + err := <-pruneDiskRet + if err != nil { + log.Error("asyncPruneExpired, pruneDiskRet err", "err", err) + expirePruneCh <- err + return + } + + st := db.EpochMetaSnapTree() + if st != nil { + if err := st.Journal(); err != nil { + log.Error("asyncPruneExpired, SnapTree Journal err", "err", err) + expirePruneCh <- err + return + } + } } -func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, expiredCh chan *trie.NodeInfo) { +func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, expiredCh chan *trie.NodeInfo, ret chan error) { + var ( + trieCount = 0 + trieSize common.StorageSize + snapCount = 0 + snapSize common.StorageSize + epochMetaCount = 0 + epochMetaSize common.StorageSize + start = time.Now() + logged = time.Now() + ) batch := diskdb.NewBatch() for info := range expiredCh { log.Info("found expired state", "addr", info.Addr, "path", @@ -703,13 +741,19 @@ func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, expiredCh chan *trie. info.IsBranch, "isLeaf", info.IsLeaf) addr := info.Addr // delete trie kv + trieCount++ + trieSize += common.StorageSize(len(info.Key) + 32) rawdb.DeleteTrieNode(batch, addr, info.Path, info.Hash, rawdb.HashScheme) // delete epoch meta if info.IsBranch { + epochMetaCount++ + epochMetaSize += common.StorageSize(32 + len(info.Path) + 32) rawdb.DeleteEpochMetaPlainState(batch, addr, string(info.Path)) } // replace snapshot kv only epoch if info.IsLeaf { + snapCount++ + snapSize += common.StorageSize(32) if err := snapshot.ShrinkExpiredLeaf(batch, addr, info.Key, info.Epoch); err != nil { log.Error("ShrinkExpiredLeaf err", "addr", addr, "key", info.Key, "err", err) } @@ -718,11 +762,21 @@ func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, expiredCh chan *trie. batch.Write() batch.Reset() } + if time.Since(logged) > 8*time.Second { + log.Info("Pruning expired states", "trieNodes", trieCount, "trieSize", trieSize, + "SnapKV", snapCount, "SnapKVSize", snapSize, "EpochMeta", epochMetaCount, + "EpochMetaSize", epochMetaSize) + logged = time.Now() + } } if batch.ValueSize() > 0 { batch.Write() batch.Reset() } + log.Info("Pruned expired states", "trieNodes", trieCount, "trieSize", trieSize, + "SnapKV", snapCount, "SnapKVSize", snapSize, "EpochMeta", epochMetaCount, + "EpochMetaSize", epochMetaSize, "elapsed", common.PrettyDuration(time.Since(start))) + ret <- nil } // RecoverPruning will resume the pruning procedure during the system restart. diff --git a/core/state/snapshot/conversion.go b/core/state/snapshot/conversion.go index 6c4d77990b..ac11e1cd5e 100644 --- a/core/state/snapshot/conversion.go +++ b/core/state/snapshot/conversion.go @@ -337,7 +337,7 @@ func generateTrieRoot(db ethdb.KeyValueWriter, scheme string, it Iterator, accou // async prune trie expired states if pruneExpiredCh != nil { pruneExpiredCh <- &ContractItem{ - Addr: it.Hash(), + Addr: hash, Root: account.Root, } } diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index 17c3746bae..d48f3591db 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -56,10 +56,13 @@ func reviveStorageTrie(addr common.Address, tr Trie, proof types.ReviveStoragePr return nil, err } - nubs := tr.ReviveTrie(key, proofCache.CacheNubs()) + nubs, err := tr.ReviveTrie(key, proofCache.CacheNubs()) + if err != nil { + return nil, err + } // check if it could get from trie - if _, err := tr.GetStorage(addr, key); err != nil { + if _, err = tr.GetStorage(addr, key); err != nil { return nil, err } diff --git a/core/state/state_object.go b/core/state/state_object.go index 9412fc6ee0..eb506c172c 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -232,6 +232,7 @@ func (s *stateObject) setOriginStorage(key common.Hash, value common.Hash) { // GetCommittedState retrieves a value from the committed account storage trie. func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { + getCommittedStorageMeter.Mark(1) // If we have a pending write or clean cached, return that if value, pending := s.pendingStorage[key]; pending { return value @@ -268,6 +269,7 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { value common.Hash ) if s.db.snap != nil { + getCommittedStorageSnapMeter.Mark(1) start := time.Now() // handle state expiry situation if s.db.EnableExpire() { @@ -299,6 +301,7 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { // If the snapshot is unavailable or reading from it fails, load from the database. if s.needLoadFromTrie(err, sv) { + getCommittedStorageTrieMeter.Mark(1) start := time.Now() tr, err := s.getTrie() if err != nil { @@ -789,7 +792,7 @@ func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash) return nil, err } - log.Info("fetchExpiredStorageFromRemote in stateDB", "addr", s.address, "prefixKey", prefixKey, "key", key, "tr", fmt.Sprintf("%p", tr)) + log.Debug("fetchExpiredStorageFromRemote in stateDB", "addr", s.address, "prefixKey", prefixKey, "key", key, "tr", fmt.Sprintf("%p", tr)) kvs, err := fetchExpiredStorageFromRemote(s.db.fullStateDB, s.db.originalHash, s.address, tr, prefixKey, key) if err != nil { @@ -803,6 +806,7 @@ func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash) s.pendingReviveState[k] = common.BytesToHash(v) } + getCommittedStorageRemoteMeter.Mark(1) val := s.pendingReviveState[string(crypto.Keccak256(key[:]))] return val.Bytes(), nil } diff --git a/core/state/statedb.go b/core/state/statedb.go index 4e91be2b39..7acddabc9f 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -44,6 +44,13 @@ import ( const defaultNumOfSlots = 100 +var ( + getCommittedStorageMeter = metrics.NewRegisteredMeter("state/contract/committed", nil) + getCommittedStorageSnapMeter = metrics.NewRegisteredMeter("state/contract/committed/snap", nil) + getCommittedStorageTrieMeter = metrics.NewRegisteredMeter("state/contract/committed/trie", nil) + getCommittedStorageRemoteMeter = metrics.NewRegisteredMeter("state/contract/committed/remote", nil) +) + type revision struct { id int journalIndex int @@ -249,7 +256,7 @@ func (s *StateDB) InitStateExpiryFeature(config *params.ChainConfig, remote ethd s.fullStateDB = remote s.epoch = types.GetStateEpoch(config, expectHeight) s.originalHash = startAtBlockHash - log.Info("StateDB enable state expiry feature", "expectHeight", expectHeight, "startAtBlockHash", startAtBlockHash, "epoch", s.epoch) + log.Debug("StateDB enable state expiry feature", "expectHeight", expectHeight, "startAtBlockHash", startAtBlockHash, "epoch", s.epoch) return s } diff --git a/core/state/trie_prefetcher.go b/core/state/trie_prefetcher.go index 88dccb6a41..633dbba999 100644 --- a/core/state/trie_prefetcher.go +++ b/core/state/trie_prefetcher.go @@ -584,7 +584,7 @@ func (sf *subfetcher) loop() { if sf.enableStateExpiry { if exErr, match := err.(*trie2.ExpiredNodeError); match { key := common.BytesToHash(task) - log.Info("fetchExpiredStorageFromRemote in trie prefetcher", "addr", sf.addr, "prefixKey", exErr.Path, "key", key, "tr", fmt.Sprintf("%p", sf.trie)) + log.Debug("fetchExpiredStorageFromRemote in trie prefetcher", "addr", sf.addr, "prefixKey", exErr.Path, "key", key, "tr", fmt.Sprintf("%p", sf.trie)) _, err = fetchExpiredStorageFromRemote(sf.fullStateDB, sf.blockHash, sf.addr, sf.trie, exErr.Path, key) if err != nil { log.Error("subfetcher fetchExpiredStorageFromRemote err", "addr", sf.addr, "path", exErr.Path, "err", err) diff --git a/ethdb/fullstatedb.go b/ethdb/fullstatedb.go index d737a8a764..26170e2126 100644 --- a/ethdb/fullstatedb.go +++ b/ethdb/fullstatedb.go @@ -7,12 +7,18 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/rpc" lru "github.com/hashicorp/golang-lru" "strings" "time" ) +var ( + getProofMeter = metrics.NewRegisteredMeter("ethdb/fullstatedb/getproof", nil) + getProofHitCacheMeter = metrics.NewRegisteredMeter("ethdb/fullstatedb/getproof/cache", nil) +) + // FullStateDB expired state could fetch from it type FullStateDB interface { // GetStorageReviveProof fetch target proof according to specific params @@ -52,18 +58,20 @@ func NewFullStateRPCServer(endpoint string) (FullStateDB, error) { } func (f *FullStateRPCServer) GetStorageReviveProof(blockHash common.Hash, account common.Address, prefixKeys, keys []string) ([]types.ReviveStorageProof, error) { + getProofMeter.Mark(1) // find from lru cache, now it cache key proof uncahcedPrefixKeys := make([]string, 0, len(prefixKeys)) uncahcedKeys := make([]string, 0, len(keys)) ret := make([]types.ReviveStorageProof, 0, len(keys)) for i, key := range keys { val, ok := f.cache.Get(proofCacheKey(blockHash, account, prefixKeys[i], key)) - log.Info("GetStorageReviveProof hit cache", "account", account, "key", key, "ok", ok) + log.Debug("GetStorageReviveProof hit cache", "account", account, "key", key, "ok", ok) if !ok { uncahcedPrefixKeys = append(uncahcedPrefixKeys, prefixKeys[i]) uncahcedKeys = append(uncahcedKeys, keys[i]) continue } + getProofHitCacheMeter.Mark(1) ret = append(ret, val.(types.ReviveStorageProof)) } diff --git a/light/trie.go b/light/trie.go index afea904709..d8b23e7667 100644 --- a/light/trie.go +++ b/light/trie.go @@ -115,7 +115,7 @@ type odrTrie struct { trie *trie.Trie } -func (t *odrTrie) ReviveTrie(key []byte, proof []*trie.MPTProofNub) []*trie.MPTProofNub { +func (t *odrTrie) ReviveTrie(key []byte, proof []*trie.MPTProofNub) ([]*trie.MPTProofNub, error) { panic("not implemented") } diff --git a/trie/database.go b/trie/database.go index a12b140457..f5584a8f1e 100644 --- a/trie/database.go +++ b/trie/database.go @@ -377,3 +377,7 @@ func (db *Database) SetBufferSize(size int) error { } return pdb.SetBufferSize(size) } + +func (db *Database) EpochMetaSnapTree() *epochmeta.SnapshotTree { + return db.snapTree +} diff --git a/trie/epochmeta/difflayer.go b/trie/epochmeta/difflayer.go index 85a4cb5608..f314c79c55 100644 --- a/trie/epochmeta/difflayer.go +++ b/trie/epochmeta/difflayer.go @@ -82,7 +82,7 @@ func NewEpochMetaSnapTree(diskdb ethdb.KeyValueStore) (*SnapshotTree, error) { func (s *SnapshotTree) Cap(blockRoot common.Hash) error { snap := s.Snapshot(blockRoot) if snap == nil { - return errors.New("snapshot missing") + return fmt.Errorf("epoch meta snapshot missing: [%#x]", blockRoot) } nextDiff, ok := snap.(*diffLayer) if !ok { @@ -384,14 +384,12 @@ func (s *diffLayer) Journal(buffer *bytes.Buffer) (common.Hash, error) { return common.Hash{}, err } - if s.parent != nil { - if err := rlp.Encode(buffer, s.parent.Root()); err != nil { - return common.Hash{}, err - } - } else { - if err := rlp.Encode(buffer, types.EmptyRootHash); err != nil { - return common.Hash{}, err - } + if s.parent == nil { + return common.Hash{}, errors.New("found nil parent in Journal") + } + + if err := rlp.Encode(buffer, s.parent.Root()); err != nil { + return common.Hash{}, err } if err := rlp.Encode(buffer, s.blockRoot); err != nil { @@ -556,7 +554,7 @@ func (s *diskLayer) writeHistory(number *big.Int, batch ethdb.Batch, nodeSet map } } } - log.Info("shadow node history pruned, only keep plainState", "number", number, "count", len(nodeSet)) + log.Debug("shadow node history pruned, only keep plainState", "number", number, "count", len(nodeSet)) return nil } diff --git a/trie/epochmeta/difflayer_test.go b/trie/epochmeta/difflayer_test.go index 52c134272f..6a0f61fc9f 100644 --- a/trie/epochmeta/difflayer_test.go +++ b/trie/epochmeta/difflayer_test.go @@ -219,6 +219,11 @@ func TestEpochMetaDiffLayer_capDiffLayers(t *testing.T) { // store err = tree.Journal() assert.NoError(t, err) + + tree, err = NewEpochMetaSnapTree(diskdb) + assert.NoError(t, err) + assert.Equal(t, 129, len(tree.layers)) + assert.Equal(t, 128, len(tree.children)) } func makeHash(s string) common.Hash { diff --git a/trie/errors.go b/trie/errors.go index 024875f0ae..e6d61f0228 100644 --- a/trie/errors.go +++ b/trie/errors.go @@ -52,6 +52,22 @@ func (err *MissingNodeError) Error() string { return fmt.Sprintf("missing trie node %x (owner %x) (path %x) %v", err.NodeHash, err.Owner, err.Path, err.err) } +type ReviveNotExpiredError struct { + Path []byte // hex-encoded path to the expired node + Epoch types.StateEpoch +} + +func NewReviveNotExpiredErr(path []byte, epoch types.StateEpoch) error { + return &ReviveNotExpiredError{ + Path: path, + Epoch: epoch, + } +} + +func (e *ReviveNotExpiredError) Error() string { + return fmt.Sprintf("revive not expired kv, path: %v, epoch: %v", e.Path, e.Epoch) +} + type ExpiredNodeError struct { Path []byte // hex-encoded path to the expired node Epoch types.StateEpoch diff --git a/trie/secure_trie.go b/trie/secure_trie.go index 05852b1c60..a2c0f5dccd 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -316,7 +316,7 @@ func (t *StateTrie) getSecKeyCache() map[string][]byte { return t.secKeyCache } -func (t *StateTrie) ReviveTrie(key []byte, proof []*MPTProofNub) []*MPTProofNub { +func (t *StateTrie) ReviveTrie(key []byte, proof []*MPTProofNub) ([]*MPTProofNub, error) { key = t.hashKey(key) return t.trie.ReviveTrie(key, proof) } diff --git a/trie/trie.go b/trie/trie.go index d52409cd2c..df92f0320e 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -24,9 +24,16 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/trie/trienode" ) +var ( + reviveMeter = metrics.NewRegisteredMeter("trie/revive", nil) + reviveNotExpiredMeter = metrics.NewRegisteredMeter("trie/revive/noexpired", nil) + reviveErrMeter = metrics.NewRegisteredMeter("trie/revive/err", nil) +) + // Trie is a Merkle Patricia Trie. Use New to create a trie that sits on // top of a database. Whenever trie performs a commit operation, the generated // nodes will be gathered and returned in a set. Once the trie is committed, @@ -1154,36 +1161,32 @@ func (t *Trie) Owner() common.Hash { // ReviveTrie performs full or partial revive and returns a list of successful // nubs. ReviveTrie does not guarantee that a value will be revived completely, // if the proof is not fully valid. -func (t *Trie) ReviveTrie(key []byte, proof []*MPTProofNub) (successNubs []*MPTProofNub) { +func (t *Trie) ReviveTrie(key []byte, proof []*MPTProofNub) ([]*MPTProofNub, error) { key = keybytesToHex(key) - successNubs, err := t.TryRevive(key, proof) - if err != nil { - log.Error(fmt.Sprintf("Failed to revive trie: %v", err)) - } - return successNubs + return t.TryRevive(key, proof) } -func (t *Trie) TryRevive(key []byte, proof []*MPTProofNub) (successNubs []*MPTProofNub, err error) { - +func (t *Trie) TryRevive(key []byte, proof []*MPTProofNub) ([]*MPTProofNub, error) { + successNubs := make([]*MPTProofNub, 0, len(proof)) + reviveMeter.Mark(int64(len(proof))) // Revive trie with each proof nub for _, nub := range proof { rootExpired := types.EpochExpired(t.getRootEpoch(), t.currentEpoch) newNode, didRevive, err := t.tryRevive(t.root, key, nub.n1PrefixKey, *nub, 0, t.currentEpoch, rootExpired) + if _, ok := err.(*ReviveNotExpiredError); ok { + reviveNotExpiredMeter.Mark(1) + continue + } if err != nil { - log.Error("tryRevive err", "prefix", nub.n1PrefixKey, "didRevive", didRevive, "err", err) + reviveErrMeter.Mark(1) + return nil, err } - if didRevive && err == nil { + if didRevive { successNubs = append(successNubs, nub) t.root = newNode t.rootEpoch = t.currentEpoch } } - - // If no nubs were successful, return error - if len(successNubs) == 0 && len(proof) != 0 { - return successNubs, fmt.Errorf("all nubs failed to revive trie") - } - return successNubs, nil } @@ -1196,7 +1199,7 @@ func (t *Trie) tryRevive(n node, key []byte, targetPrefixKey []byte, nub MPTProo if pos == len(targetPrefixKey) { if !isExpired { - return nil, false, fmt.Errorf("target revive node is not expired") + return nil, false, NewReviveNotExpiredErr(key[:pos], epoch) } hn, ok := n.(hashNode) if !ok { From 753658c909a1f5b045c7413c5521fe799f4be870 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Mon, 18 Sep 2023 17:14:08 +0800 Subject: [PATCH 24/65] trie/trie: fix compile error; --- internal/ethapi/api.go | 6 +++--- miner/miner.go | 2 +- trie/dummy_trie.go | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 5f9a8b7032..7727d1a492 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1459,11 +1459,11 @@ func (s *BlockChainAPI) needToReplay(ctx context.Context, block *types.Block, ac if err != nil { return false, fmt.Errorf("block not found for block number (%d): %v", block.NumberU64()-1, err) } - parentState, err := s.b.Chain().StateAt(parent.Root()) + parentState, err := s.b.Chain().StateAt(parent.Root(), parent.Hash(), parent.Number()) if err != nil { return false, fmt.Errorf("statedb not found for block number (%d): %v", block.NumberU64()-1, err) } - currentState, err := s.b.Chain().StateAt(block.Root()) + currentState, err := s.b.Chain().StateAt(block.Root(), block.Hash(), block.Number()) if err != nil { return false, fmt.Errorf("statedb not found for block number (%d): %v", block.NumberU64(), err) } @@ -1489,7 +1489,7 @@ func (s *BlockChainAPI) replay(ctx context.Context, block *types.Block, accounts if err != nil { return nil, nil, fmt.Errorf("block not found for block number (%d): %v", block.NumberU64()-1, err) } - statedb, err := s.b.Chain().StateAt(parent.Root()) + statedb, err := s.b.Chain().StateAt(parent.Root(), parent.Hash(), block.Number()) if err != nil { return nil, nil, fmt.Errorf("state not found for block number (%d): %v", block.NumberU64()-1, err) } diff --git a/miner/miner.go b/miner/miner.go index 4db6140803..176188fd3f 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -220,7 +220,7 @@ func (miner *Miner) Pending() (*types.Block, *state.StateDB) { if block == nil { return nil, nil } - stateDb, err := miner.worker.chain.StateAt(block.Root) + stateDb, err := miner.worker.chain.StateAt(block.Root, block.Hash(), block.Number) if err != nil { return nil, nil } diff --git a/trie/dummy_trie.go b/trie/dummy_trie.go index 052c9e6689..34415ff4a1 100644 --- a/trie/dummy_trie.go +++ b/trie/dummy_trie.go @@ -96,6 +96,21 @@ func (t *EmptyTrie) ReviveTrie(key []byte, proof []*MPTProofNub) []*MPTProofNub func (t *EmptyTrie) SetEpoch(epoch types.StateEpoch) { } +func (t *EmptyTrie) ProvePath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error { + return nil +} + +func (t *EmptyTrie) GetStorageAndUpdateEpoch(addr common.Address, key []byte) ([]byte, error) { + return nil, nil +} + +func (t *EmptyTrie) SetEpoch(epoch types.StateEpoch) { +} + +func (t *EmptyTrie) ReviveTrie(key []byte, proof []*MPTProofNub) ([]*MPTProofNub, error) { + return nil, nil +} + // Copy returns a copy of SecureTrie. func (t *EmptyTrie) Copy() *EmptyTrie { cpy := *t From 0c86308cac5c950e3ba42acbeca670407e9c2acb Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Fri, 8 Sep 2023 16:54:53 +0800 Subject: [PATCH 25/65] bugfix: fix some proof generate, revive bugs; fullstatedb: opt storage trie init method; metrics: add more cost time metrics; --- core/blockchain.go | 5 +++++ core/state/state_expiry.go | 12 ++++++++++-- core/state/state_object.go | 2 +- core/state/trie_prefetcher.go | 2 +- eth/api_backend.go | 11 ++++++++--- ethdb/fullstatedb.go | 22 ++++++++++++++-------- internal/ethapi/api.go | 25 ++++++++++++------------- internal/ethapi/backend.go | 1 + les/api_backend.go | 4 ++++ trie/proof.go | 28 ++++++++++++++++++++++++---- 10 files changed, 80 insertions(+), 32 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 7a918a0195..066b6bd01a 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -3150,3 +3150,8 @@ func (bc *BlockChain) SetTrieFlushInterval(interval time.Duration) { func (bc *BlockChain) GetTrieFlushInterval() time.Duration { return time.Duration(bc.flushInterval.Load()) } + +// StorageTrie just get Storage trie from db +func (bc *BlockChain) StorageTrie(stateRoot common.Hash, addr common.Address, root common.Hash) (state.Trie, error) { + return bc.stateCache.OpenStorageTrie(stateRoot, addr, root) +} diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index d48f3591db..a10d9f4d40 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -7,11 +7,17 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/trie" + "time" +) + +var ( + reviveStorageTrieTimer = metrics.NewRegisteredTimer("state/revivetrie/rt", nil) ) // fetchExpiredStorageFromRemote request expired state from remote full state node; -func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, blockHash common.Hash, addr common.Address, tr Trie, prefixKey []byte, key common.Hash) (map[string][]byte, error) { +func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, stateRoot common.Hash, addr common.Address, root common.Hash, tr Trie, prefixKey []byte, key common.Hash) (map[string][]byte, error) { // if no prefix, query from revive trie, got the newest expired info if len(prefixKey) == 0 { _, err := tr.GetStorage(addr, key.Bytes()) @@ -19,7 +25,7 @@ func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, blockHash common.Ha prefixKey = enErr.Path } } - proofs, err := fullDB.GetStorageReviveProof(blockHash, addr, []string{common.Bytes2Hex(prefixKey)}, []string{common.Bytes2Hex(key[:])}) + proofs, err := fullDB.GetStorageReviveProof(stateRoot, addr, root, []string{common.Bytes2Hex(prefixKey)}, []string{common.Bytes2Hex(key[:])}) if err != nil { return nil, err } @@ -34,6 +40,8 @@ func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, blockHash common.Ha // reviveStorageTrie revive trie's expired state from proof func reviveStorageTrie(addr common.Address, tr Trie, proof types.ReviveStorageProof, targetKey common.Hash) (map[string][]byte, error) { + start := time.Now() + defer reviveStorageTrieTimer.Update(time.Since(start)) // Decode keys and proofs key := common.FromHex(proof.Key) if !bytes.Equal(targetKey[:], key) { diff --git a/core/state/state_object.go b/core/state/state_object.go index eb506c172c..318796f1a1 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -793,7 +793,7 @@ func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash) } log.Debug("fetchExpiredStorageFromRemote in stateDB", "addr", s.address, "prefixKey", prefixKey, "key", key, "tr", fmt.Sprintf("%p", tr)) - kvs, err := fetchExpiredStorageFromRemote(s.db.fullStateDB, s.db.originalHash, s.address, tr, prefixKey, key) + kvs, err := fetchExpiredStorageFromRemote(s.db.fullStateDB, s.db.originalRoot, s.address, s.data.Root, tr, prefixKey, key) if err != nil { // Keys may not exist in the trie, so they can't be revived. diff --git a/core/state/trie_prefetcher.go b/core/state/trie_prefetcher.go index 633dbba999..6c495793f9 100644 --- a/core/state/trie_prefetcher.go +++ b/core/state/trie_prefetcher.go @@ -585,7 +585,7 @@ func (sf *subfetcher) loop() { if exErr, match := err.(*trie2.ExpiredNodeError); match { key := common.BytesToHash(task) log.Debug("fetchExpiredStorageFromRemote in trie prefetcher", "addr", sf.addr, "prefixKey", exErr.Path, "key", key, "tr", fmt.Sprintf("%p", sf.trie)) - _, err = fetchExpiredStorageFromRemote(sf.fullStateDB, sf.blockHash, sf.addr, sf.trie, exErr.Path, key) + _, err = fetchExpiredStorageFromRemote(sf.fullStateDB, sf.state, sf.addr, sf.root, sf.trie, exErr.Path, key) if err != nil { log.Error("subfetcher fetchExpiredStorageFromRemote err", "addr", sf.addr, "path", exErr.Path, "err", err) } diff --git a/eth/api_backend.go b/eth/api_backend.go index ada7c3f944..7e802f2bc1 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -19,6 +19,7 @@ package eth import ( "context" "errors" + "fmt" "math/big" "time" @@ -102,7 +103,7 @@ func (b *EthAPIBackend) HeaderByNumberOrHash(ctx context.Context, blockNrOrHash if hash, ok := blockNrOrHash.Hash(); ok { header := b.eth.blockchain.GetHeaderByHash(hash) if header == nil { - return nil, errors.New("header for hash not found") + return nil, fmt.Errorf("header for hash not found, hash: %#x", hash) } if blockNrOrHash.RequireCanonical && b.eth.blockchain.GetCanonicalHash(header.Number.Uint64()) != hash { return nil, errors.New("hash is not currently canonical") @@ -169,7 +170,7 @@ func (b *EthAPIBackend) BlockByNumberOrHash(ctx context.Context, blockNrOrHash r if hash, ok := blockNrOrHash.Hash(); ok { header := b.eth.blockchain.GetHeaderByHash(hash) if header == nil { - return nil, errors.New("header for hash not found") + return nil, fmt.Errorf("header for hash not found, hash: %#x", hash) } if blockNrOrHash.RequireCanonical && b.eth.blockchain.GetCanonicalHash(header.Number.Uint64()) != hash { return nil, errors.New("hash is not currently canonical") @@ -218,7 +219,7 @@ func (b *EthAPIBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockN return nil, nil, err } if header == nil { - return nil, nil, errors.New("header for hash not found") + return nil, nil, fmt.Errorf("header for hash not found, hash: %#x", hash) } if blockNrOrHash.RequireCanonical && b.eth.blockchain.GetCanonicalHash(header.Number.Uint64()) != hash { return nil, nil, errors.New("hash is not currently canonical") @@ -229,6 +230,10 @@ func (b *EthAPIBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockN return nil, nil, errors.New("invalid arguments; neither block nor hash specified") } +func (b *EthAPIBackend) StorageTrie(stateRoot common.Hash, addr common.Address, root common.Hash) (state.Trie, error) { + return b.eth.BlockChain().StorageTrie(stateRoot, addr, root) +} + func (b *EthAPIBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) { return b.eth.blockchain.GetReceiptsByHash(hash), nil } diff --git a/ethdb/fullstatedb.go b/ethdb/fullstatedb.go index 26170e2126..efb92453f9 100644 --- a/ethdb/fullstatedb.go +++ b/ethdb/fullstatedb.go @@ -17,12 +17,13 @@ import ( var ( getProofMeter = metrics.NewRegisteredMeter("ethdb/fullstatedb/getproof", nil) getProofHitCacheMeter = metrics.NewRegisteredMeter("ethdb/fullstatedb/getproof/cache", nil) + getStorageProofTimer = metrics.NewRegisteredTimer("ethdb/fullstatedb/getproof/rt", nil) ) // FullStateDB expired state could fetch from it type FullStateDB interface { // GetStorageReviveProof fetch target proof according to specific params - GetStorageReviveProof(blockHash common.Hash, account common.Address, prefixKeys, keys []string) ([]types.ReviveStorageProof, error) + GetStorageReviveProof(stateRoot common.Hash, account common.Address, root common.Hash, prefixKeys, keys []string) ([]types.ReviveStorageProof, error) } type FullStateRPCServer struct { @@ -57,14 +58,16 @@ func NewFullStateRPCServer(endpoint string) (FullStateDB, error) { }, nil } -func (f *FullStateRPCServer) GetStorageReviveProof(blockHash common.Hash, account common.Address, prefixKeys, keys []string) ([]types.ReviveStorageProof, error) { +func (f *FullStateRPCServer) GetStorageReviveProof(stateRoot common.Hash, account common.Address, root common.Hash, prefixKeys, keys []string) ([]types.ReviveStorageProof, error) { + start := time.Now() + defer getStorageProofTimer.Update(time.Since(start)) getProofMeter.Mark(1) // find from lru cache, now it cache key proof uncahcedPrefixKeys := make([]string, 0, len(prefixKeys)) uncahcedKeys := make([]string, 0, len(keys)) ret := make([]types.ReviveStorageProof, 0, len(keys)) for i, key := range keys { - val, ok := f.cache.Get(proofCacheKey(blockHash, account, prefixKeys[i], key)) + val, ok := f.cache.Get(proofCacheKey(account, root, prefixKeys[i], key)) log.Debug("GetStorageReviveProof hit cache", "account", account, "key", key, "ok", ok) if !ok { uncahcedPrefixKeys = append(uncahcedPrefixKeys, prefixKeys[i]) @@ -74,31 +77,34 @@ func (f *FullStateRPCServer) GetStorageReviveProof(blockHash common.Hash, accoun getProofHitCacheMeter.Mark(1) ret = append(ret, val.(types.ReviveStorageProof)) } + if len(uncahcedKeys) == 0 { + return ret, nil + } // TODO(0xbundler): add timeout in flags? ctx, cancelFunc := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancelFunc() proofs := make([]types.ReviveStorageProof, 0, len(uncahcedKeys)) - err := f.client.CallContext(ctx, &proofs, "eth_getStorageReviveProof", account, uncahcedKeys, uncahcedPrefixKeys, blockHash) + err := f.client.CallContext(ctx, &proofs, "eth_getStorageReviveProof", stateRoot, account, root, uncahcedKeys, uncahcedPrefixKeys) if err != nil { return nil, err } // add to cache for _, proof := range proofs { - f.cache.Add(proofCacheKey(blockHash, account, proof.PrefixKey, proof.Key), proof) + f.cache.Add(proofCacheKey(account, root, proof.PrefixKey, proof.Key), proof) } ret = append(ret, proofs...) return ret, err } -func proofCacheKey(blockHash common.Hash, account common.Address, prefix, key string) string { +func proofCacheKey(account common.Address, root common.Hash, prefix, key string) string { buf := bytes.NewBuffer(make([]byte, 0, 67+len(prefix)+len(key))) - buf.Write(blockHash[:]) - buf.WriteByte('$') buf.Write(account[:]) buf.WriteByte('$') + buf.Write(root[:]) + buf.WriteByte('$') buf.WriteString(common.No0xPrefix(prefix)) buf.WriteByte('$') buf.WriteString(common.No0xPrefix(key)) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 7727d1a492..c46caf2d53 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -21,6 +21,7 @@ import ( "encoding/hex" "errors" "fmt" + "github.com/ethereum/go-ethereum/metrics" "math/big" "strings" "time" @@ -64,6 +65,11 @@ func max(a, b int64) int64 { // PublicEthereumAPI provides an API to access Ethereum related information. // It offers only methods that operate on public data that is freely available to anyone. +var ( + getStorageProofTimer = metrics.NewRegisteredTimer("ethapi/getstorageproof/rt", nil) +) + +// EthereumAPI provides an API to access Ethereum related information. type EthereumAPI struct { b Backend } @@ -759,7 +765,9 @@ func (s *BlockChainAPI) GetProof(ctx context.Context, address common.Address, st // GetStorageReviveProof returns the proof for the given keys. Prefix keys can be specified to obtain partial proof for a given key. // Both keys and prefix keys should have the same length. If user wish to obtain full proof for a given key, the corresponding prefix key should be empty string. -func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, address common.Address, storageKeys []string, storagePrefixKeys []string, blockNrOrHash rpc.BlockNumberOrHash) ([]types.ReviveStorageProof, error) { +func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, stateRoot common.Hash, address common.Address, root common.Hash, storageKeys []string, storagePrefixKeys []string) ([]types.ReviveStorageProof, error) { + start := time.Now() + defer getStorageProofTimer.Update(time.Since(start)) if len(storageKeys) != len(storagePrefixKeys) { return nil, errors.New("storageKeys and storagePrefixKeys must be same length") @@ -770,7 +778,6 @@ func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, address commo keyLengths = make([]int, len(storageKeys)) prefixKeys = make([][]byte, len(storagePrefixKeys)) storageProof = make([]types.ReviveStorageProof, len(storageKeys)) - storageTrie state.Trie ) // Deserialize all keys. This prevents state access on invalid input. for i, hexKey := range storageKeys { @@ -790,17 +797,9 @@ func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, address commo } } - state, _, err := s.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) - if state == nil || err != nil { - return nil, err - } - if storageTrie, err = state.StorageTrie(address); err != nil { - return nil, err - } - - // Must have storage trie - if storageTrie == nil { - return nil, errors.New("storageTrie is nil") + storageTrie, err := s.b.StorageTrie(stateRoot, address, root) + if err != nil || storageTrie == nil { + return nil, fmt.Errorf("open StorageTrie err: %v", err) } // Create the proofs for the storageKeys. diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index d71d7e8eba..a6f0c31b88 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -67,6 +67,7 @@ type Backend interface { BlockByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Block, error) StateAndHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*state.StateDB, *types.Header, error) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*state.StateDB, *types.Header, error) + StorageTrie(stateRoot common.Hash, addr common.Address, root common.Hash) (state.Trie, error) PendingBlockAndReceipts() (*types.Block, types.Receipts) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) GetTd(ctx context.Context, hash common.Hash) *big.Int diff --git a/les/api_backend.go b/les/api_backend.go index 9ad566f1ad..391f571806 100644 --- a/les/api_backend.go +++ b/les/api_backend.go @@ -149,6 +149,10 @@ func (b *LesApiBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.B return light.NewState(ctx, header, b.eth.odr), header, nil } +func (b *LesApiBackend) StorageTrie(stateRoot common.Hash, addr common.Address, root common.Hash) (state.Trie, error) { + panic("not implemented") +} + func (b *LesApiBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*state.StateDB, *types.Header, error) { if blockNr, ok := blockNrOrHash.Number(); ok { return b.StateAndHeaderByNumber(ctx, blockNr) diff --git a/trie/proof.go b/trie/proof.go index a6ba41586b..edfde61b4f 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -124,17 +124,32 @@ func (t *Trie) traverseNodes(tn node, key []byte, nodes *[]node, epoch types.Sta for len(key) > 0 && tn != nil { switch n := tn.(type) { case *shortNode: - if len(key) < len(n.Key) || !bytes.Equal(n.Key, key[:len(n.Key)]) { - // The trie doesn't contain the key. - tn = nil - } else { + if len(key) >= len(n.Key) && bytes.Equal(n.Key, key[:len(n.Key)]) { tn = n.Val prefix = append(prefix, n.Key...) key = key[len(n.Key):] + if nodes != nil { + *nodes = append(*nodes, n) + } + continue } + + tn = nil if nodes != nil { *nodes = append(*nodes, n) } + // if there is a extern node, must put the val + hn, isExternNode := n.Val.(hashNode) + if isExternNode && nodes != nil { + prefix = append(prefix, n.Key...) + nextBlob, err := t.reader.node(prefix, common.BytesToHash(hn)) + if err != nil { + log.Error("Unhandled next trie error in traverseNodes", "err", err) + return nil, err + } + next := mustDecodeNodeUnsafe(hn, nextBlob) + *nodes = append(*nodes, next) + } case *fullNode: tn = n.Children[key[0]] prefix = append(prefix, key[0]) @@ -196,6 +211,11 @@ func (t *Trie) ProvePath(key []byte, prefixKeyHex []byte, proofDb ethdb.KeyValue return err } + if len(nodes) == 0 { + log.Error("found nothing....", "prefix", prefixKeyHex, "key", key) + return fmt.Errorf("cannot find target proof, prefix: %#x, suffix: %#x", prefixKeyHex, key) + } + hasher := newHasher(false) defer returnHasherToPool(hasher) From 001a2c577f33ea11544dfc419ec7dcfd44078d71 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Mon, 11 Sep 2023 15:42:21 +0800 Subject: [PATCH 26/65] trie/epochmeta: opt graceful shutdown logic; pruner: refactor expired prune; --- core/blockchain.go | 40 ++++---- core/chain_makers.go | 2 +- core/genesis.go | 2 +- core/state/pruner/pruner.go | 130 ++++++++++++++----------- core/state/snapshot/conversion.go | 97 +++++++++++++++--- core/state/snapshot/generate.go | 6 +- core/state/snapshot/generate_test.go | 4 +- core/state/snapshot/snapshot.go | 4 +- core/state/snapshot/snapshot_expire.go | 14 +-- core/state/snapshot/snapshot_value.go | 5 +- trie/database.go | 11 +++ trie/trie.go | 9 +- 12 files changed, 213 insertions(+), 111 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 066b6bd01a..5bb37607bc 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1201,12 +1201,6 @@ func (bc *BlockChain) Stop() { } } - epochMetaSnapTree := bc.triedb.EpochMetaSnapTree() - if epochMetaSnapTree != nil { - if err := epochMetaSnapTree.Journal(); err != nil { - log.Error("Failed to journal epochMetaSnapTree", "err", err) - } - } if bc.triedb.Scheme() == rawdb.PathScheme { // Ensure that the in-memory trie nodes are journaled to disk properly. if err := bc.triedb.Journal(bc.CurrentBlock().Root); err != nil { @@ -1220,7 +1214,7 @@ func (bc *BlockChain) Stop() { // - HEAD-127: So we have a hard limit on the number of blocks reexecuted if !bc.cacheConfig.TrieDirtyDisabled { triedb := bc.triedb - var once sync.Once + var once sync.Once for _, offset := range []uint64{0, 1, TriesInMemory - 1} { if number := bc.CurrentBlock().Number.Uint64(); number > offset { @@ -1231,9 +1225,9 @@ func (bc *BlockChain) Stop() { } else { rawdb.WriteSafePointBlockNumber(bc.db, recent.NumberU64()) once.Do(func() { - rawdb.WriteHeadBlockHash(bc.db, recent.Hash()) + rawdb.WriteHeadBlockHash(bc.db, recent.Hash()) }) - } + } } } if snapBase != (common.Hash{}) { @@ -1243,14 +1237,8 @@ func (bc *BlockChain) Stop() { } else { rawdb.WriteSafePointBlockNumber(bc.db, bc.CurrentBlock().Number.Uint64()) } - } - - if snapBase != (common.Hash{}) { - log.Info("Writing snapshot state to disk", "root", snapBase) - if err := bc.triedb.Commit(snapBase, true); err != nil { - log.Error("Failed to commit recent state trie", "err", err) - } else { - rawdb.WriteSafePointBlockNumber(bc.db, bc.CurrentBlock().Number.Uint64()) + if err := triedb.CommitEpochMeta(snapBase); err != nil { + log.Error("Failed to commit recent epoch meta", "err", err) } } for !bc.triegc.Empty() { @@ -1261,9 +1249,17 @@ func (bc *BlockChain) Stop() { } } } - // Close the trie database, release all the held resources as the last step. - if err := bc.triedb.Close(); err != nil { - log.Error("Failed to close trie database", "err", err) + + epochMetaSnapTree := bc.triedb.EpochMetaSnapTree() + if epochMetaSnapTree != nil { + if err := epochMetaSnapTree.Journal(); err != nil { + log.Error("Failed to journal epochMetaSnapTree", "err", err) + } + } + + // Flush the collected preimages to disk + if err := bc.stateCache.TrieDB().Close(); err != nil { + log.Error("Failed to close trie db", "err", err) } log.Info("Blockchain stopped") } @@ -1648,7 +1644,7 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. triedb := bc.stateCache.TrieDB() // If we're running an archive node, always flush if bc.cacheConfig.TrieDirtyDisabled { - err := triedb.Commit(block.Root(), false) + err := triedb.CommitAll(block.Root(), false) if err != nil { return err } @@ -1692,7 +1688,7 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. log.Info("State in memory for too long, committing", "time", bc.gcproc, "allowance", flushInterval, "optimum", float64(chosen-bc.lastWrite)/float64(bc.triesInMemory)) } // Flush an entire trie and restart the counters - triedb.Commit(header.Root, true) + triedb.CommitAll(header.Root, true) rawdb.WriteSafePointBlockNumber(bc.db, chosen) bc.lastWrite = chosen bc.gcproc = 0 diff --git a/core/chain_makers.go b/core/chain_makers.go index f0026089ac..58593bc9bf 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -324,7 +324,7 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse if err != nil { panic(fmt.Sprintf("state write error: %v", err)) } - if err = triedb.Commit(root, false); err != nil { + if err := statedb.Database().TrieDB().CommitAll(root, false); err != nil { panic(fmt.Sprintf("trie write error: %v", err)) } return block, b.receipts diff --git a/core/genesis.go b/core/genesis.go index 8fa107ac66..d3ed27df2e 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -172,7 +172,7 @@ func (ga *GenesisAlloc) flush(db ethdb.Database, triedb *trie.Database, blockhas } // Commit newly generated states into disk if it's not empty. if root != types.EmptyRootHash { - if err := triedb.Commit(root, true); err != nil { + if err := triedb.CommitAll(root, true); err != nil { return err } } diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go index a8a05b6e97..03a0329795 100644 --- a/core/state/pruner/pruner.go +++ b/core/state/pruner/pruner.go @@ -27,6 +27,7 @@ import ( "os" "path/filepath" "strings" + "sync" "time" "github.com/prometheus/tsdb/fileutil" @@ -638,19 +639,10 @@ func (p *Pruner) Prune(root common.Hash) error { } middleRoots[layer.Root()] = struct{}{} } - - var pruneExpiredCh chan *snapshot.ContractItem - var expireRetCh chan error - if p.config.EnableStateExpiry { - pruneExpiredCh = make(chan *snapshot.ContractItem, 100000) - expireRetCh = make(chan error, 1) - epoch := types.GetStateEpoch(p.config.ChainConfig, p.chainHeader.Number) - go asyncPruneExpired(p.db, root, epoch, pruneExpiredCh, expireRetCh) - } // Traverse the target state, re-construct the whole state trie and // commit to the given bloom filter. start := time.Now() - if err := snapshot.GenerateTrie(p.snaptree, root, p.db, p.stateBloom, pruneExpiredCh); err != nil { + if err := snapshot.GenerateTrie(p.snaptree, root, p.db, p.stateBloom); err != nil { return err } // Traverse the genesis, put all genesis state entries into the @@ -665,31 +657,65 @@ func (p *Pruner) Prune(root common.Hash) error { return err } log.Info("State bloom filter committed", "name", filterName) - err = prune(p.snaptree, root, p.db, p.stateBloom, filterName, middleRoots, start) + if err = prune(p.snaptree, root, p.db, p.stateBloom, filterName, middleRoots, start); err != nil { + return err + } - // wait expired result - if expireRetCh != nil { - expireErr := <-expireRetCh - if expireErr != nil { - return expireErr + // it must run later to prune, using bloom filter to prevent pruning in use trie node, cannot prune concurrently. + if p.config.EnableStateExpiry { + var ( + pruneExpiredTrieCh = make(chan *snapshot.ContractItem, 100000) + pruneExpiredInDiskCh = make(chan *trie.NodeInfo, 100000) + epoch = types.GetStateEpoch(p.config.ChainConfig, p.chainHeader.Number) + rets = make([]error, 3) + expiryWG sync.WaitGroup + ) + trieDB := trie.NewDatabaseWithConfig(p.db, &trie.Config{ + EnableStateExpiry: true, + PathDB: nil, // TODO(0xbundler): support later + }) + expiryWG.Add(2) + go func() { + defer expiryWG.Done() + rets[0] = asyncScanExpiredInTrie(trieDB, root, epoch, pruneExpiredTrieCh, pruneExpiredInDiskCh) + }() + go func() { + defer expiryWG.Done() + rets[1] = asyncPruneExpiredStorageInDisk(p.db, pruneExpiredInDiskCh, p.stateBloom) + }() + rets[2] = snapshot.TraverseContractTrie(p.snaptree, root, pruneExpiredTrieCh) + + // wait task done + expiryWG.Wait() + for i, item := range rets { + if item != nil { + log.Error("prune expired state got error", "index", i, "err", item) + } } + + // recap epoch meta snap, save journal + snap := trieDB.EpochMetaSnapTree() + if snap != nil { + if err := snap.Cap(root); err != nil { + log.Error("asyncPruneExpired, SnapTree Cap err", "err", err) + return err + } + if err := snap.Journal(); err != nil { + log.Error("asyncPruneExpired, SnapTree Journal err", "err", err) + return err + } + } + log.Info("Expired State pruning successful") } - return err + + return nil } -// asyncPruneExpired prune trie expired state -// TODO(0xbundler): here are some issues when just delete it from hash-based storage, because it's shared kv in hbss +// asyncScanExpiredInTrie prune trie expired state +// here are some issues when just delete it from hash-based storage, because it's shared kv in hbss // but it's ok for pbss. -func asyncPruneExpired(diskdb ethdb.Database, stateRoot common.Hash, currentEpoch types.StateEpoch, expireRootCh chan *snapshot.ContractItem, expirePruneCh chan error) { - db := trie.NewDatabaseWithConfig(diskdb, &trie.Config{ - EnableStateExpiry: true, - PathDB: nil, // TODO(0xbundler): support later - }) - - pruneItemCh := make(chan *trie.NodeInfo, 100000) - pruneDiskRet := make(chan error, 1) - go asyncPruneExpiredStorageInDisk(diskdb, pruneItemCh, pruneDiskRet) - for item := range expireRootCh { +func asyncScanExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch types.StateEpoch, expireContractCh chan *snapshot.ContractItem, pruneExpiredInDisk chan *trie.NodeInfo) error { + for item := range expireContractCh { log.Info("start scan trie expired state", "addr", item.Addr, "root", item.Root) tr, err := trie.New(&trie.ID{ StateRoot: stateRoot, @@ -697,53 +723,43 @@ func asyncPruneExpired(diskdb ethdb.Database, stateRoot common.Hash, currentEpoc Root: item.Root, }, db) if err != nil { - log.Error("asyncPruneExpired, trie.New err", "id", item, "err", err) - continue - } - tr.SetEpoch(currentEpoch) - if err = tr.PruneExpired(pruneItemCh); err != nil { - log.Error("asyncPruneExpired, PruneExpired err", "id", item, "err", err) + log.Error("asyncScanExpiredInTrie, trie.New err", "id", item, "err", err) + return err } - } - - err := <-pruneDiskRet - if err != nil { - log.Error("asyncPruneExpired, pruneDiskRet err", "err", err) - expirePruneCh <- err - return - } - - st := db.EpochMetaSnapTree() - if st != nil { - if err := st.Journal(); err != nil { - log.Error("asyncPruneExpired, SnapTree Journal err", "err", err) - expirePruneCh <- err - return + tr.SetEpoch(epoch) + if err = tr.PruneExpired(pruneExpiredInDisk); err != nil { + log.Error("asyncScanExpiredInTrie, PruneExpired err", "id", item, "err", err) + return err } } + close(pruneExpiredInDisk) + return nil } -func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, expiredCh chan *trie.NodeInfo, ret chan error) { +func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, pruneExpiredInDisk chan *trie.NodeInfo, bloom *stateBloom) error { var ( trieCount = 0 - trieSize common.StorageSize + epochMetaCount = 0 snapCount = 0 + trieSize common.StorageSize snapSize common.StorageSize - epochMetaCount = 0 epochMetaSize common.StorageSize start = time.Now() logged = time.Now() ) batch := diskdb.NewBatch() - for info := range expiredCh { - log.Info("found expired state", "addr", info.Addr, "path", + for info := range pruneExpiredInDisk { + log.Debug("found expired state", "addr", info.Addr, "path", hex.EncodeToString(info.Path), "epoch", info.Epoch, "isBranch", info.IsBranch, "isLeaf", info.IsLeaf) addr := info.Addr // delete trie kv trieCount++ trieSize += common.StorageSize(len(info.Key) + 32) - rawdb.DeleteTrieNode(batch, addr, info.Path, info.Hash, rawdb.HashScheme) + // hbss has shared kv, so using bloom to filter them out. + if !bloom.Contain(info.Hash.Bytes()) { + rawdb.DeleteTrieNode(batch, addr, info.Path, info.Hash, rawdb.HashScheme) + } // delete epoch meta if info.IsBranch { epochMetaCount++ @@ -776,7 +792,7 @@ func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, expiredCh chan *trie. log.Info("Pruned expired states", "trieNodes", trieCount, "trieSize", trieSize, "SnapKV", snapCount, "SnapKVSize", snapSize, "EpochMeta", epochMetaCount, "EpochMetaSize", epochMetaSize, "elapsed", common.PrettyDuration(time.Since(start))) - ret <- nil + return nil } // RecoverPruning will resume the pruning procedure during the system restart. diff --git a/core/state/snapshot/conversion.go b/core/state/snapshot/conversion.go index ac11e1cd5e..9e1f9242fc 100644 --- a/core/state/snapshot/conversion.go +++ b/core/state/snapshot/conversion.go @@ -59,18 +59,18 @@ type ( // GenerateAccountTrieRoot takes an account iterator and reproduces the root hash. func GenerateAccountTrieRoot(it AccountIterator) (common.Hash, error) { - return generateTrieRoot(nil, "", it, common.Hash{}, stackTrieGenerate, nil, newGenerateStats(), true, nil) + return generateTrieRoot(nil, "", it, common.Hash{}, stackTrieGenerate, nil, newGenerateStats(), true) } // GenerateStorageTrieRoot takes a storage iterator and reproduces the root hash. func GenerateStorageTrieRoot(account common.Hash, it StorageIterator) (common.Hash, error) { - return generateTrieRoot(nil, "", it, account, stackTrieGenerate, nil, newGenerateStats(), true, nil) + return generateTrieRoot(nil, "", it, account, stackTrieGenerate, nil, newGenerateStats(), true) } // GenerateTrie takes the whole snapshot tree as the input, traverses all the // accounts as well as the corresponding storages and regenerate the whole state // (account trie + all storage tries). -func GenerateTrie(snaptree *Tree, root common.Hash, src ethdb.Database, dst ethdb.KeyValueWriter, pruneExpiredCh chan *ContractItem) error { +func GenerateTrie(snaptree *Tree, root common.Hash, src ethdb.Database, dst ethdb.KeyValueWriter) error { // Traverse all state by snapshot, re-generate the whole state trie acctIt, err := snaptree.AccountIterator(root, common.Hash{}) if err != nil { @@ -95,12 +95,12 @@ func GenerateTrie(snaptree *Tree, root common.Hash, src ethdb.Database, dst ethd } defer storageIt.Release() - hash, err := generateTrieRoot(dst, scheme, storageIt, accountHash, stackTrieGenerate, nil, stat, false, nil) + hash, err := generateTrieRoot(dst, scheme, storageIt, accountHash, stackTrieGenerate, nil, stat, false) if err != nil { return common.Hash{}, err } return hash, nil - }, newGenerateStats(), true, pruneExpiredCh) + }, newGenerateStats(), true) if err != nil { return err @@ -111,6 +111,71 @@ func GenerateTrie(snaptree *Tree, root common.Hash, src ethdb.Database, dst ethd return nil } +// TraverseContractTrie traverse contract from snap iterator +func TraverseContractTrie(snaptree *Tree, root common.Hash, pruneExpiredTrieCh chan *ContractItem) error { + stats := newGenerateStats() + // Traverse all state by snapshot, re-generate the whole state trie + acctIt, err := snaptree.AccountIterator(root, common.Hash{}) + if err != nil { + return err // The required snapshot might not exist. + } + defer acctIt.Release() + + var ( + stoplog = make(chan bool, 1) // 1-size buffer, works when logging is not enabled + wg sync.WaitGroup + ) + // Spin up a go-routine for progress logging + if stats != nil { + wg.Add(1) + go func() { + defer wg.Done() + runReport(stats, stoplog) + }() + } + + var ( + logged = time.Now() + processed = uint64(0) + account *types.StateAccount + ) + // Start to feed leaves + for acctIt.Next() { + // Fetch the next account and process it concurrently + account, err = types.FullAccount(acctIt.(AccountIterator).Account()) + if err != nil { + break + } + + hash := acctIt.Hash() + // async prune trie expired states + if pruneExpiredTrieCh != nil && (account.Root != common.Hash{} && account.Root != types.EmptyRootHash) { + pruneExpiredTrieCh <- &ContractItem{ + Addr: hash, + Root: account.Root, + } + } + + // Accumulate the generation statistic if it's required. + processed++ + if time.Since(logged) > 3*time.Second && stats != nil { + stats.progressAccounts(hash, processed) + logged, processed = time.Now(), 0 + } + } + close(pruneExpiredTrieCh) + stoplog <- true + + // Commit the last part statistic. + if processed > 0 && stats != nil { + stats.finishAccounts(processed) + } + + // wait tasks down + wg.Wait() + return err +} + // generateStats is a collection of statistics gathered by the trie generator // for logging purposes. type generateStats struct { @@ -250,7 +315,7 @@ func runReport(stats *generateStats, stop chan bool) { // generateTrieRoot generates the trie hash based on the snapshot iterator. // It can be used for generating account trie, storage trie or even the // whole state which connects the accounts and the corresponding storages. -func generateTrieRoot(db ethdb.KeyValueWriter, scheme string, it Iterator, account common.Hash, generatorFn trieGeneratorFn, leafCallback leafCallbackFn, stats *generateStats, report bool, pruneExpiredCh chan *ContractItem) (common.Hash, error) { +func generateTrieRoot(db ethdb.KeyValueWriter, scheme string, it Iterator, account common.Hash, generatorFn trieGeneratorFn, leafCallback leafCallbackFn, stats *generateStats, report bool) (common.Hash, error) { var ( in = make(chan trieKV) // chan to pass leaves out = make(chan common.Hash, 1) // chan to collect result @@ -334,13 +399,6 @@ func generateTrieRoot(db ethdb.KeyValueWriter, scheme string, it Iterator, accou results <- fmt.Errorf("invalid subroot(path %x), want %x, have %x", hash, account.Root, subroot) return } - // async prune trie expired states - if pruneExpiredCh != nil { - pruneExpiredCh <- &ContractItem{ - Addr: hash, - Root: account.Root, - } - } results <- nil }) fullData, err = rlp.EncodeToBytes(account) @@ -350,7 +408,18 @@ func generateTrieRoot(db ethdb.KeyValueWriter, scheme string, it Iterator, accou } leaf = trieKV{it.Hash(), fullData} } else { - leaf = trieKV{it.Hash(), common.CopyBytes(it.(StorageIterator).Slot())} + enc := common.CopyBytes(it.(StorageIterator).Slot()) + if len(enc) > 0 { + val, err := DecodeValueFromRLPBytes(enc) + if err != nil { + return stop(err) + } + if val.GetEpoch() == 0 { + log.Info("found epoch0") + } + enc, _ = rlp.EncodeToBytes(val.GetVal()) + } + leaf = trieKV{it.Hash(), enc} } in <- leaf diff --git a/core/state/snapshot/generate.go b/core/state/snapshot/generate.go index fbd83e3b52..28f2abd289 100644 --- a/core/state/snapshot/generate.go +++ b/core/state/snapshot/generate.go @@ -367,8 +367,10 @@ func (dl *diskLayer) generateRange(ctx *generatorContext, trieId *trie.ID, prefi return false, nil, err } if nodes != nil { - tdb.Update(root, types.EmptyRootHash, 0, trienode.NewWithNodeSet(nodes), nil) - tdb.Commit(root, false) + // TODO(Nathan): why block is zero? + block := uint64(0) + tdb.Update(root, types.EmptyRootHash, block, trienode.NewWithNodeSet(nodes), nil) + tdb.CommitAll(root, false) } resolver = func(owner common.Hash, path []byte, hash common.Hash) []byte { return rawdb.ReadTrieNode(mdb, owner, path, hash, tdb.Scheme()) diff --git a/core/state/snapshot/generate_test.go b/core/state/snapshot/generate_test.go index 7d9cdc0ace..07016b675c 100644 --- a/core/state/snapshot/generate_test.go +++ b/core/state/snapshot/generate_test.go @@ -136,12 +136,12 @@ func checkSnapRoot(t *testing.T, snap *diskLayer, trieRoot common.Hash) { storageIt, _ := snap.StorageIterator(accountHash, common.Hash{}) defer storageIt.Release() - hash, err := generateTrieRoot(nil, "", storageIt, accountHash, stackTrieGenerate, nil, stat, false, nil) + hash, err := generateTrieRoot(nil, "", storageIt, accountHash, stackTrieGenerate, nil, stat, false) if err != nil { return common.Hash{}, err } return hash, nil - }, newGenerateStats(), true, nil) + }, newGenerateStats(), true) if err != nil { t.Fatal(err) } diff --git a/core/state/snapshot/snapshot.go b/core/state/snapshot/snapshot.go index 6de5755ce2..f18b6faabe 100644 --- a/core/state/snapshot/snapshot.go +++ b/core/state/snapshot/snapshot.go @@ -813,12 +813,12 @@ func (t *Tree) Verify(root common.Hash) error { } defer storageIt.Release() - hash, err := generateTrieRoot(nil, "", storageIt, accountHash, stackTrieGenerate, nil, stat, false, nil) + hash, err := generateTrieRoot(nil, "", storageIt, accountHash, stackTrieGenerate, nil, stat, false) if err != nil { return common.Hash{}, err } return hash, nil - }, newGenerateStats(), true, nil) + }, newGenerateStats(), true) if err != nil { return err diff --git a/core/state/snapshot/snapshot_expire.go b/core/state/snapshot/snapshot_expire.go index 7c53e7dde5..9fa8cf29f4 100644 --- a/core/state/snapshot/snapshot_expire.go +++ b/core/state/snapshot/snapshot_expire.go @@ -2,18 +2,18 @@ package snapshot import ( "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" ) // ShrinkExpiredLeaf tool function for snapshot kv prune func ShrinkExpiredLeaf(db ethdb.KeyValueWriter, accountHash common.Hash, storageHash common.Hash, epoch types.StateEpoch) error { - valWithEpoch := NewValueWithEpoch(epoch, nil) - enc, err := EncodeValueToRLPBytes(valWithEpoch) - if err != nil { - return err - } - rawdb.WriteStorageSnapshot(db, accountHash, storageHash, enc) + // TODO: cannot prune snapshot in hbss, because it will used for trie prune, but it's ok in pbss. + //valWithEpoch := NewValueWithEpoch(epoch, nil) + //enc, err := EncodeValueToRLPBytes(valWithEpoch) + //if err != nil { + // return err + //} + //rawdb.WriteStorageSnapshot(db, accountHash, storageHash, enc) return nil } diff --git a/core/state/snapshot/snapshot_value.go b/core/state/snapshot/snapshot_value.go index b6c479be84..e7e0ff3525 100644 --- a/core/state/snapshot/snapshot_value.go +++ b/core/state/snapshot/snapshot_value.go @@ -94,7 +94,10 @@ func EncodeValueToRLPBytes(val SnapValue) ([]byte, error) { } func DecodeValueFromRLPBytes(b []byte) (SnapValue, error) { - if len(b) > 0 && b[0] > 0x7f { + if len(b) == 0 { + return &RawValue{}, nil + } + if b[0] > 0x7f { var data RawValue _, data, _, err := rlp.Split(b) if err != nil { diff --git a/trie/database.go b/trie/database.go index f5584a8f1e..1aeec8a945 100644 --- a/trie/database.go +++ b/trie/database.go @@ -221,6 +221,17 @@ func (db *Database) Commit(root common.Hash, report bool) error { if err := db.backend.Commit(root, report); err != nil { return err } + return nil +} + +func (db *Database) CommitAll(root common.Hash, report bool) error { + if err := db.Commit(root, report); err != nil { + return err + } + return db.CommitEpochMeta(root) +} + +func (db *Database) CommitEpochMeta(root common.Hash) error { if db.snapTree != nil { if err := db.snapTree.Cap(root); err != nil { return err diff --git a/trie/trie.go b/trie/trie.go index df92f0320e..6f69700f67 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -1472,6 +1472,8 @@ func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, p return t.findExpiredSubTree(resolve, path, epoch, pruner) case valueNode: return nil + case nil: + return nil default: panic(fmt.Sprintf("invalid node type: %T", n)) } @@ -1500,8 +1502,9 @@ func (t *Trie) recursePruneExpiredNode(n node, path []byte, epoch types.StateEpo } return nil case *fullNode: - for i, child := range n.Children { - err := t.recursePruneExpiredNode(child, append(path, byte(i)), n.EpochMap[i], pruneItemCh) + // recurse child, and except valueNode + for i := 0; i < BranchNodeLength-1; i++ { + err := t.recursePruneExpiredNode(n.Children[i], append(path, byte(i)), n.EpochMap[i], pruneItemCh) if err != nil { return err } @@ -1528,6 +1531,8 @@ func (t *Trie) recursePruneExpiredNode(n node, path []byte, epoch types.StateEpo case valueNode: // value node is not a single storage uint, so pass to prune. return nil + case nil: + return nil default: panic(fmt.Sprintf("invalid node type: %T", n)) } From 079a1c6c7fea055ee4fce5b295179986dd501180 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Mon, 11 Sep 2023 21:21:25 +0800 Subject: [PATCH 27/65] trie/inspect: add inspect trie tools; --- cmd/geth/dbcmd.go | 83 +++++++++ core/rawdb/ancient_utils.go | 6 +- trie/inspect_trie.go | 324 ++++++++++++++++++++++++++++++++++++ trie/trie.go | 8 + 4 files changed, 418 insertions(+), 3 deletions(-) create mode 100644 trie/inspect_trie.go diff --git a/cmd/geth/dbcmd.go b/cmd/geth/dbcmd.go index eb6185fc2f..0b0df3fbd2 100644 --- a/cmd/geth/dbcmd.go +++ b/cmd/geth/dbcmd.go @@ -59,6 +59,7 @@ Remove blockchain and state databases`, ArgsUsage: "", Subcommands: []*cli.Command{ dbInspectCmd, + dbInspectTrieCmd, dbStatCmd, dbCompactCmd, dbGetCmd, @@ -88,6 +89,16 @@ Remove blockchain and state databases`, Usage: "Inspect the storage size for each type of data in the database", Description: `This commands iterates the entire database. If the optional 'prefix' and 'start' arguments are provided, then the iteration is limited to the given subset of data.`, } + dbInspectTrieCmd = &cli.Command{ + Action: inspectTrie, + Name: "inspect-trie", + ArgsUsage: " ", + Flags: flags.Merge([]cli.Flag{ + utils.SyncModeFlag, + }, utils.DatabasePathFlags), + Usage: "Inspect the MPT tree of the account and contract.", + Description: `This commands iterates the entrie WorldState.`, + } dbCheckStateContentCmd = &cli.Command{ Action: checkStateContent, Name: "check-state-content", @@ -352,6 +363,78 @@ func ancientInspect(ctx *cli.Context) error { return rawdb.AncientInspect(db) } +func inspectTrie(ctx *cli.Context) error { + if ctx.NArg() < 1 { + return fmt.Errorf("required arguments: %v", ctx.Command.ArgsUsage) + } + + if ctx.NArg() > 3 { + return fmt.Errorf("Max 3 arguments: %v", ctx.Command.ArgsUsage) + } + + var ( + blockNumber uint64 + trieRootHash common.Hash + jobnum uint64 + ) + + stack, _ := makeConfigNode(ctx) + defer stack.Close() + + db := utils.MakeChainDatabase(ctx, stack, true, true) + defer db.Close() + + var headerBlockHash common.Hash + if ctx.NArg() >= 1 { + if ctx.Args().Get(0) == "latest" { + headerHash := rawdb.ReadHeadHeaderHash(db) + blockNumber = *(rawdb.ReadHeaderNumber(db, headerHash)) + } else if ctx.Args().Get(0) == "snapshot" { + trieRootHash = rawdb.ReadSnapshotRoot(db) + blockNumber = math.MaxUint64 + } else { + var err error + blockNumber, err = strconv.ParseUint(ctx.Args().Get(0), 10, 64) + if err != nil { + return fmt.Errorf("failed to Parse blocknum, Args[0]: %v, err: %v", ctx.Args().Get(0), err) + } + } + + if ctx.NArg() == 1 { + jobnum = 1000 + } else { + var err error + jobnum, err = strconv.ParseUint(ctx.Args().Get(1), 10, 64) + if err != nil { + return fmt.Errorf("failed to Parse jobnum, Args[1]: %v, err: %v", ctx.Args().Get(1), err) + } + } + + if blockNumber != math.MaxUint64 { + headerBlockHash = rawdb.ReadCanonicalHash(db, blockNumber) + if headerBlockHash == (common.Hash{}) { + return fmt.Errorf("ReadHeadBlockHash empry hash") + } + blockHeader := rawdb.ReadHeader(db, headerBlockHash, blockNumber) + trieRootHash = blockHeader.Root + } + if (trieRootHash == common.Hash{}) { + log.Error("Empty root hash") + } + fmt.Printf("ReadBlockHeader, root: %v, blocknum: %v\n", trieRootHash, blockNumber) + trieDB := trie.NewDatabase(db) + theTrie, err := trie.New(trie.TrieID(trieRootHash), trieDB) + if err != nil { + fmt.Printf("fail to new trie tree, err: %v, rootHash: %v\n", err, trieRootHash.String()) + return err + } + theInspect, err := trie.NewInspector(trieDB, theTrie, blockNumber, jobnum) + theInspect.Run() + theInspect.DisplayResult() + } + return nil +} + func checkStateContent(ctx *cli.Context) error { var ( prefix []byte diff --git a/core/rawdb/ancient_utils.go b/core/rawdb/ancient_utils.go index 392ac79631..39632fb3be 100644 --- a/core/rawdb/ancient_utils.go +++ b/core/rawdb/ancient_utils.go @@ -109,9 +109,9 @@ func inspectFreezers(db ethdb.Database) ([]freezerInfo, error) { return nil, err } infos = append(infos, info) - - default: - return nil, fmt.Errorf("unknown freezer, supported ones: %v", freezers) + //TODO(0xbundler): bug? + //default: + // return nil, fmt.Errorf("unknown freezer, supported ones: %v", freezers) } } return infos, nil diff --git a/trie/inspect_trie.go b/trie/inspect_trie.go new file mode 100644 index 0000000000..818ad63cdd --- /dev/null +++ b/trie/inspect_trie.go @@ -0,0 +1,324 @@ +package trie + +import ( + "bytes" + "errors" + "fmt" + "github.com/ethereum/go-ethereum/core/types" + "math/big" + "os" + "runtime" + "sort" + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rlp" + "github.com/olekukonko/tablewriter" +) + +const ( + DEFAULT_TRIEDBCACHE_SIZE = 1024 * 1024 * 1024 +) + +type Account struct { + Nonce uint64 + Balance *big.Int + Root common.Hash // merkle root of the storage trie + CodeHash []byte +} + +type Inspector struct { + trieDB *Database + trie *Trie // traverse trie + blocknum uint64 + root node // root of triedb + num uint64 // block number + result *TotalTrieTreeStat // inspector result + totalNum uint64 + concurrentQueue chan struct{} + wg sync.WaitGroup +} + +type RWMap struct { + sync.RWMutex + m map[uint64]*TrieTreeStat +} + +// 新建一个RWMap +func NewRWMap() *RWMap { + return &RWMap{ + m: make(map[uint64]*TrieTreeStat, 1), + } +} +func (m *RWMap) Get(k uint64) (*TrieTreeStat, bool) { //从map中读取一个值 + m.RLock() + defer m.RUnlock() + v, existed := m.m[k] // 在锁的保护下从map中读取 + return v, existed +} + +func (m *RWMap) Set(k uint64, v *TrieTreeStat) { // 设置一个键值对 + m.Lock() // 锁保护 + defer m.Unlock() + m.m[k] = v +} + +func (m *RWMap) Delete(k uint64) { //删除一个键 + m.Lock() // 锁保护 + defer m.Unlock() + delete(m.m, k) +} + +func (m *RWMap) Len() int { // map的长度 + m.RLock() // 锁保护 + defer m.RUnlock() + return len(m.m) +} + +func (m *RWMap) Each(f func(k uint64, v *TrieTreeStat) bool) { // 遍历map + m.RLock() //遍历期间一直持有读锁 + defer m.RUnlock() + + for k, v := range m.m { + if !f(k, v) { + return + } + } +} + +type TotalTrieTreeStat struct { + theTrieTreeStats RWMap +} + +type TrieTreeStat struct { + is_account_trie bool + theNodeStatByLevel [15]NodeStat + totalNodeStat NodeStat +} + +type NodeStat struct { + ShortNodeCnt uint64 + FullNodeCnt uint64 + ValueNodeCnt uint64 +} + +func (trieStat *TrieTreeStat) AtomicAdd(theNode node, height uint32) { + switch (theNode).(type) { + case *shortNode: + atomic.AddUint64(&trieStat.totalNodeStat.ShortNodeCnt, 1) + atomic.AddUint64(&(trieStat.theNodeStatByLevel[height].ShortNodeCnt), 1) + case *fullNode: + atomic.AddUint64(&trieStat.totalNodeStat.FullNodeCnt, 1) + atomic.AddUint64(&trieStat.theNodeStatByLevel[height].FullNodeCnt, 1) + case valueNode: + atomic.AddUint64(&trieStat.totalNodeStat.ValueNodeCnt, 1) + atomic.AddUint64(&((trieStat.theNodeStatByLevel[height]).ValueNodeCnt), 1) + default: + panic(errors.New("Invalid node type to statistics")) + } +} + +func (trieStat *TrieTreeStat) Display(rootHash uint64, treeType string) { + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"TrieType", "Level", "ShortNodeCnt", "FullNodeCnt", "ValueNodeCnt"}) + table.SetAlignment(1) + for i := 0; i < len(trieStat.theNodeStatByLevel); i++ { + nodeStat := trieStat.theNodeStatByLevel[i] + if nodeStat.FullNodeCnt == 0 && nodeStat.ShortNodeCnt == 0 && nodeStat.ValueNodeCnt == 0 { + break + } + table.AppendBulk([][]string{ + {"-", strconv.Itoa(i), nodeStat.ShortNodeCount(), nodeStat.FullNodeCount(), nodeStat.ValueNodeCount()}, + }) + } + table.AppendBulk([][]string{ + {fmt.Sprintf("%v-%v", treeType, rootHash), "Total", trieStat.totalNodeStat.ShortNodeCount(), trieStat.totalNodeStat.FullNodeCount(), trieStat.totalNodeStat.ValueNodeCount()}, + }) + table.Render() +} + +func Uint64ToString(cnt uint64) string { + return fmt.Sprintf("%v", cnt) +} + +func (nodeStat *NodeStat) ShortNodeCount() string { + return Uint64ToString(nodeStat.ShortNodeCnt) +} + +func (nodeStat *NodeStat) FullNodeCount() string { + return Uint64ToString(nodeStat.FullNodeCnt) +} +func (nodeStat *NodeStat) ValueNodeCount() string { + return Uint64ToString(nodeStat.ValueNodeCnt) +} + +// NewInspector return a inspector obj +func NewInspector(trieDB *Database, tr *Trie, blocknum uint64, jobnum uint64) (*Inspector, error) { + if tr == nil { + return nil, errors.New("trie is nil") + } + + if tr.root == nil { + return nil, errors.New("trie root is nil") + } + + ins := &Inspector{ + trieDB: trieDB, + trie: tr, + blocknum: blocknum, + root: tr.root, + result: &TotalTrieTreeStat{ + theTrieTreeStats: *NewRWMap(), + }, + totalNum: (uint64)(0), + concurrentQueue: make(chan struct{}, jobnum), + wg: sync.WaitGroup{}, + } + + return ins, nil +} + +// Run statistics, external call +func (inspect *Inspector) Run() { + accountTrieStat := new(TrieTreeStat) + roothash := inspect.trie.Hash().Big().Uint64() + path := make([]byte, 0) + + ticker := time.NewTicker(30 * time.Second) + go func() { + defer ticker.Stop() + for { + select { + case <-ticker.C: + inspect.trieDB.Cap(DEFAULT_TRIEDBCACHE_SIZE) + } + } + }() + + inspect.result.theTrieTreeStats.Set(roothash, accountTrieStat) + log.Info("Find Account Trie Tree, rootHash: ", inspect.trie.Hash().String(), "BlockNum: ", inspect.blocknum) + inspect.ConcurrentTraversal(inspect.trie, accountTrieStat, inspect.root, 0, path) + inspect.wg.Wait() +} + +func (inspect *Inspector) SubConcurrentTraversal(theTrie *Trie, theTrieTreeStat *TrieTreeStat, theNode node, height uint32, path []byte) { + inspect.concurrentQueue <- struct{}{} + inspect.ConcurrentTraversal(theTrie, theTrieTreeStat, theNode, height, path) + <-inspect.concurrentQueue + inspect.wg.Done() + return +} + +func (inspect *Inspector) ConcurrentTraversal(theTrie *Trie, theTrieTreeStat *TrieTreeStat, theNode node, height uint32, path []byte) { + // print process progress + total_num := atomic.AddUint64(&inspect.totalNum, 1) + if total_num%100000 == 0 { + fmt.Printf("Complete progress: %v, go routines Num: %v, inspect concurrentQueue: %v\n", total_num, runtime.NumGoroutine(), len(inspect.concurrentQueue)) + } + + // nil node + if theNode == nil { + return + } + + switch current := (theNode).(type) { + case *shortNode: + path = append(path, current.Key...) + inspect.ConcurrentTraversal(theTrie, theTrieTreeStat, current.Val, height+1, path) + path = path[:len(path)-len(current.Key)] + case *fullNode: + for idx, child := range current.Children { + if child == nil { + continue + } + childPath := path + childPath = append(childPath, byte(idx)) + if len(inspect.concurrentQueue)*2 < cap(inspect.concurrentQueue) { + inspect.wg.Add(1) + go inspect.SubConcurrentTraversal(theTrie, theTrieTreeStat, child, height+1, childPath) + } else { + inspect.ConcurrentTraversal(theTrie, theTrieTreeStat, child, height+1, childPath) + } + } + case hashNode: + n, err := theTrie.resolveHash(current, path) + if err != nil { + fmt.Printf("Resolve HashNode error: %v, TrieRoot: %v, Height: %v, Path: %v\n", err, theTrie.Hash().String(), height+1, path) + return + } + inspect.ConcurrentTraversal(theTrie, theTrieTreeStat, n, height, path) + return + case valueNode: + if !hasTerm(path) { + break + } + var account Account + if err := rlp.Decode(bytes.NewReader(current), &account); err != nil { + break + } + if account.Root == (common.Hash{}) || account.Root == types.EmptyRootHash { + break + } + root, _ := theTrie.root.cache() + contractTrie, err := New(StorageTrieID(common.BytesToHash(root), common.BytesToHash(hexToKeybytes(path)), account.Root), inspect.trieDB) + if err != nil { + // fmt.Printf("New contract trie node: %v, error: %v, Height: %v, Path: %v\n", theNode, err, height, path) + break + } + trieStat := new(TrieTreeStat) + trieStat.is_account_trie = false + subRootHash := contractTrie.Hash().Big().Uint64() + inspect.result.theTrieTreeStats.Set(subRootHash, trieStat) + contractPath := make([]byte, 0) + // log.Info("Find Contract Trie Tree, rootHash: ", contractTrie.Hash().String(), "") + inspect.wg.Add(1) + go inspect.SubConcurrentTraversal(contractTrie, trieStat, contractTrie.root, 0, contractPath) + default: + panic(errors.New("Invalid node type to traverse.")) + } + theTrieTreeStat.AtomicAdd(theNode, height) + return +} + +func (inspect *Inspector) DisplayResult() { + // display root hash + roothash := inspect.trie.Hash().Big().Uint64() + rootStat, _ := inspect.result.theTrieTreeStats.Get(roothash) + rootStat.Display(roothash, "AccountTrie") + + // display contract trie + trieNodeNums := make([][]uint64, 0, inspect.result.theTrieTreeStats.Len()-1) + var totalContactsNodeStat NodeStat + var contractTrieCnt uint64 = 0 + inspect.result.theTrieTreeStats.Each(func(rootHash uint64, stat *TrieTreeStat) bool { + if rootHash == roothash { + return true + } + contractTrieCnt++ + totalContactsNodeStat.ShortNodeCnt += stat.totalNodeStat.ShortNodeCnt + totalContactsNodeStat.FullNodeCnt += stat.totalNodeStat.FullNodeCnt + totalContactsNodeStat.ValueNodeCnt += stat.totalNodeStat.ValueNodeCnt + totalNodeCnt := stat.totalNodeStat.ShortNodeCnt + stat.totalNodeStat.ValueNodeCnt + stat.totalNodeStat.FullNodeCnt + trieNodeNums = append(trieNodeNums, []uint64{totalNodeCnt, rootHash}) + return true + }) + + fmt.Printf("Contract Trie, total trie num: %v, ShortNodeCnt: %v, FullNodeCnt: %v, ValueNodeCnt: %v\n", + contractTrieCnt, totalContactsNodeStat.ShortNodeCnt, totalContactsNodeStat.FullNodeCnt, totalContactsNodeStat.ValueNodeCnt) + sort.Slice(trieNodeNums, func(i, j int) bool { + return trieNodeNums[i][0] > trieNodeNums[j][0] + }) + // only display top 5 + for i, cntHash := range trieNodeNums { + if i > 5 { + break + } + stat, _ := inspect.result.theTrieTreeStats.Get(cntHash[1]) + stat.Display(cntHash[1], "ContractTrie") + i++ + } +} diff --git a/trie/trie.go b/trie/trie.go index 6f69700f67..cc38d889b5 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -1028,6 +1028,14 @@ func (t *Trie) resolveAndTrack(n hashNode, prefix []byte) (node, error) { return mustDecodeNode(n, blob), nil } +func (t *Trie) resolveHash(n hashNode, prefix []byte) (node, error) { + blob, err := t.reader.node(prefix, common.BytesToHash(n)) + if err != nil { + return nil, err + } + return mustDecodeNode(n, blob), nil +} + // resolveEpochMeta resolve full node's epoch map. func (t *Trie) resolveEpochMeta(n node, epoch types.StateEpoch, prefix []byte) error { if !t.enableExpiry { From 7ac0a0be9621efbc9dd53493b0fcab699fedbd8a Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Tue, 12 Sep 2023 11:23:03 +0800 Subject: [PATCH 28/65] metrics: opt some timer, add more miner metrics; --- core/rawdb/database.go | 4 ++++ core/state/snapshot/conversion.go | 3 --- core/state/state_expiry.go | 6 ++++-- ethdb/fullstatedb.go | 8 +++++--- internal/ethapi/api.go | 5 +++-- miner/worker.go | 3 +-- 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/core/rawdb/database.go b/core/rawdb/database.go index d889796b72..886cb01ded 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -629,6 +629,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { txLookups stat accountSnaps stat storageSnaps stat + snapJournal stat preimages stat bloomBits stat cliqueSnaps stat @@ -710,6 +711,8 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { bloomTrieNodes.Add(size) case bytes.Equal(key, epochMetaPlainStateMeta): epochMetaMetaSize.Add(size) + case bytes.Equal(key, snapshotJournalKey): + snapJournal.Add(size) case bytes.Equal(key, epochMetaSnapshotJournalKey): epochMetaSnapJournalSize.Add(size) case bytes.HasPrefix(key, EpochMetaPlainStatePrefix) && len(key) >= (len(EpochMetaPlainStatePrefix)+common.HashLength): @@ -757,6 +760,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { {"Key-Value store", "Trie preimages", preimages.Size(), preimages.Count()}, {"Key-Value store", "Account snapshot", accountSnaps.Size(), accountSnaps.Count()}, {"Key-Value store", "Storage snapshot", storageSnaps.Size(), storageSnaps.Count()}, + {"Key-Value store", "Snapshot Journal", snapJournal.Size(), snapJournal.Count()}, {"Key-Value store", "Clique snapshots", cliqueSnaps.Size(), cliqueSnaps.Count()}, {"Key-Value store", "Parlia snapshots", parliaSnaps.Size(), parliaSnaps.Count()}, {"Key-Value store", "Singleton metadata", metadata.Size(), metadata.Count()}, diff --git a/core/state/snapshot/conversion.go b/core/state/snapshot/conversion.go index 9e1f9242fc..9e272a4d98 100644 --- a/core/state/snapshot/conversion.go +++ b/core/state/snapshot/conversion.go @@ -414,9 +414,6 @@ func generateTrieRoot(db ethdb.KeyValueWriter, scheme string, it Iterator, accou if err != nil { return stop(err) } - if val.GetEpoch() == 0 { - log.Info("found epoch0") - } enc, _ = rlp.EncodeToBytes(val.GetVal()) } leaf = trieKV{it.Hash(), enc} diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index a10d9f4d40..9b2207fd40 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -40,8 +40,10 @@ func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, stateRoot common.Ha // reviveStorageTrie revive trie's expired state from proof func reviveStorageTrie(addr common.Address, tr Trie, proof types.ReviveStorageProof, targetKey common.Hash) (map[string][]byte, error) { - start := time.Now() - defer reviveStorageTrieTimer.Update(time.Since(start)) + defer func(start time.Time) { + reviveStorageTrieTimer.Update(time.Since(start)) + }(time.Now()) + // Decode keys and proofs key := common.FromHex(proof.Key) if !bytes.Equal(targetKey[:], key) { diff --git a/ethdb/fullstatedb.go b/ethdb/fullstatedb.go index efb92453f9..724094262a 100644 --- a/ethdb/fullstatedb.go +++ b/ethdb/fullstatedb.go @@ -59,9 +59,11 @@ func NewFullStateRPCServer(endpoint string) (FullStateDB, error) { } func (f *FullStateRPCServer) GetStorageReviveProof(stateRoot common.Hash, account common.Address, root common.Hash, prefixKeys, keys []string) ([]types.ReviveStorageProof, error) { - start := time.Now() - defer getStorageProofTimer.Update(time.Since(start)) - getProofMeter.Mark(1) + defer func(start time.Time) { + getStorageProofTimer.Update(time.Since(start)) + }(time.Now()) + + getProofMeter.Mark(int64(len(keys))) // find from lru cache, now it cache key proof uncahcedPrefixKeys := make([]string, 0, len(prefixKeys)) uncahcedKeys := make([]string, 0, len(keys)) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index c46caf2d53..c1cc5844d5 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -766,8 +766,9 @@ func (s *BlockChainAPI) GetProof(ctx context.Context, address common.Address, st // GetStorageReviveProof returns the proof for the given keys. Prefix keys can be specified to obtain partial proof for a given key. // Both keys and prefix keys should have the same length. If user wish to obtain full proof for a given key, the corresponding prefix key should be empty string. func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, stateRoot common.Hash, address common.Address, root common.Hash, storageKeys []string, storagePrefixKeys []string) ([]types.ReviveStorageProof, error) { - start := time.Now() - defer getStorageProofTimer.Update(time.Since(start)) + defer func(start time.Time) { + getStorageProofTimer.Update(time.Since(start)) + }(time.Now()) if len(storageKeys) != len(storagePrefixKeys) { return nil, errors.New("storageKeys and storagePrefixKeys must be same length") diff --git a/miner/worker.go b/miner/worker.go index 75580e1d9d..d641eb899a 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -19,6 +19,7 @@ package miner import ( "errors" "fmt" + "github.com/ethereum/go-ethereum/metrics" "math/big" "sync" "sync/atomic" @@ -35,7 +36,6 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" lru "github.com/hashicorp/golang-lru" @@ -1182,7 +1182,6 @@ func (w *worker) commit(env *environment, interval func(), update bool, start ti // Create a local environment copy, avoid the data race with snapshot state. // https://github.com/ethereum/go-ethereum/issues/24299 env := env.copy() - // If we're post merge, just ignore if !w.isTTDReached(block.Header()) { select { From c9bd5b4a4f3f925149903bf2f34c51ab713e7ca5 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Tue, 12 Sep 2023 11:55:01 +0800 Subject: [PATCH 29/65] =?UTF-8?q?fixbugs:=20fix=20prune=20initial=EF=BC=9B?= =?UTF-8?q?=20trie/trie:=20fix=20some=20migrate=20issues;=20trie:=20fix=20?= =?UTF-8?q?compile=20error;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/geth/dbcmd.go | 37 +++++++++++----------- consensus/parlia/parlia.go | 2 +- core/blockchain.go | 2 +- core/rawdb/database.go | 2 +- core/state/database.go | 3 ++ core/state/pruner/pruner.go | 2 ++ core/state/snapshot/snapshot_value.go | 2 +- core/state/snapshot/snapshot_value_test.go | 16 ++++++++++ core/state/state_expiry.go | 6 ++-- core/state/state_object.go | 20 +++++++----- core/state/trie_prefetcher.go | 2 ++ core/types/state_epoch.go | 11 +++---- internal/ethapi/api.go | 4 +-- params/config.go | 7 ++-- trie/dummy_trie.go | 15 --------- 15 files changed, 73 insertions(+), 58 deletions(-) diff --git a/cmd/geth/dbcmd.go b/cmd/geth/dbcmd.go index 0b0df3fbd2..9db2f4670c 100644 --- a/cmd/geth/dbcmd.go +++ b/cmd/geth/dbcmd.go @@ -18,6 +18,7 @@ package main import ( "bytes" + "errors" "fmt" "math" "os" @@ -373,9 +374,9 @@ func inspectTrie(ctx *cli.Context) error { } var ( - blockNumber uint64 - trieRootHash common.Hash - jobnum uint64 + blockNumber uint64 + blockRoot common.Hash + jobnum uint64 ) stack, _ := makeConfigNode(ctx) @@ -384,13 +385,16 @@ func inspectTrie(ctx *cli.Context) error { db := utils.MakeChainDatabase(ctx, stack, true, true) defer db.Close() - var headerBlockHash common.Hash if ctx.NArg() >= 1 { if ctx.Args().Get(0) == "latest" { - headerHash := rawdb.ReadHeadHeaderHash(db) - blockNumber = *(rawdb.ReadHeaderNumber(db, headerHash)) + headBlock := rawdb.ReadHeadBlock(db) + if headBlock == nil { + return errors.New("failed to load head block") + } + blockNumber = headBlock.NumberU64() + blockRoot = headBlock.Root() } else if ctx.Args().Get(0) == "snapshot" { - trieRootHash = rawdb.ReadSnapshotRoot(db) + blockRoot = rawdb.ReadSnapshotRoot(db) blockNumber = math.MaxUint64 } else { var err error @@ -398,6 +402,9 @@ func inspectTrie(ctx *cli.Context) error { if err != nil { return fmt.Errorf("failed to Parse blocknum, Args[0]: %v, err: %v", ctx.Args().Get(0), err) } + blockHash := rawdb.ReadCanonicalHash(db, blockNumber) + block := rawdb.ReadBlock(db, blockHash, blockNumber) + blockRoot = block.Root() } if ctx.NArg() == 1 { @@ -410,22 +417,14 @@ func inspectTrie(ctx *cli.Context) error { } } - if blockNumber != math.MaxUint64 { - headerBlockHash = rawdb.ReadCanonicalHash(db, blockNumber) - if headerBlockHash == (common.Hash{}) { - return fmt.Errorf("ReadHeadBlockHash empry hash") - } - blockHeader := rawdb.ReadHeader(db, headerBlockHash, blockNumber) - trieRootHash = blockHeader.Root - } - if (trieRootHash == common.Hash{}) { + if (blockRoot == common.Hash{}) { log.Error("Empty root hash") } - fmt.Printf("ReadBlockHeader, root: %v, blocknum: %v\n", trieRootHash, blockNumber) + fmt.Printf("ReadBlockHeader, root: %v, blocknum: %v\n", blockRoot, blockNumber) trieDB := trie.NewDatabase(db) - theTrie, err := trie.New(trie.TrieID(trieRootHash), trieDB) + theTrie, err := trie.New(trie.TrieID(blockRoot), trieDB) if err != nil { - fmt.Printf("fail to new trie tree, err: %v, rootHash: %v\n", err, trieRootHash.String()) + fmt.Printf("fail to new trie tree, err: %v, rootHash: %v\n", err, blockRoot.String()) return err } theInspect, err := trie.NewInspector(trieDB, theTrie, blockNumber, jobnum) diff --git a/consensus/parlia/parlia.go b/consensus/parlia/parlia.go index 8f8fd18cc1..36aa2d96ca 100644 --- a/consensus/parlia/parlia.go +++ b/consensus/parlia/parlia.go @@ -1959,7 +1959,7 @@ func applyMessage( msg.Value(), ) if err != nil { - log.Error("apply message failed", "msg", string(ret), "err", err) + log.Error("apply message failed", "contract", msg.To(), "caller", msg.From(), "data", msg.Data(), "msg", string(ret), "err", err, "dberror", state.Error()) } return msg.Gas() - returnGas, err } diff --git a/core/blockchain.go b/core/blockchain.go index 5bb37607bc..65039242f3 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1869,7 +1869,6 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) if bc.insertStopped() { return 0, nil } - // Start a parallel signature recovery (signer will fluke on fork transition, minimal perf loss) signer := types.MakeSigner(bc.chainConfig, chain[0].Number(), chain[0].Time()) go SenderCacher.RecoverFromBlocks(signer, chain) @@ -2131,6 +2130,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) status, err = bc.writeBlockAndSetHead(block, receipts, logs, statedb, false) } if err != nil { + log.Error("insert chain commit err", "err", err) return it.index, err } // Update the metrics touched during block commit diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 886cb01ded..d6d1d65d59 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -721,7 +721,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { var accounted bool for _, meta := range [][]byte{ databaseVersionKey, headHeaderKey, headBlockKey, headFastBlockKey, - lastPivotKey, fastTrieProgressKey, snapshotDisabledKey, SnapshotRootKey, snapshotJournalKey, + lastPivotKey, fastTrieProgressKey, snapshotDisabledKey, SnapshotRootKey, snapshotGeneratorKey, snapshotRecoveryKey, txIndexTailKey, fastTxLookupLimitKey, uncleanShutdownKey, badBlockKey, transitionStatusKey, skeletonSyncStatusKey, persistentStateIDKey, trieJournalKey, snapshotSyncStatusKey, diff --git a/core/state/database.go b/core/state/database.go index e480315e2e..d005ca1de8 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -19,6 +19,7 @@ package state import ( "errors" "fmt" + "github.com/ethereum/go-ethereum/log" "time" "github.com/ethereum/go-ethereum/common" @@ -202,6 +203,7 @@ func NewDatabaseWithNodeDB(db ethdb.Database, triedb *trie.Database) Database { } } +// TODO(0xbundler): may TrieCacheSize not support in PBSS func NewDatabaseWithConfigAndCache(db ethdb.Database, config *trie.Config) Database { atc, _ := exlru.New(accountTrieCacheSize) stc, _ := exlru.New(storageTrieCacheSize) @@ -278,6 +280,7 @@ func (db *cachingDB) OpenStorageTrie(stateRoot common.Hash, address common.Addre triesPairs := tries.([3]*triePair) for _, triePair := range triesPairs { if triePair != nil && triePair.root == root { + log.Info("OpenStorageTrie hit storageTrieCache", "addr", address, "root", root) return triePair.trie.(*trie.SecureTrie).Copy(), nil } } diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go index 03a0329795..1ea8d344c9 100644 --- a/core/state/pruner/pruner.go +++ b/core/state/pruner/pruner.go @@ -247,6 +247,7 @@ func pruneAll(maindb ethdb.Database, g *core.Genesis) error { } func prune(snaptree *snapshot.Tree, root common.Hash, maindb ethdb.Database, stateBloom *stateBloom, bloomPath string, middleStateRoots map[common.Hash]struct{}, start time.Time) error { + log.Info("Start Prune state data", "root", root) // Delete all stale trie nodes in the disk. With the help of state bloom // the trie nodes(and codes) belong to the active state will be filtered // out. A very small part of stale tries will also be filtered because of @@ -696,6 +697,7 @@ func (p *Pruner) Prune(root common.Hash) error { // recap epoch meta snap, save journal snap := trieDB.EpochMetaSnapTree() if snap != nil { + log.Info("epoch meta snap handle", "root", root) if err := snap.Cap(root); err != nil { log.Error("asyncPruneExpired, SnapTree Cap err", "err", err) return err diff --git a/core/state/snapshot/snapshot_value.go b/core/state/snapshot/snapshot_value.go index e7e0ff3525..3a89bab148 100644 --- a/core/state/snapshot/snapshot_value.go +++ b/core/state/snapshot/snapshot_value.go @@ -97,7 +97,7 @@ func DecodeValueFromRLPBytes(b []byte) (SnapValue, error) { if len(b) == 0 { return &RawValue{}, nil } - if b[0] > 0x7f { + if len(b) == 1 || b[0] > 0x7f { var data RawValue _, data, _, err := rlp.Split(b) if err != nil { diff --git a/core/state/snapshot/snapshot_value_test.go b/core/state/snapshot/snapshot_value_test.go index 7af79a6772..d316c95fbb 100644 --- a/core/state/snapshot/snapshot_value_test.go +++ b/core/state/snapshot/snapshot_value_test.go @@ -2,6 +2,7 @@ package snapshot import ( "encoding/hex" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" "github.com/stretchr/testify/assert" @@ -25,9 +26,24 @@ func TestSnapValEncodeDecode(t *testing.T) { tests := []struct { raw SnapValue }{ + { + raw: NewRawValue(common.FromHex("0x3")), + }, { raw: NewRawValue(val), }, + { + raw: NewValueWithEpoch(types.StateEpoch(0), common.FromHex("0x00")), + }, + { + raw: NewValueWithEpoch(types.StateEpoch(0), common.FromHex("0x3")), + }, + { + raw: NewValueWithEpoch(types.StateEpoch(1), common.FromHex("0x3")), + }, + { + raw: NewValueWithEpoch(types.StateEpoch(0), val), + }, { raw: NewValueWithEpoch(types.StateEpoch(1000), val), }, diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index 9b2207fd40..af58715576 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -21,9 +21,11 @@ func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, stateRoot common.Ha // if no prefix, query from revive trie, got the newest expired info if len(prefixKey) == 0 { _, err := tr.GetStorage(addr, key.Bytes()) - if enErr, ok := err.(*trie.ExpiredNodeError); ok { - prefixKey = enErr.Path + enErr, ok := err.(*trie.ExpiredNodeError) + if !ok { + return nil, fmt.Errorf("cannot find expired state from trie") } + prefixKey = enErr.Path } proofs, err := fullDB.GetStorageReviveProof(stateRoot, addr, root, []string{common.Bytes2Hex(prefixKey)}, []string{common.Bytes2Hex(key[:])}) if err != nil { diff --git a/core/state/state_object.go b/core/state/state_object.go index 318796f1a1..7a469b3f39 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -18,6 +18,7 @@ package state import ( "bytes" + "errors" "fmt" "github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/log" @@ -276,7 +277,7 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { var dbError error sv, err, dbError = s.getExpirySnapStorage(key) if dbError != nil { - s.db.setError(dbError) + s.db.setError(fmt.Errorf("state expiry getExpirySnapStorage, contract: %v, key: %v, err: %v", s.address, key, dbError)) return common.Hash{} } // if query success, just set val, otherwise request from trie @@ -305,7 +306,7 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { start := time.Now() tr, err := s.getTrie() if err != nil { - s.db.setError(err) + s.db.setError(fmt.Errorf("state object getTrie err, contract: %v, err: %v", s.address, err)) return common.Hash{} } val, err := tr.GetStorage(s.address, key.Bytes()) @@ -323,7 +324,7 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { //} } if err != nil { - s.db.setError(err) + s.db.setError(fmt.Errorf("state object get storage err, contract: %v, key: %v, err: %v", s.address, key, err)) return common.Hash{} } value.SetBytes(val) @@ -417,7 +418,7 @@ func (s *stateObject) updateTrie() (Trie, error) { tr, err = s.getTrie() } if err != nil { - s.db.setError(err) + s.db.setError(fmt.Errorf("state object update trie getTrie err, contract: %v, err: %v", s.address, err)) return nil, err } // Insert all the pending updates into the trie @@ -455,12 +456,12 @@ func (s *stateObject) updateTrie() (Trie, error) { for key, value := range dirtyStorage { if len(value) == 0 { if err := tr.DeleteStorage(s.address, key[:]); err != nil { - s.db.setError(err) + s.db.setError(fmt.Errorf("state object update trie DeleteStorage err, contract: %v, key: %v, err: %v", s.address, key, err)) } s.db.StorageDeleted += 1 } else { if err := tr.UpdateStorage(s.address, key[:], value); err != nil { - s.db.setError(err) + s.db.setError(fmt.Errorf("state object update trie UpdateStorage err, contract: %v, key: %v, err: %v", s.address, key, err)) } s.db.StorageUpdated += 1 } @@ -792,7 +793,7 @@ func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash) return nil, err } - log.Debug("fetchExpiredStorageFromRemote in stateDB", "addr", s.address, "prefixKey", prefixKey, "key", key, "tr", fmt.Sprintf("%p", tr)) + log.Info("fetchExpiredStorageFromRemote in stateDB", "addr", s.address, "prefixKey", prefixKey, "key", key, "tr", fmt.Sprintf("%p", tr)) kvs, err := fetchExpiredStorageFromRemote(s.db.fullStateDB, s.db.originalRoot, s.address, s.data.Root, tr, prefixKey, key) if err != nil { @@ -807,7 +808,10 @@ func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash) } getCommittedStorageRemoteMeter.Mark(1) - val := s.pendingReviveState[string(crypto.Keccak256(key[:]))] + val, ok := s.pendingReviveState[string(crypto.Keccak256(key[:]))] + if !ok { + return nil, errors.New("cannot find revived state") + } return val.Bytes(), nil } diff --git a/core/state/trie_prefetcher.go b/core/state/trie_prefetcher.go index 6c495793f9..6d97fa459d 100644 --- a/core/state/trie_prefetcher.go +++ b/core/state/trie_prefetcher.go @@ -136,6 +136,7 @@ func (p *triePrefetcher) mainLoop() { if p.enableStateExpiry { fetcher.initStateExpiryFeature(p.epoch, p.blockHash, p.fullStateDB) } + fetcher.start() p.fetchersMutex.Lock() p.fetchers[id] = fetcher p.fetchersMutex.Unlock() @@ -473,6 +474,7 @@ func (sf *subfetcher) scheduleParallel(keys [][]byte) { if sf.enableStateExpiry { child.initStateExpiryFeature(sf.epoch, sf.blockHash, sf.fullStateDB) } + child.start() sf.paraChildren = append(sf.paraChildren, child) endIndex := (i + 1) * parallelTriePrefetchCapacity if endIndex >= keysLeftSize { diff --git a/core/types/state_epoch.go b/core/types/state_epoch.go index 82192d6433..b03b96ea18 100644 --- a/core/types/state_epoch.go +++ b/core/types/state_epoch.go @@ -8,8 +8,7 @@ import ( ) const ( - //DefaultStateEpochPeriod = uint64(7_008_000) - DefaultStateEpochPeriod = uint64(30) + DefaultStateEpochPeriod = uint64(7_008_000) StateEpoch0 = StateEpoch(0) StateEpoch1 = StateEpoch(1) StateEpochKeepLiveNum = StateEpoch(2) @@ -31,10 +30,10 @@ func GetStateEpoch(config *params.ChainConfig, blockNumber *big.Int) StateEpoch epoch1Block := epochPeriod epoch2Block := new(big.Int).Add(epoch1Block, epochPeriod) - if config.Clique != nil && config.Clique.StateEpochPeriod != 0 { - epochPeriod = new(big.Int).SetUint64(config.Clique.StateEpochPeriod) - epoch1Block = new(big.Int).SetUint64(config.Clique.StateEpoch1Block) - epoch2Block = new(big.Int).SetUint64(config.Clique.StateEpoch2Block) + if config.Parlia != nil && config.Parlia.StateEpochPeriod != 0 { + epochPeriod = new(big.Int).SetUint64(config.Parlia.StateEpochPeriod) + epoch1Block = new(big.Int).SetUint64(config.Parlia.StateEpoch1Block) + epoch2Block = new(big.Int).SetUint64(config.Parlia.StateEpoch2Block) } if isBlockReached(blockNumber, epoch2Block) { ret := new(big.Int).Sub(blockNumber, epoch2Block) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index c1cc5844d5..33f5545939 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -2269,9 +2269,9 @@ func SubmitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (c if tx.To() == nil { addr := crypto.CreateAddress(from, tx.Nonce()) - log.Info("Submitted contract creation", "hash", tx.Hash().Hex(), "from", from, "nonce", tx.Nonce(), "contract", addr.Hex(), "value", tx.Value(), "x-forward-ip", xForward) + log.Debug("Submitted contract creation", "hash", tx.Hash().Hex(), "from", from, "nonce", tx.Nonce(), "contract", addr.Hex(), "value", tx.Value(), "x-forward-ip", xForward) } else { - log.Info("Submitted transaction", "hash", tx.Hash().Hex(), "from", from, "nonce", tx.Nonce(), "recipient", tx.To(), "value", tx.Value(), "x-forward-ip", xForward) + log.Debug("Submitted transaction", "hash", tx.Hash().Hex(), "from", from, "nonce", tx.Nonce(), "recipient", tx.To(), "value", tx.Value(), "x-forward-ip", xForward) } return tx.Hash(), nil } diff --git a/params/config.go b/params/config.go index fc1f40e6de..b4a8cc050e 100644 --- a/params/config.go +++ b/params/config.go @@ -512,8 +512,11 @@ func (c *CliqueConfig) String() string { // ParliaConfig is the consensus engine configs for proof-of-staked-authority based sealing. type ParliaConfig struct { - Period uint64 `json:"period"` // Number of seconds between blocks to enforce - Epoch uint64 `json:"epoch"` // Epoch length to update validatorSet + Period uint64 `json:"period"` // Number of seconds between blocks to enforce + Epoch uint64 `json:"epoch"` // Epoch length to update validatorSet + StateEpoch1Block uint64 `json:"stateEpoch1Block"` + StateEpoch2Block uint64 `json:"stateEpoch2Block"` + StateEpochPeriod uint64 `json:"stateEpochPeriod"` } // String implements the stringer interface, returning the consensus engine details. diff --git a/trie/dummy_trie.go b/trie/dummy_trie.go index 34415ff4a1..3d08803369 100644 --- a/trie/dummy_trie.go +++ b/trie/dummy_trie.go @@ -89,21 +89,6 @@ func (t *EmptyTrie) ProvePath(key []byte, path []byte, proofDb ethdb.KeyValueWri return nil } -func (t *EmptyTrie) ReviveTrie(key []byte, proof []*MPTProofNub) []*MPTProofNub { - return nil -} - -func (t *EmptyTrie) SetEpoch(epoch types.StateEpoch) { -} - -func (t *EmptyTrie) ProvePath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error { - return nil -} - -func (t *EmptyTrie) GetStorageAndUpdateEpoch(addr common.Address, key []byte) ([]byte, error) { - return nil, nil -} - func (t *EmptyTrie) SetEpoch(epoch types.StateEpoch) { } From 91577fb42d106f6f0011efd9eaecf1065f1ea94a Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Wed, 20 Sep 2023 16:54:20 +0800 Subject: [PATCH 30/65] bugfix: fix trie update; --- core/state/database.go | 3 +++ core/state/state_expiry.go | 9 --------- core/state/state_object.go | 32 +++++++++++++++++++++++--------- core/state/trie_prefetcher.go | 3 +++ light/trie.go | 4 ++++ trie/dummy_trie.go | 4 ++++ trie/secure_trie.go | 4 ++++ trie/trie.go | 2 +- trie/trienode/node.go | 7 +++++-- 9 files changed, 47 insertions(+), 21 deletions(-) diff --git a/core/state/database.go b/core/state/database.go index d005ca1de8..f64a363c0b 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -166,6 +166,9 @@ type Trie interface { // SetEpoch set current epoch in trie, it must set in initial period, or it will get error behavior. SetEpoch(types.StateEpoch) + + // Epoch get current epoch in trie + Epoch() types.StateEpoch } // NewDatabase creates a backing store for state. The returned database is safe for diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index af58715576..42eda07de0 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -18,15 +18,6 @@ var ( // fetchExpiredStorageFromRemote request expired state from remote full state node; func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, stateRoot common.Hash, addr common.Address, root common.Hash, tr Trie, prefixKey []byte, key common.Hash) (map[string][]byte, error) { - // if no prefix, query from revive trie, got the newest expired info - if len(prefixKey) == 0 { - _, err := tr.GetStorage(addr, key.Bytes()) - enErr, ok := err.(*trie.ExpiredNodeError) - if !ok { - return nil, fmt.Errorf("cannot find expired state from trie") - } - prefixKey = enErr.Path - } proofs, err := fullDB.GetStorageReviveProof(stateRoot, addr, root, []string{common.Bytes2Hex(prefixKey)}, []string{common.Bytes2Hex(key[:])}) if err != nil { return nil, err diff --git a/core/state/state_object.go b/core/state/state_object.go index 7a469b3f39..e80839f621 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -18,7 +18,6 @@ package state import ( "bytes" - "errors" "fmt" "github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/log" @@ -304,7 +303,12 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { if s.needLoadFromTrie(err, sv) { getCommittedStorageTrieMeter.Mark(1) start := time.Now() - tr, err := s.getTrie() + var tr Trie + if s.db.EnableExpire() { + tr, err = s.getPendingReviveTrie() + } else { + tr, err = s.getTrie() + } if err != nil { s.db.setError(fmt.Errorf("state object getTrie err, contract: %v, err: %v", s.address, err)) return common.Hash{} @@ -316,7 +320,7 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { // handle state expiry situation if s.db.EnableExpire() { if enErr, ok := err.(*trie.ExpiredNodeError); ok { - val, err = s.fetchExpiredFromRemote(enErr.Path, key) + val, err = s.fetchExpiredFromRemote(enErr.Path, key, false) } // TODO(0xbundler): add epoch record cache for prevent frequency access epoch update, may implement later //if err != nil { @@ -446,6 +450,7 @@ func (s *stateObject) updateTrie() (Trie, error) { // it must hit in cache value := s.GetState(key) dirtyStorage[key] = common.TrimLeftZeroes(value[:]) + log.Debug("updateTrie access state", "contract", s.address, "key", key, "epoch", s.db.epoch) } } @@ -464,6 +469,7 @@ func (s *stateObject) updateTrie() (Trie, error) { s.db.setError(fmt.Errorf("state object update trie UpdateStorage err, contract: %v, key: %v, err: %v", s.address, key, err)) } s.db.StorageUpdated += 1 + log.Debug("updateTrie UpdateStorage", "contract", s.address, "key", key, "epoch", s.db.epoch, "value", value, "tr", tr.Epoch()) } // Cache the items for preloading usedStorage = append(usedStorage, common.CopyBytes(key[:])) @@ -499,6 +505,7 @@ func (s *stateObject) updateTrie() (Trie, error) { snapshotVal, _ = rlp.EncodeToBytes(value) } storage[khash] = snapshotVal // snapshotVal will be nil if it's deleted + log.Debug("updateTrie snapshot", "contract", s.address, "key", key, "epoch", s.db.epoch, "value", snapshotVal) // Track the original value of slot only if it's mutated first time prev := s.originStorage[key] @@ -787,12 +794,22 @@ func (s *stateObject) queryFromReviveState(reviveState map[string]common.Hash, k } // fetchExpiredStorageFromRemote request expired state from remote full state node; -func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash) ([]byte, error) { +func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash, resolvePath bool) ([]byte, error) { tr, err := s.getPendingReviveTrie() if err != nil { return nil, err } + // if no prefix, query from revive trie, got the newest expired info + if resolvePath { + _, err := tr.GetStorage(s.address, key.Bytes()) + enErr, ok := err.(*trie.ExpiredNodeError) + if !ok { + return nil, fmt.Errorf("cannot find expired state from trie, err: %v", err) + } + prefixKey = enErr.Path + } + log.Info("fetchExpiredStorageFromRemote in stateDB", "addr", s.address, "prefixKey", prefixKey, "key", key, "tr", fmt.Sprintf("%p", tr)) kvs, err := fetchExpiredStorageFromRemote(s.db.fullStateDB, s.db.originalRoot, s.address, s.data.Root, tr, prefixKey, key) @@ -808,10 +825,7 @@ func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash) } getCommittedStorageRemoteMeter.Mark(1) - val, ok := s.pendingReviveState[string(crypto.Keccak256(key[:]))] - if !ok { - return nil, errors.New("cannot find revived state") - } + val := s.pendingReviveState[string(crypto.Keccak256(key[:]))] return val.Bytes(), nil } @@ -837,7 +851,7 @@ func (s *stateObject) getExpirySnapStorage(key common.Hash) (snapshot.SnapValue, } // handle from remoteDB, if got err just setError, just return to revert in consensus version. - valRaw, err := s.fetchExpiredFromRemote(nil, key) + valRaw, err := s.fetchExpiredFromRemote(nil, key, true) if err != nil { return nil, nil, err } diff --git a/core/state/trie_prefetcher.go b/core/state/trie_prefetcher.go index 6d97fa459d..eedd597992 100644 --- a/core/state/trie_prefetcher.go +++ b/core/state/trie_prefetcher.go @@ -548,6 +548,9 @@ func (sf *subfetcher) loop() { } else { // address is useless sf.trie, err = sf.db.OpenStorageTrie(sf.state, sf.addr, sf.root) + if err == nil && sf.enableStateExpiry { + trie.SetEpoch(sf.epoch) + } } if err != nil { continue diff --git a/light/trie.go b/light/trie.go index d8b23e7667..3daa7025bd 100644 --- a/light/trie.go +++ b/light/trie.go @@ -228,6 +228,10 @@ func (t *odrTrie) ProvePath(key []byte, path []byte, proofDb ethdb.KeyValueWrite return errors.New("not implemented, needs client/server interface split") } +func (t *odrTrie) Epoch() types.StateEpoch { + return types.StateEpoch0 +} + // do tries and retries to execute a function until it returns with no error or // an error type other than MissingNodeError func (t *odrTrie) do(key []byte, fn func() error) error { diff --git a/trie/dummy_trie.go b/trie/dummy_trie.go index 3d08803369..4ee8cce1e6 100644 --- a/trie/dummy_trie.go +++ b/trie/dummy_trie.go @@ -26,6 +26,10 @@ import ( type EmptyTrie struct{} +func (t *EmptyTrie) Epoch() types.StateEpoch { + return types.StateEpoch0 +} + // NewSecure creates a dummy trie func NewEmptyTrie() *EmptyTrie { return &EmptyTrie{} diff --git a/trie/secure_trie.go b/trie/secure_trie.go index a2c0f5dccd..b695609997 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -258,6 +258,10 @@ func (t *StateTrie) SetEpoch(epoch types.StateEpoch) { t.trie.SetEpoch(epoch) } +func (t *StateTrie) Epoch() types.StateEpoch { + return t.trie.currentEpoch +} + // Copy returns a copy of StateTrie. func (t *StateTrie) Copy() *StateTrie { return &StateTrie{ diff --git a/trie/trie.go b/trie/trie.go index cc38d889b5..d2b3c93001 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -1388,7 +1388,7 @@ func (t *Trie) renewNode(epoch types.StateEpoch, childDirty bool, updateEpoch bo } // when no epoch update, same as before - if epoch == t.getRootEpoch() { + if epoch == t.currentEpoch { return childDirty } diff --git a/trie/trienode/node.go b/trie/trienode/node.go index e23216995e..3b0b71e9e5 100644 --- a/trie/trienode/node.go +++ b/trie/trienode/node.go @@ -130,7 +130,7 @@ func (set *NodeSet) AddAccountMeta(meta types.StateMeta) error { } // Merge adds a set of nodes into the set. -func (set *NodeSet) Merge(owner common.Hash, nodes map[string]*Node) error { +func (set *NodeSet) Merge(owner common.Hash, nodes map[string]*Node, metas map[string][]byte) error { if set.Owner != owner { return fmt.Errorf("nodesets belong to different owner are not mergeable %x-%x", set.Owner, owner) } @@ -146,6 +146,9 @@ func (set *NodeSet) Merge(owner common.Hash, nodes map[string]*Node) error { } set.AddNode([]byte(path), node) } + for path, meta := range metas { + set.EpochMetas[path] = meta + } return nil } @@ -213,7 +216,7 @@ func NewWithNodeSet(set *NodeSet) *MergedNodeSet { func (set *MergedNodeSet) Merge(other *NodeSet) error { subset, present := set.Sets[other.Owner] if present { - return subset.Merge(other.Owner, other.Nodes) + return subset.Merge(other.Owner, other.Nodes, other.EpochMetas) } set.Sets[other.Owner] = other return nil From 16f91a875015ff06bf7b881a3a888e943896012e Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Fri, 22 Sep 2023 15:50:58 +0800 Subject: [PATCH 31/65] bugfix: fix state prefetcher concurrent bugs; bugfix: fix trie epoch update bugs; --- .github/workflows/commit-lint.yml | 2 ++ .github/workflows/integration-test.yml | 2 ++ .github/workflows/lint.yml | 2 ++ .github/workflows/unit-test.yml | 2 ++ consensus/parlia/parlia.go | 18 +++++++------- core/state/state_object.go | 10 ++++---- core/state/trie_prefetcher.go | 2 -- core/state_prefetcher.go | 9 +++++-- trie/proof.go | 18 ++++++++++++++ trie/trie.go | 33 +++++++++++++++++++------- 10 files changed, 72 insertions(+), 26 deletions(-) diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml index 7df13ebcac..4ace940171 100644 --- a/.github/workflows/commit-lint.yml +++ b/.github/workflows/commit-lint.yml @@ -5,11 +5,13 @@ on: branches: - master - develop + - state_expiry_mvp0.1_dev pull_request: branches: - master - develop + - state_expiry_mvp0.1_dev jobs: commitlint: diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 234cbfda5b..393930ed92 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -5,11 +5,13 @@ on: branches: - master - develop + - state_expiry_mvp0.1_dev pull_request: branches: - master - develop + - state_expiry_mvp0.1_dev jobs: truffle-test: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 87bfb710dd..a8b0a49dc4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,11 +5,13 @@ on: branches: - master - develop + - state_expiry_mvp0.1_dev pull_request: branches: - master - develop + - state_expiry_mvp0.1_dev jobs: golang-lint: diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index f4d9e053f2..fd860cfe6e 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -5,11 +5,13 @@ on: branches: - master - develop + - state_expiry_mvp0.1_dev pull_request: branches: - master - develop + - state_expiry_mvp0.1_dev jobs: unit-test: diff --git a/consensus/parlia/parlia.go b/consensus/parlia/parlia.go index 36aa2d96ca..8ce71f76a1 100644 --- a/consensus/parlia/parlia.go +++ b/consensus/parlia/parlia.go @@ -1122,7 +1122,7 @@ func (p *Parlia) Finalize(chain consensus.ChainHeaderReader, header *types.Heade err = p.slash(spoiledVal, state, header, cx, txs, receipts, systemTxs, usedGas, false) if err != nil { // it is possible that slash validator failed because of the slash channel is disabled. - log.Error("slash validator failed", "block hash", header.Hash(), "address", spoiledVal) + log.Error("slash validator failed", "block hash", header.Hash(), "address", spoiledVal, "err", err) } } } @@ -1183,7 +1183,7 @@ func (p *Parlia) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header * err = p.slash(spoiledVal, state, header, cx, &txs, &receipts, nil, &header.GasUsed, true) if err != nil { // it is possible that slash validator failed because of the slash channel is disabled. - log.Error("slash validator failed", "block hash", header.Hash(), "address", spoiledVal) + log.Error("slash validator failed", "block hash", header.Hash(), "address", spoiledVal, "err", err) } } } @@ -1692,13 +1692,13 @@ func (p *Parlia) applyTransaction( } actualTx := (*receivedTxs)[0] if !bytes.Equal(p.signer.Hash(actualTx).Bytes(), expectedHash.Bytes()) { - return fmt.Errorf("expected tx hash %v, get %v, nonce %d, to %s, value %s, gas %d, gasPrice %s, data %s", expectedHash.String(), actualTx.Hash().String(), - expectedTx.Nonce(), - expectedTx.To().String(), - expectedTx.Value().String(), - expectedTx.Gas(), - expectedTx.GasPrice().String(), - hex.EncodeToString(expectedTx.Data()), + return fmt.Errorf("expected tx hash %v, get %v, nonce %d:%d, to %s:%s, value %s:%s, gas %d:%d, gasPrice %s:%s, data %s:%s", expectedHash.String(), actualTx.Hash().String(), + expectedTx.Nonce(), actualTx.Nonce(), + expectedTx.To().String(), actualTx.To().String(), + expectedTx.Value().String(), actualTx.Value().String(), + expectedTx.Gas(), actualTx.Gas(), + expectedTx.GasPrice().String(), actualTx.GasPrice().String(), + hex.EncodeToString(expectedTx.Data()), hex.EncodeToString(actualTx.Data()), ) } expectedTx = actualTx diff --git a/core/state/state_object.go b/core/state/state_object.go index e80839f621..2eb184677b 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -469,7 +469,6 @@ func (s *stateObject) updateTrie() (Trie, error) { s.db.setError(fmt.Errorf("state object update trie UpdateStorage err, contract: %v, key: %v, err: %v", s.address, key, err)) } s.db.StorageUpdated += 1 - log.Debug("updateTrie UpdateStorage", "contract", s.address, "key", key, "epoch", s.db.epoch, "value", value, "tr", tr.Epoch()) } // Cache the items for preloading usedStorage = append(usedStorage, common.CopyBytes(key[:])) @@ -505,7 +504,6 @@ func (s *stateObject) updateTrie() (Trie, error) { snapshotVal, _ = rlp.EncodeToBytes(value) } storage[khash] = snapshotVal // snapshotVal will be nil if it's deleted - log.Debug("updateTrie snapshot", "contract", s.address, "key", key, "epoch", s.db.epoch, "value", snapshotVal) // Track the original value of slot only if it's mutated first time prev := s.originStorage[key] @@ -810,7 +808,6 @@ func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash, prefixKey = enErr.Path } - log.Info("fetchExpiredStorageFromRemote in stateDB", "addr", s.address, "prefixKey", prefixKey, "key", key, "tr", fmt.Sprintf("%p", tr)) kvs, err := fetchExpiredStorageFromRemote(s.db.fullStateDB, s.db.originalRoot, s.address, s.data.Root, tr, prefixKey, key) if err != nil { @@ -850,11 +847,16 @@ func (s *stateObject) getExpirySnapStorage(key common.Hash) (snapshot.SnapValue, return val, nil, nil } + // TODO(0xbundler): if found value not been pruned, just return + //if len(val.GetVal()) > 0 { + // return val, nil, nil + //} + // handle from remoteDB, if got err just setError, just return to revert in consensus version. valRaw, err := s.fetchExpiredFromRemote(nil, key, true) if err != nil { return nil, nil, err } - return snapshot.NewValueWithEpoch(s.db.epoch, valRaw), nil, nil + return snapshot.NewValueWithEpoch(val.GetEpoch(), valRaw), nil, nil } diff --git a/core/state/trie_prefetcher.go b/core/state/trie_prefetcher.go index eedd597992..56103e0c79 100644 --- a/core/state/trie_prefetcher.go +++ b/core/state/trie_prefetcher.go @@ -17,7 +17,6 @@ package state import ( - "fmt" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" "sync" @@ -589,7 +588,6 @@ func (sf *subfetcher) loop() { if sf.enableStateExpiry { if exErr, match := err.(*trie2.ExpiredNodeError); match { key := common.BytesToHash(task) - log.Debug("fetchExpiredStorageFromRemote in trie prefetcher", "addr", sf.addr, "prefixKey", exErr.Path, "key", key, "tr", fmt.Sprintf("%p", sf.trie)) _, err = fetchExpiredStorageFromRemote(sf.fullStateDB, sf.state, sf.addr, sf.root, sf.trie, exErr.Path, key) if err != nil { log.Error("subfetcher fetchExpiredStorageFromRemote err", "addr", sf.addr, "path", exErr.Path, "err", err) diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go index f1bb60febd..f8b7fb5fd5 100644 --- a/core/state_prefetcher.go +++ b/core/state_prefetcher.go @@ -59,7 +59,9 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c for i := 0; i < prefetchThread; i++ { go func() { newStatedb := statedb.CopyDoPrefetch() - newStatedb.EnableWriteOnSharedStorage() + if !statedb.EnableExpire() { + newStatedb.EnableWriteOnSharedStorage() + } gaspool := new(GasPool).AddGas(block.GasLimit()) blockContext := NewEVMBlockContext(header, p.bc, nil) evm := vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, *cfg) @@ -106,7 +108,10 @@ func (p *statePrefetcher) PrefetchMining(txs TransactionsByPriceAndNonce, header go func(startCh <-chan *types.Transaction, stopCh <-chan struct{}) { idx := 0 newStatedb := statedb.CopyDoPrefetch() - newStatedb.EnableWriteOnSharedStorage() + // TODO(0xbundler): access empty in trie cause shared concurrent bug? opt later + if !statedb.EnableExpire() { + newStatedb.EnableWriteOnSharedStorage() + } gaspool := new(GasPool).AddGas(gasLimit) blockContext := NewEVMBlockContext(header, p.bc, nil) evm := vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg) diff --git a/trie/proof.go b/trie/proof.go index edfde61b4f..f7fc1cd4a1 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -18,6 +18,7 @@ package trie import ( "bytes" + "encoding/hex" "errors" "fmt" "github.com/ethereum/go-ethereum/rlp" @@ -906,6 +907,23 @@ func (m *MPTProofNub) GetValue() []byte { return nil } +func (m *MPTProofNub) String() string { + buf := bytes.NewBuffer(nil) + buf.WriteString("n1: ") + buf.WriteString(hex.EncodeToString(m.n1PrefixKey)) + buf.WriteString(", n1proof: ") + if m.n1 != nil { + buf.WriteString(m.n1.fstring("")) + } + buf.WriteString(", n2: ") + buf.WriteString(hex.EncodeToString(m.n2PrefixKey)) + buf.WriteString(", n2proof: ") + if m.n2 != nil { + buf.WriteString(m.n2.fstring("")) + } + return buf.String() +} + func getNubValue(origin node, prefixKey []byte) []byte { switch n := origin.(type) { case nil, hashNode: diff --git a/trie/trie.go b/trie/trie.go index d2b3c93001..27ff3134f4 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -55,7 +55,7 @@ type Trie struct { unhashed int // reader is the handler trie can retrieve nodes from. - reader *trieReader // TODO (asyukii): create a reader for state expiry metadata + reader *trieReader // tracer is the tool to track the trie changes. // It will be reset after each commit operation. @@ -211,6 +211,7 @@ func (t *Trie) GetAndUpdateEpoch(key []byte) (value []byte, err error) { if err == nil && didResolve { t.root = newroot + t.rootEpoch = t.currentEpoch } return value, err } @@ -324,8 +325,8 @@ func (t *Trie) updateChildNodeEpoch(origNode node, key []byte, pos int, epoch ty n = n.copy() n.Val = newnode n.setEpoch(t.currentEpoch) + n.flags = t.newFlag() } - return n, true, err case *fullNode: newnode, updateEpoch, err = t.updateChildNodeEpoch(n.Children[key[pos]], key, pos+1, epoch) @@ -334,6 +335,7 @@ func (t *Trie) updateChildNodeEpoch(origNode node, key []byte, pos int, epoch ty n.Children[key[pos]] = newnode n.setEpoch(t.currentEpoch) n.UpdateChildEpoch(int(key[pos]), t.currentEpoch) + n.flags = t.newFlag() } return n, true, err case hashNode: @@ -627,12 +629,14 @@ func (t *Trie) insertWithEpoch(n node, prefix, key []byte, value node, epoch typ // Replace this shortNode with the branch if it occurs at index 0. if matchlen == 0 { + t.tracer.onExpandToBranchNode(prefix) return true, branch, nil } // New branch node is created as a child of the original short node. // Track the newly inserted node in the tracer. The node identifier // passed is the path from the root node. t.tracer.onInsert(append(prefix, key[:matchlen]...)) + t.tracer.onExpandToBranchNode(append(prefix, key[:matchlen]...)) // Replace it with a short node leading up to the branch. return true, &shortNode{Key: key[:matchlen], Val: branch, flags: t.newFlag(), epoch: t.currentEpoch}, nil @@ -908,6 +912,8 @@ func (t *Trie) deleteWithEpoch(n node, prefix, key []byte, epoch types.StateEpoc n = n.copy() n.flags = t.newFlag() n.Children[key[0]] = nn + n.setEpoch(t.currentEpoch) + n.UpdateChildEpoch(int(key[0]), t.currentEpoch) // Because n is a full node, it must've contained at least two children // before the delete operation. If the new child value is non-nil, n still @@ -990,7 +996,7 @@ func (t *Trie) deleteWithEpoch(n node, prefix, key []byte, epoch types.StateEpoc } dirty, nn, err := t.deleteWithEpoch(rn, prefix, key, epoch) - if !dirty || err != nil { + if !t.renewNode(epoch, dirty, true) || err != nil { return false, rn, err } return true, nn, nil @@ -1229,8 +1235,15 @@ func (t *Trie) tryRevive(n node, key []byte, targetPrefixKey []byte, nub MPTProo if err != nil { return nil, false, fmt.Errorf("update child node epoch while reviving failed, err: %v", err) } + n1 = n1.copy() n1.Val = newnode - return n1, true, nil + n1.flags = t.newFlag() + tryUpdateNodeEpoch(nub.n1, t.currentEpoch) + renew, _, err := t.updateChildNodeEpoch(nub.n1, key, pos, t.currentEpoch) + if err != nil { + return nil, false, fmt.Errorf("update child node epoch while reviving failed, err: %v", err) + } + return renew, true, nil } tryUpdateNodeEpoch(nub.n1, t.currentEpoch) @@ -1256,6 +1269,7 @@ func (t *Trie) tryRevive(n node, key []byte, targetPrefixKey []byte, nub MPTProo n = n.copy() n.Val = newNode n.setEpoch(t.currentEpoch) + n.flags = t.newFlag() } return n, didRevive, err case *fullNode: @@ -1267,6 +1281,7 @@ func (t *Trie) tryRevive(n node, key []byte, targetPrefixKey []byte, nub MPTProo n.Children[childIndex] = newNode n.setEpoch(t.currentEpoch) n.UpdateChildEpoch(childIndex, t.currentEpoch) + n.flags = t.newFlag() } if e, ok := err.(*ExpiredNodeError); ok { @@ -1387,13 +1402,13 @@ func (t *Trie) renewNode(epoch types.StateEpoch, childDirty bool, updateEpoch bo return childDirty } - // when no epoch update, same as before - if epoch == t.currentEpoch { - return childDirty + // node need update epoch, just renew + if t.currentEpoch > epoch { + return true } - // node need update epoch, just renew - return true + // when no epoch update, same as before + return childDirty } func (t *Trie) epochExpired(n node, epoch types.StateEpoch) bool { From 4b7801358788a11488470ec0239ebc9403c113a7 Mon Sep 17 00:00:00 2001 From: asyukii Date: Mon, 25 Sep 2023 22:48:02 +0800 Subject: [PATCH 32/65] fix: rebase bsc/pbss_active conflicts --- cmd/geth/dbcmd.go | 2 +- core/blockchain.go | 20 ++++++++++---------- core/state/pruner/pruner.go | 2 +- core/state/state_expiry.go | 1 + eth/state_accessor.go | 2 +- internal/ethapi/api_test.go | 4 ++++ internal/ethapi/transaction_args_test.go | 4 ++++ trie/database.go | 7 +++++++ trie/proof_test.go | 4 ++-- trie/trie_test.go | 2 +- 10 files changed, 32 insertions(+), 16 deletions(-) diff --git a/cmd/geth/dbcmd.go b/cmd/geth/dbcmd.go index 9db2f4670c..86e8154394 100644 --- a/cmd/geth/dbcmd.go +++ b/cmd/geth/dbcmd.go @@ -421,7 +421,7 @@ func inspectTrie(ctx *cli.Context) error { log.Error("Empty root hash") } fmt.Printf("ReadBlockHeader, root: %v, blocknum: %v\n", blockRoot, blockNumber) - trieDB := trie.NewDatabase(db) + trieDB := trie.NewDatabase(db, nil) theTrie, err := trie.New(trie.TrieID(blockRoot), trieDB) if err != nil { fmt.Printf("fail to new trie tree, err: %v, rootHash: %v\n", err, blockRoot.String()) diff --git a/core/blockchain.go b/core/blockchain.go index 65039242f3..0c979f52b9 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1248,20 +1248,20 @@ func (bc *BlockChain) Stop() { log.Error("Dangling trie nodes after full cleanup") } } - } - epochMetaSnapTree := bc.triedb.EpochMetaSnapTree() - if epochMetaSnapTree != nil { - if err := epochMetaSnapTree.Journal(); err != nil { - log.Error("Failed to journal epochMetaSnapTree", "err", err) + epochMetaSnapTree := bc.triedb.EpochMetaSnapTree() + if epochMetaSnapTree != nil { + if err := epochMetaSnapTree.Journal(); err != nil { + log.Error("Failed to journal epochMetaSnapTree", "err", err) + } } - } - // Flush the collected preimages to disk - if err := bc.stateCache.TrieDB().Close(); err != nil { - log.Error("Failed to close trie db", "err", err) + // Flush the collected preimages to disk + if err := bc.stateCache.TrieDB().Close(); err != nil { + log.Error("Failed to close trie db", "err", err) + } + log.Info("Blockchain stopped") } - log.Info("Blockchain stopped") } // StopInsert interrupts all insertion methods, causing them to return diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go index 1ea8d344c9..8a59a4443f 100644 --- a/core/state/pruner/pruner.go +++ b/core/state/pruner/pruner.go @@ -671,7 +671,7 @@ func (p *Pruner) Prune(root common.Hash) error { rets = make([]error, 3) expiryWG sync.WaitGroup ) - trieDB := trie.NewDatabaseWithConfig(p.db, &trie.Config{ + trieDB := trie.NewDatabase(p.db, &trie.Config{ EnableStateExpiry: true, PathDB: nil, // TODO(0xbundler): support later }) diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index 42eda07de0..6870205fa8 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -18,6 +18,7 @@ var ( // fetchExpiredStorageFromRemote request expired state from remote full state node; func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, stateRoot common.Hash, addr common.Address, root common.Hash, tr Trie, prefixKey []byte, key common.Hash) (map[string][]byte, error) { + log.Debug("fetching expired storage from remoteDB", "addr", addr, "prefix", prefixKey, "key", key) proofs, err := fullDB.GetStorageReviveProof(stateRoot, addr, root, []string{common.Bytes2Hex(prefixKey)}, []string{common.Bytes2Hex(key[:])}) if err != nil { return nil, err diff --git a/eth/state_accessor.go b/eth/state_accessor.go index dd4aefa9bc..445a57fc4d 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -195,7 +195,7 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u func (eth *Ethereum) pathState(block *types.Block) (*state.StateDB, func(), error) { // Check if the requested state is available in the live chain. - statedb, err := eth.blockchain.StateAt(block.Root()) + statedb, err := eth.blockchain.StateAt(block.Root(), block.Hash(), block.Number()) if err == nil { return statedb, noopReleaser, nil } diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 5b1b52f567..a5d9e332a8 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -370,6 +370,10 @@ func newTestBackend(t *testing.T, n int, gspec *core.Genesis, generator func(i i return backend } +func (b *testBackend) StorageTrie(stateRoot common.Hash, addr common.Address, root common.Hash) (state.Trie, error) { + panic("not implemented") +} + // nolint:unused func (b *testBackend) setPendingBlock(block *types.Block) { b.pending = block diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index fc42df3ddb..a5b53bab6a 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -243,6 +243,10 @@ func newBackendMock() *backendMock { } } +func (b *backendMock) StorageTrie(stateRoot common.Hash, addr common.Address, root common.Hash) (state.Trie, error) { + panic("not implemented") +} + func (b *backendMock) activateLondon() { b.current.Number = big.NewInt(1100) } diff --git a/trie/database.go b/trie/database.go index 1aeec8a945..15d0178539 100644 --- a/trie/database.go +++ b/trie/database.go @@ -166,6 +166,13 @@ func NewDatabase(diskdb ethdb.Database, config *Config) *Database { } db.backend = hashdb.New(diskdb, config.HashDB, mptResolver{}) } + if config != nil && config.EnableStateExpiry { + snapTree, err := epochmeta.NewEpochMetaSnapTree(diskdb) + if err != nil { + panic(fmt.Sprintf("init SnapshotTree err: %v", err)) + } + db.snapTree = snapTree + } return db } diff --git a/trie/proof_test.go b/trie/proof_test.go index 220200e821..16beee31b7 100644 --- a/trie/proof_test.go +++ b/trie/proof_test.go @@ -74,7 +74,7 @@ func makeProvers(trie *Trie) []func(key []byte) *memorydb.Database { } func TestOneElementPathProof(t *testing.T) { - trie := NewEmpty(NewDatabase(rawdb.NewMemoryDatabase())) + trie := NewEmpty(NewDatabase(rawdb.NewMemoryDatabase(), nil)) updateString(trie, "k", "v") var proofList proofList @@ -1108,7 +1108,7 @@ func nonRandomTrie(n int) (*Trie, map[string]*kv) { } func nonRandomTrieWithExpiry(n int) (*Trie, map[string]*kv) { - db := NewDatabase(rawdb.NewMemoryDatabase()) + db := NewDatabase(rawdb.NewMemoryDatabase(), nil) trie := NewEmpty(db) trie.currentEpoch = 10 trie.rootEpoch = 10 diff --git a/trie/trie_test.go b/trie/trie_test.go index b9f09dd641..c35f1609ee 100644 --- a/trie/trie_test.go +++ b/trie/trie_test.go @@ -1080,7 +1080,7 @@ func TestReviveCustom(t *testing.T) { } func createCustomTrie(data map[string]string, epoch types.StateEpoch) *Trie { - db := NewDatabase(rawdb.NewMemoryDatabase()) + db := NewDatabase(rawdb.NewMemoryDatabase(), nil) trie := NewEmpty(db) trie.rootEpoch = epoch trie.currentEpoch = epoch From 26e7967ba8181a4ef0ff6c537b0003e152d803c9 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Tue, 26 Sep 2023 15:25:50 +0800 Subject: [PATCH 33/65] trie/proof: fix pbss trie proof generate bug; --- trie/proof.go | 32 +++++++++++++++----------------- trie/triedb/pathdb/difflayer.go | 2 +- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/trie/proof.go b/trie/proof.go index f7fc1cd4a1..a88ae6eacb 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -118,17 +118,15 @@ func (t *StateTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error { // If the trie contains the key, the returned node is the node that contains the // value for the key. If nodes is specified, the traversed nodes are appended to // it. -func (t *Trie) traverseNodes(tn node, key []byte, nodes *[]node, epoch types.StateEpoch, updateEpoch bool) (node, error) { - - var prefix []byte - - for len(key) > 0 && tn != nil { +func (t *Trie) traverseNodes(tn node, prefixKey, suffixKey []byte, nodes *[]node, epoch types.StateEpoch, updateEpoch bool) (node, error) { + for len(suffixKey) > 0 && tn != nil { + log.Info("traverseNodes loop", "prefix", common.Bytes2Hex(prefixKey), "suffix", common.Bytes2Hex(suffixKey), "n", tn.fstring("")) switch n := tn.(type) { case *shortNode: - if len(key) >= len(n.Key) && bytes.Equal(n.Key, key[:len(n.Key)]) { + if len(suffixKey) >= len(n.Key) && bytes.Equal(n.Key, suffixKey[:len(n.Key)]) { tn = n.Val - prefix = append(prefix, n.Key...) - key = key[len(n.Key):] + prefixKey = append(prefixKey, n.Key...) + suffixKey = suffixKey[len(n.Key):] if nodes != nil { *nodes = append(*nodes, n) } @@ -142,8 +140,8 @@ func (t *Trie) traverseNodes(tn node, key []byte, nodes *[]node, epoch types.Sta // if there is a extern node, must put the val hn, isExternNode := n.Val.(hashNode) if isExternNode && nodes != nil { - prefix = append(prefix, n.Key...) - nextBlob, err := t.reader.node(prefix, common.BytesToHash(hn)) + prefixKey = append(prefixKey, n.Key...) + nextBlob, err := t.reader.node(prefixKey, common.BytesToHash(hn)) if err != nil { log.Error("Unhandled next trie error in traverseNodes", "err", err) return nil, err @@ -152,9 +150,9 @@ func (t *Trie) traverseNodes(tn node, key []byte, nodes *[]node, epoch types.Sta *nodes = append(*nodes, next) } case *fullNode: - tn = n.Children[key[0]] - prefix = append(prefix, key[0]) - key = key[1:] + tn = n.Children[suffixKey[0]] + prefixKey = append(prefixKey, suffixKey[0]) + suffixKey = suffixKey[1:] if nodes != nil { *nodes = append(*nodes, n) } @@ -164,7 +162,7 @@ func (t *Trie) traverseNodes(tn node, key []byte, nodes *[]node, epoch types.Sta // loaded blob will be tracked, while it's not required here since // all loaded nodes won't be linked to trie at all and track nodes // may lead to out-of-memory issue. - blob, err := t.reader.node(prefix, common.BytesToHash(n)) + blob, err := t.reader.node(prefixKey, common.BytesToHash(n)) if err != nil { log.Error("Unhandled trie error in traverseNodes", "err", err) return nil, err @@ -173,7 +171,7 @@ func (t *Trie) traverseNodes(tn node, key []byte, nodes *[]node, epoch types.Sta // clean cache or the database, they are all in their own // copy and safe to use unsafe decoder. tn = mustDecodeNodeUnsafe(n, blob) - if err = t.resolveEpochMeta(tn, epoch, prefix); err != nil { + if err = t.resolveEpochMeta(tn, epoch, prefixKey); err != nil { return nil, err } default: @@ -199,7 +197,7 @@ func (t *Trie) ProvePath(key []byte, prefixKeyHex []byte, proofDb ethdb.KeyValue // traverse down using the prefixKeyHex var nodes []node tn := t.root - startNode, err := t.traverseNodes(tn, prefixKeyHex, nil, 0, false) // obtain the node where the prefixKeyHex leads to + startNode, err := t.traverseNodes(tn, nil, prefixKeyHex, nil, 0, false) // obtain the node where the prefixKeyHex leads to if err != nil { return err } @@ -207,7 +205,7 @@ func (t *Trie) ProvePath(key []byte, prefixKeyHex []byte, proofDb ethdb.KeyValue key = key[len(prefixKeyHex):] // obtain the suffix key // traverse through the suffix key - _, err = t.traverseNodes(startNode, key, &nodes, 0, false) + _, err = t.traverseNodes(startNode, prefixKeyHex, key, &nodes, 0, false) if err != nil { return err } diff --git a/trie/triedb/pathdb/difflayer.go b/trie/triedb/pathdb/difflayer.go index d25ac1c601..bea0208461 100644 --- a/trie/triedb/pathdb/difflayer.go +++ b/trie/triedb/pathdb/difflayer.go @@ -113,7 +113,7 @@ func (dl *diffLayer) node(owner common.Hash, path []byte, hash common.Hash, dept // bubble up an error here. It shouldn't happen at all. if n.Hash != hash { dirtyFalseMeter.Mark(1) - log.Error("Unexpected trie node in diff layer", "owner", owner, "path", path, "expect", hash, "got", n.Hash) + log.Error("Unexpected trie node in diff layer", "root", dl.root, "owner", owner, "path", path, "expect", hash, "got", n.Hash) return nil, newUnexpectedNodeError("diff", hash, n.Hash, owner, path) } dirtyHitMeter.Mark(1) From 7dde14a7dbe4b16bd6da6f423c34b1a5857d1679 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Tue, 26 Sep 2023 23:22:08 +0800 Subject: [PATCH 34/65] pruner: support PBSS expired state prune; --- cmd/geth/snapshot.go | 23 +++- core/blockchain.go | 6 +- core/state/pruner/pruner.go | 126 ++++++++++++-------- core/state/snapshot/snapshot_expire.go | 21 ++-- core/state/snapshot/snapshot_expire_test.go | 2 +- 5 files changed, 115 insertions(+), 63 deletions(-) diff --git a/cmd/geth/snapshot.go b/cmd/geth/snapshot.go index 54ef78076a..86b12b3533 100644 --- a/cmd/geth/snapshot.go +++ b/cmd/geth/snapshot.go @@ -424,20 +424,39 @@ func pruneState(ctx *cli.Context) error { return err } - if rawdb.ReadStateScheme(chaindb) != rawdb.HashScheme { - log.Crit("Offline pruning is not required for path scheme") + cacheConfig := &core.CacheConfig{ + TrieCleanLimit: cfg.Eth.TrieCleanCache, + TrieCleanNoPrefetch: cfg.Eth.NoPrefetch, + TrieDirtyLimit: cfg.Eth.TrieDirtyCache, + TrieDirtyDisabled: cfg.Eth.NoPruning, + TrieTimeLimit: cfg.Eth.TrieTimeout, + NoTries: cfg.Eth.TriesVerifyMode != core.LocalVerify, + SnapshotLimit: cfg.Eth.SnapshotCache, + TriesInMemory: cfg.Eth.TriesInMemory, + Preimages: cfg.Eth.Preimages, + StateHistory: cfg.Eth.StateHistory, + StateScheme: cfg.Eth.StateScheme, + EnableStateExpiry: cfg.Eth.StateExpiryEnable, + RemoteEndPoint: cfg.Eth.StateExpiryFullStateEndpoint, } prunerconfig := pruner.Config{ Datadir: stack.ResolvePath(""), BloomSize: ctx.Uint64(utils.BloomFilterSizeFlag.Name), EnableStateExpiry: cfg.Eth.StateExpiryEnable, ChainConfig: chainConfig, + CacheConfig: cacheConfig, } pruner, err := pruner.NewPruner(chaindb, prunerconfig, ctx.Uint64(utils.TriesInMemoryFlag.Name)) if err != nil { log.Error("Failed to open snapshot tree", "err", err) return err } + + if cfg.Eth.StateScheme == rawdb.PathScheme { + // when using PathScheme, only prune expired state + return pruner.ExpiredPrune(common.Big0, common.Hash{}) + } + if ctx.NArg() > 1 { log.Error("Too many arguments given") return errors.New("too many arguments") diff --git a/core/blockchain.go b/core/blockchain.go index 0c979f52b9..f040fc5652 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -164,8 +164,8 @@ type CacheConfig struct { RemoteEndPoint string } -// triedbConfig derives the configures for trie database. -func (c *CacheConfig) triedbConfig() *trie.Config { +// TriedbConfig derives the configures for trie database. +func (c *CacheConfig) TriedbConfig() *trie.Config { config := &trie.Config{ Cache: c.TrieCleanLimit, Preimages: c.Preimages, @@ -322,7 +322,7 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis diffLayerChanCache, _ := exlru.New(diffLayerCacheLimit) // Open trie database with provided config - triedb := trie.NewDatabase(db, cacheConfig.triedbConfig()) + triedb := trie.NewDatabase(db, cacheConfig.TriedbConfig()) // Setup the genesis block, commit the provided genesis specification // to database if the genesis block is not present yet, or load the diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go index 8a59a4443f..5102b8f3d8 100644 --- a/core/state/pruner/pruner.go +++ b/core/state/pruner/pruner.go @@ -24,6 +24,7 @@ import ( "fmt" "github.com/ethereum/go-ethereum/params" "math" + "math/big" "os" "path/filepath" "strings" @@ -69,6 +70,7 @@ type Config struct { BloomSize uint64 // The Megabytes of memory allocated to bloom-filter EnableStateExpiry bool ChainConfig *params.ChainConfig + CacheConfig *core.CacheConfig } // Pruner is an offline tool to prune the stale state with the @@ -89,6 +91,7 @@ type Pruner struct { stateBloom *stateBloom snaptree *snapshot.Tree triesInMemory uint64 + flattenBlock *types.Header } type BlockPruner struct { @@ -128,6 +131,9 @@ func NewPruner(db ethdb.Database, config Config, triesInMemory uint64) (*Pruner, if err != nil { return nil, err } + + flattenBlockHash := rawdb.ReadCanonicalHash(db, headBlock.NumberU64()-triesInMemory) + flattenBlock := rawdb.ReadHeader(db, flattenBlockHash, headBlock.NumberU64()-triesInMemory) return &Pruner{ config: config, chainHeader: headBlock.Header(), @@ -135,6 +141,7 @@ func NewPruner(db ethdb.Database, config Config, triesInMemory uint64) (*Pruner, stateBloom: stateBloom, snaptree: snaptree, triesInMemory: triesInMemory, + flattenBlock: flattenBlock, }, nil } @@ -662,53 +669,69 @@ func (p *Pruner) Prune(root common.Hash) error { return err } - // it must run later to prune, using bloom filter to prevent pruning in use trie node, cannot prune concurrently. - if p.config.EnableStateExpiry { - var ( - pruneExpiredTrieCh = make(chan *snapshot.ContractItem, 100000) - pruneExpiredInDiskCh = make(chan *trie.NodeInfo, 100000) - epoch = types.GetStateEpoch(p.config.ChainConfig, p.chainHeader.Number) - rets = make([]error, 3) - expiryWG sync.WaitGroup - ) - trieDB := trie.NewDatabase(p.db, &trie.Config{ - EnableStateExpiry: true, - PathDB: nil, // TODO(0xbundler): support later - }) - expiryWG.Add(2) - go func() { - defer expiryWG.Done() - rets[0] = asyncScanExpiredInTrie(trieDB, root, epoch, pruneExpiredTrieCh, pruneExpiredInDiskCh) - }() - go func() { - defer expiryWG.Done() - rets[1] = asyncPruneExpiredStorageInDisk(p.db, pruneExpiredInDiskCh, p.stateBloom) - }() - rets[2] = snapshot.TraverseContractTrie(p.snaptree, root, pruneExpiredTrieCh) - - // wait task done - expiryWG.Wait() - for i, item := range rets { - if item != nil { - log.Error("prune expired state got error", "index", i, "err", item) - } - } + if err = p.ExpiredPrune(p.chainHeader.Number, root); err != nil { + return err + } - // recap epoch meta snap, save journal - snap := trieDB.EpochMetaSnapTree() - if snap != nil { - log.Info("epoch meta snap handle", "root", root) - if err := snap.Cap(root); err != nil { - log.Error("asyncPruneExpired, SnapTree Cap err", "err", err) - return err - } - if err := snap.Journal(); err != nil { - log.Error("asyncPruneExpired, SnapTree Journal err", "err", err) - return err - } + return nil +} + +// ExpiredPrune it must run later to prune, using bloom filter in HBSS to prevent pruning in use trie node, cannot prune concurrently. +// but in PBSS, it need not bloom filter +func (p *Pruner) ExpiredPrune(height *big.Int, root common.Hash) error { + if !p.config.EnableStateExpiry { + log.Info("stop prune expired state, disable state expiry", "height", height, "root", root, "scheme", p.config.CacheConfig.StateScheme) + return nil + } + + // if root is empty, using the deepest snap block to prune expired state + if root == (common.Hash{}) { + height = p.flattenBlock.Number + root = p.flattenBlock.Root + } + log.Info("start prune expired state", "height", height, "root", root, "scheme", p.config.CacheConfig.StateScheme) + + var ( + pruneExpiredTrieCh = make(chan *snapshot.ContractItem, 100000) + pruneExpiredInDiskCh = make(chan *trie.NodeInfo, 100000) + epoch = types.GetStateEpoch(p.config.ChainConfig, height) + rets = make([]error, 3) + tasksWG sync.WaitGroup + ) + trieDB := trie.NewDatabase(p.db, p.config.CacheConfig.TriedbConfig()) + tasksWG.Add(2) + go func() { + defer tasksWG.Done() + rets[0] = asyncScanExpiredInTrie(trieDB, root, epoch, pruneExpiredTrieCh, pruneExpiredInDiskCh) + }() + go func() { + defer tasksWG.Done() + rets[1] = asyncPruneExpiredStorageInDisk(p.db, pruneExpiredInDiskCh, p.stateBloom, p.config.CacheConfig.StateScheme) + }() + rets[2] = snapshot.TraverseContractTrie(p.snaptree, root, pruneExpiredTrieCh) + + // wait task done + tasksWG.Wait() + for i, item := range rets { + if item != nil { + log.Error("prune expired state got error", "index", i, "err", item) + } + } + + // recap epoch meta snap, save journal + snap := trieDB.EpochMetaSnapTree() + if snap != nil { + log.Info("epoch meta snap handle", "root", root) + if err := snap.Cap(root); err != nil { + log.Error("asyncPruneExpired, SnapTree Cap err", "err", err) + return err + } + if err := snap.Journal(); err != nil { + log.Error("asyncPruneExpired, SnapTree Journal err", "err", err) + return err } - log.Info("Expired State pruning successful") } + log.Info("Expired State pruning successful") return nil } @@ -738,7 +761,7 @@ func asyncScanExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch type return nil } -func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, pruneExpiredInDisk chan *trie.NodeInfo, bloom *stateBloom) error { +func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, pruneExpiredInDisk chan *trie.NodeInfo, bloom *stateBloom, scheme string) error { var ( trieCount = 0 epochMetaCount = 0 @@ -758,9 +781,14 @@ func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, pruneExpiredInDisk ch // delete trie kv trieCount++ trieSize += common.StorageSize(len(info.Key) + 32) - // hbss has shared kv, so using bloom to filter them out. - if !bloom.Contain(info.Hash.Bytes()) { - rawdb.DeleteTrieNode(batch, addr, info.Path, info.Hash, rawdb.HashScheme) + switch scheme { + case rawdb.PathScheme: + rawdb.DeleteTrieNode(batch, addr, info.Path, info.Hash, rawdb.PathScheme) + case rawdb.HashScheme: + // hbss has shared kv, so using bloom to filter them out. + if !bloom.Contain(info.Hash.Bytes()) { + rawdb.DeleteTrieNode(batch, addr, info.Path, info.Hash, rawdb.HashScheme) + } } // delete epoch meta if info.IsBranch { @@ -772,7 +800,7 @@ func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, pruneExpiredInDisk ch if info.IsLeaf { snapCount++ snapSize += common.StorageSize(32) - if err := snapshot.ShrinkExpiredLeaf(batch, addr, info.Key, info.Epoch); err != nil { + if err := snapshot.ShrinkExpiredLeaf(batch, addr, info.Key, info.Epoch, scheme); err != nil { log.Error("ShrinkExpiredLeaf err", "addr", addr, "key", info.Key, "err", err) } } diff --git a/core/state/snapshot/snapshot_expire.go b/core/state/snapshot/snapshot_expire.go index 9fa8cf29f4..a4be6a1f84 100644 --- a/core/state/snapshot/snapshot_expire.go +++ b/core/state/snapshot/snapshot_expire.go @@ -2,18 +2,23 @@ package snapshot import ( "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" ) // ShrinkExpiredLeaf tool function for snapshot kv prune -func ShrinkExpiredLeaf(db ethdb.KeyValueWriter, accountHash common.Hash, storageHash common.Hash, epoch types.StateEpoch) error { - // TODO: cannot prune snapshot in hbss, because it will used for trie prune, but it's ok in pbss. - //valWithEpoch := NewValueWithEpoch(epoch, nil) - //enc, err := EncodeValueToRLPBytes(valWithEpoch) - //if err != nil { - // return err - //} - //rawdb.WriteStorageSnapshot(db, accountHash, storageHash, enc) +func ShrinkExpiredLeaf(db ethdb.KeyValueWriter, accountHash common.Hash, storageHash common.Hash, epoch types.StateEpoch, scheme string) error { + switch scheme { + case rawdb.HashScheme: + //cannot prune snapshot in hbss, because it will used for trie prune, but it's ok in pbss. + case rawdb.PathScheme: + valWithEpoch := NewValueWithEpoch(epoch, nil) + enc, err := EncodeValueToRLPBytes(valWithEpoch) + if err != nil { + return err + } + rawdb.WriteStorageSnapshot(db, accountHash, storageHash, enc) + } return nil } diff --git a/core/state/snapshot/snapshot_expire_test.go b/core/state/snapshot/snapshot_expire_test.go index 6aff97a6f2..50067c53b3 100644 --- a/core/state/snapshot/snapshot_expire_test.go +++ b/core/state/snapshot/snapshot_expire_test.go @@ -21,7 +21,7 @@ func TestShrinkExpiredLeaf(t *testing.T) { db := memorydb.New() rawdb.WriteStorageSnapshot(db, accountHash, storageHash1, encodeSnapVal(NewRawValue([]byte("val1")))) - err := ShrinkExpiredLeaf(db, accountHash, storageHash1, types.StateEpoch0) + err := ShrinkExpiredLeaf(db, accountHash, storageHash1, types.StateEpoch0, rawdb.PathScheme) assert.NoError(t, err) assert.Equal(t, encodeSnapVal(NewValueWithEpoch(types.StateEpoch0, nil)), rawdb.ReadStorageSnapshot(db, accountHash, storageHash1)) From 85db375dbf37cb737a48ebc9c95a6dd2792c361f Mon Sep 17 00:00:00 2001 From: asyukii Date: Tue, 26 Sep 2023 13:21:36 +0800 Subject: [PATCH 35/65] rpc(revive): get from latest block num modify modify --- core/types/revive_state.go | 9 ++++++ ethclient/gethclient/gethclient.go | 23 ++++++------- ethclient/gethclient/gethclient_test.go | 14 ++++---- ethdb/fullstatedb.go | 20 +++++++----- internal/ethapi/api.go | 43 +++++++++++++++++++++---- 5 files changed, 77 insertions(+), 32 deletions(-) diff --git a/core/types/revive_state.go b/core/types/revive_state.go index 27a80a2e1e..85da6f7cb9 100644 --- a/core/types/revive_state.go +++ b/core/types/revive_state.go @@ -1,7 +1,16 @@ package types +import ( + "github.com/ethereum/go-ethereum/common/hexutil" +) + type ReviveStorageProof struct { Key string `json:"key"` PrefixKey string `json:"prefixKey"` Proof []string `json:"proof"` } + +type ReviveResult struct { + StorageProof []ReviveStorageProof `json:"storageProof"` + BlockNum hexutil.Uint64 `json:"blockNum"` +} diff --git a/ethclient/gethclient/gethclient.go b/ethclient/gethclient/gethclient.go index f24b83d5cd..fd29676de0 100644 --- a/ethclient/gethclient/gethclient.go +++ b/ethclient/gethclient/gethclient.go @@ -127,20 +127,21 @@ func (ec *Client) GetProof(ctx context.Context, account common.Address, keys []s // GetStorageReviveProof returns the proof for the given keys. Prefix keys can be specified to obtain partial proof for a given key. // Both keys and prefix keys should have the same length. If user wish to obtain full proof for a given key, the corresponding prefix key should be empty string. -func (ec *Client) GetStorageReviveProof(ctx context.Context, account common.Address, keys []string, prefixKeys []string, hash common.Hash) ([]types.ReviveStorageProof, error) { +func (ec *Client) GetStorageReviveProof(ctx context.Context, account common.Address, keys []string, prefixKeys []string, hash common.Hash) (*types.ReviveResult, error) { + type reviveResult struct { + StorageProof []types.ReviveStorageProof `json:"storageProof"` + BlockNum hexutil.Uint64 `json:"blockNum"` + } + var err error - storageResults := make([]types.ReviveStorageProof, 0, len(keys)) + var res reviveResult - if len(keys) != len(prefixKeys) { - return nil, fmt.Errorf("keys and prefixKeys must be same length") - } + err = ec.c.CallContext(ctx, &res, "eth_getStorageReviveProof", account, keys, prefixKeys, hash) - if hash == (common.Hash{}) { - err = ec.c.CallContext(ctx, &storageResults, "eth_getStorageReviveProof", account, keys, prefixKeys, "latest") - } else { - err = ec.c.CallContext(ctx, &storageResults, "eth_getStorageReviveProof", account, keys, prefixKeys, hash) - } - return storageResults, err + return &types.ReviveResult{ + StorageProof: res.StorageProof, + BlockNum: res.BlockNum, + }, err } // CallContract executes a message call transaction, which is directly executed in the VM diff --git a/ethclient/gethclient/gethclient_test.go b/ethclient/gethclient/gethclient_test.go index b1ea98f19f..3e24fcfa37 100644 --- a/ethclient/gethclient/gethclient_test.go +++ b/ethclient/gethclient/gethclient_test.go @@ -242,21 +242,23 @@ func testGetProof(t *testing.T, client *rpc.Client) { func testGetStorageReviveProof(t *testing.T, client *rpc.Client) { ec := New(client) result, err := ec.GetStorageReviveProof(context.Background(), testAddr, []string{testSlot.String()}, []string{""}, common.Hash{}) + proofs := result.StorageProof + if err != nil { t.Fatal(err) } // test storage - if len(result) != 1 { - t.Fatalf("invalid storage proof, want 1 proof, got %v proof(s)", len(result)) + if len(proofs) != 1 { + t.Fatalf("invalid storage proof, want 1 proof, got %v proof(s)", len(proofs)) } - if result[0].Key != testSlot.String() { - t.Fatalf("invalid storage proof key, want: %q, got: %q", testSlot.String(), result[0].Key) + if proofs[0].Key != testSlot.String() { + t.Fatalf("invalid storage proof key, want: %q, got: %q", testSlot.String(), proofs[0].Key) } - if result[0].PrefixKey != "" { - t.Fatalf("invalid storage proof prefix key, want: %q, got: %q", "", result[0].PrefixKey) + if proofs[0].PrefixKey != "" { + t.Fatalf("invalid storage proof prefix key, want: %q, got: %q", "", proofs[0].PrefixKey) } } diff --git a/ethdb/fullstatedb.go b/ethdb/fullstatedb.go index 724094262a..61f591da30 100644 --- a/ethdb/fullstatedb.go +++ b/ethdb/fullstatedb.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "errors" + "fmt" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" @@ -63,35 +64,38 @@ func (f *FullStateRPCServer) GetStorageReviveProof(stateRoot common.Hash, accoun getStorageProofTimer.Update(time.Since(start)) }(time.Now()) + var result types.ReviveResult + getProofMeter.Mark(int64(len(keys))) // find from lru cache, now it cache key proof - uncahcedPrefixKeys := make([]string, 0, len(prefixKeys)) - uncahcedKeys := make([]string, 0, len(keys)) + uncachedPrefixKeys := make([]string, 0, len(prefixKeys)) + uncachedKeys := make([]string, 0, len(keys)) ret := make([]types.ReviveStorageProof, 0, len(keys)) for i, key := range keys { val, ok := f.cache.Get(proofCacheKey(account, root, prefixKeys[i], key)) log.Debug("GetStorageReviveProof hit cache", "account", account, "key", key, "ok", ok) if !ok { - uncahcedPrefixKeys = append(uncahcedPrefixKeys, prefixKeys[i]) - uncahcedKeys = append(uncahcedKeys, keys[i]) + uncachedPrefixKeys = append(uncachedPrefixKeys, prefixKeys[i]) + uncachedKeys = append(uncachedKeys, keys[i]) continue } getProofHitCacheMeter.Mark(1) ret = append(ret, val.(types.ReviveStorageProof)) } - if len(uncahcedKeys) == 0 { + if len(uncachedKeys) == 0 { return ret, nil } // TODO(0xbundler): add timeout in flags? ctx, cancelFunc := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancelFunc() - proofs := make([]types.ReviveStorageProof, 0, len(uncahcedKeys)) - err := f.client.CallContext(ctx, &proofs, "eth_getStorageReviveProof", stateRoot, account, root, uncahcedKeys, uncahcedPrefixKeys) + err := f.client.CallContext(ctx, &result, "eth_getStorageReviveProof", stateRoot, account, root, uncachedKeys, uncachedPrefixKeys) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get storage revive proof, err: %v, remote's block number: %v", err, result.BlockNum) } + proofs := result.StorageProof + // add to cache for _, proof := range proofs { f.cache.Add(proofCacheKey(account, root, proof.PrefixKey, proof.Key), proof) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 33f5545939..82ef9b82b8 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -50,6 +50,7 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/trie" "github.com/tyler-smith/go-bip39" ) @@ -765,7 +766,7 @@ func (s *BlockChainAPI) GetProof(ctx context.Context, address common.Address, st // GetStorageReviveProof returns the proof for the given keys. Prefix keys can be specified to obtain partial proof for a given key. // Both keys and prefix keys should have the same length. If user wish to obtain full proof for a given key, the corresponding prefix key should be empty string. -func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, stateRoot common.Hash, address common.Address, root common.Hash, storageKeys []string, storagePrefixKeys []string) ([]types.ReviveStorageProof, error) { +func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, stateRoot common.Hash, address common.Address, root common.Hash, storageKeys []string, storagePrefixKeys []string) (*types.ReviveResult, error) { defer func(start time.Time) { getStorageProofTimer.Update(time.Since(start)) }(time.Now()) @@ -775,11 +776,41 @@ func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, stateRoot com } var ( + blockNum hexutil.Uint64 + err error + stateDb *state.StateDB + header *types.Header + storageTrie state.Trie keys = make([]common.Hash, len(storageKeys)) keyLengths = make([]int, len(storageKeys)) prefixKeys = make([][]byte, len(storagePrefixKeys)) storageProof = make([]types.ReviveStorageProof, len(storageKeys)) ) + + openStorageTrie := func(stateDb *state.StateDB, header *types.Header, address common.Address) (state.Trie, error) { + id := trie.StorageTrieID(header.Root, crypto.Keccak256Hash(address.Bytes()), root) + tr, err := trie.NewStateTrie(id, stateDb.Database().TrieDB()) + if err != nil { + return nil, err + } + return tr, nil + } + + stateDb, header, _ = s.b.StateAndHeaderByNumber(ctx, rpc.LatestBlockNumber) + storageTrie, err = s.b.StorageTrie(stateRoot, address, root) + + if (err != nil || storageTrie == nil) && stateDb != nil { + storageTrie, err = openStorageTrie(stateDb, header, address) + blockNum = hexutil.Uint64(header.Number.Uint64()) + } + + if err != nil || storageTrie == nil { + return &types.ReviveResult{ + StorageProof: nil, + BlockNum: blockNum, + }, err + } + // Deserialize all keys. This prevents state access on invalid input. for i, hexKey := range storageKeys { var err error @@ -798,11 +829,6 @@ func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, stateRoot com } } - storageTrie, err := s.b.StorageTrie(stateRoot, address, root) - if err != nil || storageTrie == nil { - return nil, fmt.Errorf("open StorageTrie err: %v", err) - } - // Create the proofs for the storageKeys. for i, key := range keys { // Output key encoding is a bit special: if the input was a 32-byte hash, it is @@ -828,7 +854,10 @@ func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, stateRoot com } } - return storageProof, nil + return &types.ReviveResult{ + StorageProof: storageProof, + BlockNum: blockNum, + }, nil } // decodeHash parses a hex-encoded 32-byte hash. The input may optionally From 43dde20694c84e8279536785dca6c2bd5a14f50c Mon Sep 17 00:00:00 2001 From: asyukii Date: Tue, 26 Sep 2023 17:49:42 +0800 Subject: [PATCH 36/65] core/state, trie: fix bad block add TODO api fix --- core/state/state_object.go | 7 ++++++- internal/ethapi/api.go | 5 +++-- trie/proof.go | 1 - trie/trie.go | 4 ++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/core/state/state_object.go b/core/state/state_object.go index 2eb184677b..1d694d0c72 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -800,7 +800,12 @@ func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash, // if no prefix, query from revive trie, got the newest expired info if resolvePath { - _, err := tr.GetStorage(s.address, key.Bytes()) + val, err := tr.GetStorage(s.address, key.Bytes()) + // TODO(asyukii): temporary fix snap expired, but trie not expire, may investigate more later. + if val != nil { + s.pendingReviveState[string(crypto.Keccak256(key[:]))] = common.BytesToHash(val) + return val, nil + } enErr, ok := err.(*trie.ExpiredNodeError) if !ok { return nil, fmt.Errorf("cannot find expired state from trie, err: %v", err) diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 82ef9b82b8..4c981c1b13 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -797,11 +797,12 @@ func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, stateRoot com } stateDb, header, _ = s.b.StateAndHeaderByNumber(ctx, rpc.LatestBlockNumber) - storageTrie, err = s.b.StorageTrie(stateRoot, address, root) + blockNum = hexutil.Uint64(header.Number.Uint64()) + storageTrie, err = s.b.StorageTrie(stateRoot, address, root) if (err != nil || storageTrie == nil) && stateDb != nil { storageTrie, err = openStorageTrie(stateDb, header, address) - blockNum = hexutil.Uint64(header.Number.Uint64()) + log.Info("GetStorageReviveProof from latest block number", "blockNum", blockNum, "blockHash", header.Hash().Hex()) } if err != nil || storageTrie == nil { diff --git a/trie/proof.go b/trie/proof.go index a88ae6eacb..c67b3d6c3b 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -120,7 +120,6 @@ func (t *StateTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error { // it. func (t *Trie) traverseNodes(tn node, prefixKey, suffixKey []byte, nodes *[]node, epoch types.StateEpoch, updateEpoch bool) (node, error) { for len(suffixKey) > 0 && tn != nil { - log.Info("traverseNodes loop", "prefix", common.Bytes2Hex(prefixKey), "suffix", common.Bytes2Hex(suffixKey), "n", tn.fstring("")) switch n := tn.(type) { case *shortNode: if len(suffixKey) >= len(n.Key) && bytes.Equal(n.Key, suffixKey[:len(n.Key)]) { diff --git a/trie/trie.go b/trie/trie.go index 27ff3134f4..0fbb8f69ac 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -1238,8 +1238,8 @@ func (t *Trie) tryRevive(n node, key []byte, targetPrefixKey []byte, nub MPTProo n1 = n1.copy() n1.Val = newnode n1.flags = t.newFlag() - tryUpdateNodeEpoch(nub.n1, t.currentEpoch) - renew, _, err := t.updateChildNodeEpoch(nub.n1, key, pos, t.currentEpoch) + tryUpdateNodeEpoch(n1, t.currentEpoch) + renew, _, err := t.updateChildNodeEpoch(n1, key, pos, t.currentEpoch) if err != nil { return nil, false, fmt.Errorf("update child node epoch while reviving failed, err: %v", err) } From c05a72921c6c99840f81ede2d63af47d24eb8f7a Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Mon, 25 Sep 2023 16:36:40 +0800 Subject: [PATCH 37/65] feat: insert new slot in snapshot, prevent execution touch trie; feat: reuse prefetcher tire? it could fetch from remoteDB; feat: revive from local first, then fetch from remote; --- core/state/database.go | 11 +-- core/state/state_expiry.go | 24 ++++++- core/state/state_object.go | 122 +++++++++++++++++++++++----------- internal/ethapi/api.go | 2 +- light/trie.go | 22 +++--- trie/dummy_trie.go | 10 ++- trie/errors.go | 14 ---- trie/proof.go | 133 +------------------------------------ trie/proof_test.go | 25 ------- trie/secure_trie.go | 9 ++- trie/trie.go | 8 +-- trie/trie_expiry.go | 72 ++++++++++++++++++++ trie/trie_test.go | 8 +-- 13 files changed, 224 insertions(+), 236 deletions(-) create mode 100644 trie/trie_expiry.go diff --git a/core/state/database.go b/core/state/database.go index f64a363c0b..58366be0ad 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -158,17 +158,20 @@ type Trie interface { // with the node that proves the absence of the key. Prove(key []byte, proofDb ethdb.KeyValueWriter) error - // ProvePath generate proof state in trie. - ProvePath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error + // ProveByPath generate proof state in trie. + ProveByPath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error - // ReviveTrie revive expired state from proof. - ReviveTrie(key []byte, proof []*trie.MPTProofNub) ([]*trie.MPTProofNub, error) + // TryRevive revive expired state from proof. + TryRevive(key []byte, proof []*trie.MPTProofNub) ([]*trie.MPTProofNub, error) // SetEpoch set current epoch in trie, it must set in initial period, or it will get error behavior. SetEpoch(types.StateEpoch) // Epoch get current epoch in trie Epoch() types.StateEpoch + + // TryLocalRevive it revive using local non-pruned states + TryLocalRevive(addr common.Address, key []byte) ([]byte, error) } // NewDatabase creates a backing store for state. The returned database is safe for diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index 6870205fa8..232c4d0582 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -14,12 +14,34 @@ import ( var ( reviveStorageTrieTimer = metrics.NewRegisteredTimer("state/revivetrie/rt", nil) + EnableLocalRevive = false // indicate if using local revive ) // fetchExpiredStorageFromRemote request expired state from remote full state node; func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, stateRoot common.Hash, addr common.Address, root common.Hash, tr Trie, prefixKey []byte, key common.Hash) (map[string][]byte, error) { log.Debug("fetching expired storage from remoteDB", "addr", addr, "prefix", prefixKey, "key", key) + if EnableLocalRevive { + // if there need revive expired state, try to revive locally, when the node is not being pruned, just renew the epoch + val, err := tr.TryLocalRevive(addr, key.Bytes()) + log.Debug("fetchExpiredStorageFromRemote TryLocalRevive", "addr", addr, "key", key, "val", val, "err", err) + if _, ok := err.(*trie.MissingNodeError); !ok { + return nil, err + } + switch err.(type) { + case *trie.MissingNodeError: + // cannot revive locally, request from remote + case nil: + ret := make(map[string][]byte, 1) + ret[key.String()] = val + return ret, nil + default: + return nil, err + } + } + + // cannot revive locally, fetch remote proof proofs, err := fullDB.GetStorageReviveProof(stateRoot, addr, root, []string{common.Bytes2Hex(prefixKey)}, []string{common.Bytes2Hex(key[:])}) + log.Debug("fetchExpiredStorageFromRemote GetStorageReviveProof", "addr", addr, "key", key, "proofs", len(proofs), "err", err) if err != nil { return nil, err } @@ -60,7 +82,7 @@ func reviveStorageTrie(addr common.Address, tr Trie, proof types.ReviveStoragePr return nil, err } - nubs, err := tr.ReviveTrie(key, proofCache.CacheNubs()) + nubs, err := tr.TryRevive(key, proofCache.CacheNubs()) if err != nil { return nil, err } diff --git a/core/state/state_object.go b/core/state/state_object.go index 1d694d0c72..ac3a164e35 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -18,6 +18,7 @@ package state import ( "bytes" + "encoding/hex" "fmt" "github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/log" @@ -81,10 +82,11 @@ type stateObject struct { dirtyStorage Storage // Storage entries that have been modified in the current transaction execution, reset for every transaction // for state expiry feature - pendingReviveTrie Trie // pendingReviveTrie it contains pending revive trie nodes, could update & commit later - pendingReviveState map[string]common.Hash // pendingReviveState for block, when R&W, access revive state first, saved in hash key - pendingAccessedState map[common.Hash]int // pendingAccessedState record which state is accessed(only read now, update/delete/insert will auto update epoch), it will update epoch index late - originStorageEpoch map[common.Hash]types.StateEpoch // originStorageEpoch record origin state epoch, prevent frequency epoch update + pendingReviveTrie Trie // pendingReviveTrie it contains pending revive trie nodes, could update & commit later + pendingReviveState map[string]common.Hash // pendingReviveState for block, when R&W, access revive state first, saved in hash key + pendingAccessedState map[common.Hash]int // pendingAccessedState record which state is accessed(only read now, update/delete/insert will auto update epoch), it will update epoch index late + originStorageEpoch map[common.Hash]types.StateEpoch // originStorageEpoch record origin state epoch, prevent frequency epoch update + pendingFutureReviveState map[common.Hash]int // pendingFutureReviveState record empty state in snapshot. it should preftech first, and allow check in updateTrie // Cache flags. dirtyCode bool // true if the code was updated @@ -120,18 +122,19 @@ func newObject(db *StateDB, address common.Address, acct *types.StateAccount) *s } return &stateObject{ - db: db, - address: address, - addrHash: crypto.Keccak256Hash(address[:]), - origin: origin, - data: *acct, - sharedOriginStorage: storageMap, - originStorage: make(Storage), - pendingStorage: make(Storage), - dirtyStorage: make(Storage), - pendingReviveState: make(map[string]common.Hash), - pendingAccessedState: make(map[common.Hash]int), - originStorageEpoch: make(map[common.Hash]types.StateEpoch), + db: db, + address: address, + addrHash: crypto.Keccak256Hash(address[:]), + origin: origin, + data: *acct, + sharedOriginStorage: storageMap, + originStorage: make(Storage), + pendingStorage: make(Storage), + dirtyStorage: make(Storage), + pendingReviveState: make(map[string]common.Hash), + pendingAccessedState: make(map[common.Hash]int), + pendingFutureReviveState: make(map[common.Hash]int), + originStorageEpoch: make(map[common.Hash]types.StateEpoch), } } @@ -264,7 +267,6 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { // If no live objects are available, attempt to use snapshots var ( enc []byte - sv snapshot.SnapValue err error value common.Hash ) @@ -274,15 +276,13 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { // handle state expiry situation if s.db.EnableExpire() { var dbError error - sv, err, dbError = s.getExpirySnapStorage(key) + enc, err, dbError = s.getExpirySnapStorage(key) if dbError != nil { s.db.setError(fmt.Errorf("state expiry getExpirySnapStorage, contract: %v, key: %v, err: %v", s.address, key, dbError)) return common.Hash{} } - // if query success, just set val, otherwise request from trie - if err == nil && sv != nil { - value.SetBytes(sv.GetVal()) - s.originStorageEpoch[key] = sv.GetEpoch() + if len(enc) > 0 { + value.SetBytes(enc) } } else { enc, err = s.db.snap.Storage(s.addrHash, crypto.Keccak256Hash(key.Bytes())) @@ -300,7 +300,7 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { } // If the snapshot is unavailable or reading from it fails, load from the database. - if s.needLoadFromTrie(err, sv) { + if s.db.snap == nil || err != nil { getCommittedStorageTrieMeter.Mark(1) start := time.Now() var tr Trie @@ -383,6 +383,17 @@ func (s *stateObject) finalise(prefetch bool) { slotsToPrefetch = append(slotsToPrefetch, common.CopyBytes(key[:])) // Copy needed for closure } } + + // try prefetch future revive states + for key := range s.pendingFutureReviveState { + if val, ok := s.dirtyStorage[key]; ok { + if val != s.originStorage[key] { + continue + } + } + slotsToPrefetch = append(slotsToPrefetch, common.CopyBytes(key[:])) // Copy needed for closure + } + if s.db.prefetcher != nil && prefetch && len(slotsToPrefetch) > 0 && s.data.Root != types.EmptyRootHash { s.db.prefetcher.prefetch(s.addrHash, s.data.Root, s.address, slotsToPrefetch) } @@ -417,6 +428,8 @@ func (s *stateObject) updateTrie() (Trie, error) { err error ) if s.db.EnableExpire() { + // if EnableExpire, just use PendingReviveTrie, but prefetcher.trie is useful too, it warms up the db cache. + // and when no state expired or pruned, it will directly use prefetcher.trie too. tr, err = s.getPendingReviveTrie() } else { tr, err = s.getTrie() @@ -458,6 +471,23 @@ func (s *stateObject) updateTrie() (Trie, error) { wg.Add(1) go func() { defer wg.Done() + if s.db.EnableExpire() { + // revive state first, to figure out if there have conflict expiry path or local revive + for key := range s.pendingFutureReviveState { + _, err = tr.GetStorage(s.address, key.Bytes()) + if err == nil { + continue + } + enErr, ok := err.(*trie.ExpiredNodeError) + if !ok { + s.db.setError(fmt.Errorf("state object pendingFutureReviveState err, contract: %v, key: %v, err: %v", s.address, key, err)) + continue + } + if _, err = fetchExpiredStorageFromRemote(s.db.fullStateDB, s.db.originalRoot, s.address, s.data.Root, tr, enErr.Path, key); err != nil { + s.db.setError(fmt.Errorf("state object pendingFutureReviveState fetchExpiredStorageFromRemote err, contract: %v, key: %v, err: %v", s.address, key, err)) + } + } + } for key, value := range dirtyStorage { if len(value) == 0 { if err := tr.DeleteStorage(s.address, key[:]); err != nil { @@ -535,6 +565,9 @@ func (s *stateObject) updateTrie() (Trie, error) { if len(s.pendingAccessedState) > 0 { s.pendingAccessedState = make(map[common.Hash]int) } + if len(s.pendingFutureReviveState) > 0 { + s.pendingFutureReviveState = make(map[common.Hash]int) + } if len(s.originStorageEpoch) > 0 { s.originStorageEpoch = make(map[common.Hash]types.StateEpoch) } @@ -682,6 +715,10 @@ func (s *stateObject) deepCopy(db *StateDB) *stateObject { for k, v := range s.pendingAccessedState { obj.pendingAccessedState[k] = v } + obj.pendingFutureReviveState = make(map[common.Hash]int, len(s.pendingFutureReviveState)) + for k, v := range s.pendingFutureReviveState { + obj.pendingFutureReviveState[k] = v + } obj.originStorageEpoch = make(map[common.Hash]types.StateEpoch, len(s.originStorageEpoch)) for k, v := range s.originStorageEpoch { obj.originStorageEpoch[k] = v @@ -784,6 +821,16 @@ func (s *stateObject) accessState(key common.Hash) { } } +// futureReviveState record future revive state, it will load on prefetcher or updateTrie +func (s *stateObject) futureReviveState(key common.Hash) { + if !s.db.EnableExpire() { + return + } + + count := s.pendingFutureReviveState[key] + s.pendingFutureReviveState[key] = count + 1 +} + // TODO(0xbundler): add hash key cache later func (s *stateObject) queryFromReviveState(reviveState map[string]common.Hash, key common.Hash) (common.Hash, bool) { khash := crypto.HashData(s.db.hasher, key[:]) @@ -814,14 +861,10 @@ func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash, } kvs, err := fetchExpiredStorageFromRemote(s.db.fullStateDB, s.db.originalRoot, s.address, s.data.Root, tr, prefixKey, key) - if err != nil { - // Keys may not exist in the trie, so they can't be revived. - if _, ok := err.(*trie.KeyDoesNotExistError); ok { - return nil, nil - } - return nil, fmt.Errorf("revive storage trie failed, err: %v", err) + return nil, err } + for k, v := range kvs { s.pendingReviveState[k] = common.BytesToHash(v) } @@ -831,7 +874,7 @@ func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash, return val.Bytes(), nil } -func (s *stateObject) getExpirySnapStorage(key common.Hash) (snapshot.SnapValue, error, error) { +func (s *stateObject) getExpirySnapStorage(key common.Hash) ([]byte, error, error) { enc, err := s.db.snap.Storage(s.addrHash, crypto.Keccak256Hash(key.Bytes())) if err != nil { return nil, err, nil @@ -845,23 +888,28 @@ func (s *stateObject) getExpirySnapStorage(key common.Hash) (snapshot.SnapValue, } if val == nil { + // record access empty kv, try touch in updateTrie for duplication + s.futureReviveState(key) return nil, nil, nil } + s.originStorageEpoch[key] = val.GetEpoch() if !types.EpochExpired(val.GetEpoch(), s.db.epoch) { - return val, nil, nil + return val.GetVal(), nil, nil } - // TODO(0xbundler): if found value not been pruned, just return - //if len(val.GetVal()) > 0 { - // return val, nil, nil - //} + // if found value not been pruned, just return, local revive later + if EnableLocalRevive && len(val.GetVal()) > 0 { + s.futureReviveState(key) + log.Debug("getExpirySnapStorage GetVal", "addr", s.address, "key", key, "val", hex.EncodeToString(val.GetVal())) + return val.GetVal(), nil, nil + } - // handle from remoteDB, if got err just setError, just return to revert in consensus version. + // handle from remoteDB, if got err just setError, or return to revert in consensus version. valRaw, err := s.fetchExpiredFromRemote(nil, key, true) if err != nil { return nil, nil, err } - return snapshot.NewValueWithEpoch(val.GetEpoch(), valRaw), nil, nil + return valRaw, nil, nil } diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 4c981c1b13..44405c3171 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -845,7 +845,7 @@ func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, stateRoot com var proof proofList prefixKey := prefixKeys[i] - if err := storageTrie.ProvePath(crypto.Keccak256(key.Bytes()), prefixKey, &proof); err != nil { + if err := storageTrie.ProveByPath(crypto.Keccak256(key.Bytes()), prefixKey, &proof); err != nil { return nil, err } storageProof[i] = types.ReviveStorageProof{ diff --git a/light/trie.go b/light/trie.go index 3daa7025bd..81a45b4bfd 100644 --- a/light/trie.go +++ b/light/trie.go @@ -115,10 +115,6 @@ type odrTrie struct { trie *trie.Trie } -func (t *odrTrie) ReviveTrie(key []byte, proof []*trie.MPTProofNub) ([]*trie.MPTProofNub, error) { - panic("not implemented") -} - func (t *odrTrie) GetStorage(_ common.Address, key []byte) ([]byte, error) { key = crypto.Keccak256(key) var enc []byte @@ -224,10 +220,6 @@ func (t *odrTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error { return errors.New("not implemented, needs client/server interface split") } -func (t *odrTrie) ProvePath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error { - return errors.New("not implemented, needs client/server interface split") -} - func (t *odrTrie) Epoch() types.StateEpoch { return types.StateEpoch0 } @@ -260,10 +252,22 @@ func (t *odrTrie) do(key []byte, fn func() error) error { } } -func (db *odrTrie) NoTries() bool { +func (t *odrTrie) NoTries() bool { return false } +func (t *odrTrie) ProveByPath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error { + return errors.New("not implemented, needs client/server interface split") +} + +func (t *odrTrie) TryRevive(key []byte, proof []*trie.MPTProofNub) ([]*trie.MPTProofNub, error) { + return nil, errors.New("not implemented, needs client/server interface split") +} + +func (t *odrTrie) TryLocalRevive(addr common.Address, key []byte) ([]byte, error) { + return nil, errors.New("not implemented, needs client/server interface split") +} + type nodeIterator struct { trie.NodeIterator t *odrTrie diff --git a/trie/dummy_trie.go b/trie/dummy_trie.go index 4ee8cce1e6..28aef864cd 100644 --- a/trie/dummy_trie.go +++ b/trie/dummy_trie.go @@ -89,14 +89,18 @@ func (t *EmptyTrie) GetStorageAndUpdateEpoch(addr common.Address, key []byte) ([ return nil, nil } -func (t *EmptyTrie) ProvePath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error { +func (t *EmptyTrie) SetEpoch(epoch types.StateEpoch) { +} + +func (t *EmptyTrie) ProveByPath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error { return nil } -func (t *EmptyTrie) SetEpoch(epoch types.StateEpoch) { +func (t *EmptyTrie) TryRevive(key []byte, proof []*MPTProofNub) ([]*MPTProofNub, error) { + return nil, nil } -func (t *EmptyTrie) ReviveTrie(key []byte, proof []*MPTProofNub) ([]*MPTProofNub, error) { +func (t *EmptyTrie) TryLocalRevive(addr common.Address, key []byte) ([]byte, error) { return nil, nil } diff --git a/trie/errors.go b/trie/errors.go index e6d61f0228..0c1b785bf0 100644 --- a/trie/errors.go +++ b/trie/errors.go @@ -83,17 +83,3 @@ func NewExpiredNodeError(path []byte, epoch types.StateEpoch) error { func (err *ExpiredNodeError) Error() string { return fmt.Sprintf("expired trie node, path: %v, epoch: %v", err.Path, err.Epoch) } - -type KeyDoesNotExistError struct { - Key []byte -} - -func NewKeyDoesNotExistError(key []byte) error { - return &KeyDoesNotExistError{ - Key: key, - } -} - -func (err *KeyDoesNotExistError) Error() string { - return "key does not exist" -} diff --git a/trie/proof.go b/trie/proof.go index c67b3d6c3b..7e62c4a8b1 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -181,7 +181,7 @@ func (t *Trie) traverseNodes(tn node, prefixKey, suffixKey []byte, nodes *[]node return tn, nil } -func (t *Trie) ProvePath(key []byte, prefixKeyHex []byte, proofDb ethdb.KeyValueWriter) error { +func (t *Trie) ProveByPath(key []byte, prefixKeyHex []byte, proofDb ethdb.KeyValueWriter) error { if t.committed { return ErrCommitted @@ -233,135 +233,8 @@ func (t *Trie) ProvePath(key []byte, prefixKeyHex []byte, proofDb ethdb.KeyValue return nil } -// VerifyPathProof reconstructs the trie from the given proof and verifies the root hash. -func VerifyPathProof(keyHex []byte, prefixKeyHex []byte, proofList [][]byte, epoch types.StateEpoch) (node, hashNode, error) { - - if len(proofList) == 0 { - return nil, nil, fmt.Errorf("proof list is empty") - } - - n, err := ConstructTrieFromProof(keyHex, prefixKeyHex, proofList, epoch) - if err != nil { - return nil, nil, err - } - - // hash the root node - hasher := newHasher(false) - defer returnHasherToPool(hasher) - hn, cn := hasher.hash(n, true) - if hash, ok := hn.(hashNode); ok { - return cn, hash, nil - } - - return nil, nil, fmt.Errorf("path proof verification failed") -} - -// ConstructTrieFromProof constructs a trie from the given proof. It returns the root node of the trie. -func ConstructTrieFromProof(keyHex []byte, prefixKeyHex []byte, proofList [][]byte, epoch types.StateEpoch) (node, error) { - if len(proofList) == 0 { - return nil, nil - } - h := newHasher(false) - defer returnHasherToPool(h) - keyHex = keyHex[len(prefixKeyHex):] - - root, err := decodeNode(nil, proofList[0]) - if err != nil { - return nil, fmt.Errorf("decode proof root %#x, err: %v", proofList[0], err) - } - // update epoch - switch n := root.(type) { - case *shortNode: - n.setEpoch(epoch) - case *fullNode: - n.setEpoch(epoch) - } - - parentNode := root - for i := 1; i < len(proofList); i++ { - n, err := decodeNode(nil, proofList[i]) - if err != nil { - return nil, fmt.Errorf("decode proof item %#x, err: %v", proofList[i], err) - } - - // verify proof continuous - keyrest, child := get(parentNode, keyHex, false) - switch cld := child.(type) { - case nil: - return nil, NewKeyDoesNotExistError(keyHex) - case hashNode: - hashed, _ := h.hash(n, false) - if !bytes.Equal(cld, hashed.(hashNode)) { - return nil, fmt.Errorf("the child node of shortNode is not a hashNode or doesn't match the hash in the proof") - } - default: - // proof's child cannot contain valueNode/shortNode/fullNode - return nil, fmt.Errorf("worng proof, got unexpect node, fstr: %v", child.fstring("")) - } - - // update epoch - switch n := n.(type) { - case *shortNode: - n.setEpoch(epoch) - case *fullNode: - n.setEpoch(epoch) - } - - // Link the parent and child. - switch sn := parentNode.(type) { - case *shortNode: - sn.Val = n - case *fullNode: - sn.Children[keyHex[0]] = n - sn.UpdateChildEpoch(int(keyHex[0]), epoch) - } - - // reset - parentNode = n - keyHex = keyrest - } - - return root, nil -} - -// updateEpochInChildNodes traverse down a node and update the epoch of the child nodes -func updateEpochInChildNodes(tn *node, key []byte, epoch types.StateEpoch) error { - - node := *tn - startNode := node - - for len(key) > 0 && node != nil { - switch n := node.(type) { - case *shortNode: - if len(key) < len(n.Key) || !bytes.Equal(n.Key, key[:len(n.Key)]) { - // The trie doesn't contain the key. - node = nil - } else { - node = n.Val - key = key[len(n.Key):] - } - n.setEpoch(epoch) - case *fullNode: - node = n.Children[key[0]] - n.UpdateChildEpoch(int(key[0]), epoch) - n.setEpoch(epoch) - - key = key[1:] - case nil, hashNode, valueNode: - *tn = startNode - return nil - default: - panic(fmt.Sprintf("%T: invalid node: %v", tn, tn)) - } - } - - *tn = startNode - - return nil -} - -func (t *StateTrie) ProvePath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error { - return t.trie.ProvePath(key, path, proofDb) +func (t *StateTrie) ProveByPath(key []byte, path []byte, proofDb ethdb.KeyValueWriter) error { + return t.trie.ProveByPath(key, path, proofDb) } // VerifyProof checks merkle proofs. The given proof must contain the value for diff --git a/trie/proof_test.go b/trie/proof_test.go index 16beee31b7..62f337f87b 100644 --- a/trie/proof_test.go +++ b/trie/proof_test.go @@ -73,31 +73,6 @@ func makeProvers(trie *Trie) []func(key []byte) *memorydb.Database { return provers } -func TestOneElementPathProof(t *testing.T) { - trie := NewEmpty(NewDatabase(rawdb.NewMemoryDatabase(), nil)) - updateString(trie, "k", "v") - - var proofList proofList - - trie.Prove([]byte("k"), &proofList) - if proofList == nil { - t.Fatalf("nil proof") - } - - if len(proofList) != 1 { - t.Errorf("proof should have one element") - } - - _, hn, err := VerifyPathProof(keybytesToHex([]byte("k")), nil, proofList, 0) - if err != nil { - t.Fatalf("failed to verify proof: %v\nraw proof: %x", err, proofList) - } - - if common.BytesToHash(hn) != trie.Hash() { - t.Fatalf("verified root mismatch: have %x, want %x", hn, trie.Hash()) - } -} - func TestProof(t *testing.T) { trie, vals := randomTrie(500) root := trie.Hash() diff --git a/trie/secure_trie.go b/trie/secure_trie.go index b695609997..e665c8c58d 100644 --- a/trie/secure_trie.go +++ b/trie/secure_trie.go @@ -320,7 +320,12 @@ func (t *StateTrie) getSecKeyCache() map[string][]byte { return t.secKeyCache } -func (t *StateTrie) ReviveTrie(key []byte, proof []*MPTProofNub) ([]*MPTProofNub, error) { +func (t *StateTrie) TryRevive(key []byte, proof []*MPTProofNub) ([]*MPTProofNub, error) { key = t.hashKey(key) - return t.trie.ReviveTrie(key, proof) + return t.trie.TryRevive(key, proof) +} + +func (t *StateTrie) TryLocalRevive(_ common.Address, key []byte) ([]byte, error) { + key = t.hashKey(key) + return t.trie.TryLocalRevive(key) } diff --git a/trie/trie.go b/trie/trie.go index 0fbb8f69ac..6a74636b45 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -1171,16 +1171,12 @@ func (t *Trie) Owner() common.Hash { return t.owner } -// ReviveTrie attempts to revive a trie from a list of MPTProofNubs. +// TryRevive attempts to revive a trie from a list of MPTProofNubs. // ReviveTrie performs full or partial revive and returns a list of successful // nubs. ReviveTrie does not guarantee that a value will be revived completely, // if the proof is not fully valid. -func (t *Trie) ReviveTrie(key []byte, proof []*MPTProofNub) ([]*MPTProofNub, error) { - key = keybytesToHex(key) - return t.TryRevive(key, proof) -} - func (t *Trie) TryRevive(key []byte, proof []*MPTProofNub) ([]*MPTProofNub, error) { + key = keybytesToHex(key) successNubs := make([]*MPTProofNub, 0, len(proof)) reviveMeter.Mark(int64(len(proof))) // Revive trie with each proof nub diff --git a/trie/trie_expiry.go b/trie/trie_expiry.go new file mode 100644 index 0000000000..1dd6f098a1 --- /dev/null +++ b/trie/trie_expiry.go @@ -0,0 +1,72 @@ +package trie + +import ( + "bytes" + "fmt" + "github.com/ethereum/go-ethereum/core/types" +) + +func (t *Trie) TryLocalRevive(key []byte) ([]byte, error) { + // Short circuit if the trie is already committed and not usable. + if t.committed { + return nil, ErrCommitted + } + + key = keybytesToHex(key) + val, newroot, didResolve, err := t.tryLocalRevive(t.root, key, 0, t.getRootEpoch()) + if err == nil && didResolve { + t.root = newroot + t.rootEpoch = t.currentEpoch + } + return val, err +} + +func (t *Trie) tryLocalRevive(origNode node, key []byte, pos int, epoch types.StateEpoch) ([]byte, node, bool, error) { + expired := t.epochExpired(origNode, epoch) + switch n := (origNode).(type) { + case nil: + return nil, nil, false, nil + case valueNode: + return n, n, expired, nil + case *shortNode: + if len(key)-pos < len(n.Key) || !bytes.Equal(n.Key, key[pos:pos+len(n.Key)]) { + // key not found in trie + return nil, n, false, nil + } + value, newnode, didResolve, err := t.tryLocalRevive(n.Val, key, pos+len(n.Key), epoch) + if err == nil && t.renewNode(epoch, didResolve, expired) { + n = n.copy() + n.Val = newnode + n.setEpoch(t.currentEpoch) + didResolve = true + } + return value, n, didResolve, err + case *fullNode: + value, newnode, didResolve, err := t.tryLocalRevive(n.Children[key[pos]], key, pos+1, n.GetChildEpoch(int(key[pos]))) + if err == nil && t.renewNode(epoch, didResolve, expired) { + n = n.copy() + n.Children[key[pos]] = newnode + n.setEpoch(t.currentEpoch) + if newnode != nil { + n.UpdateChildEpoch(int(key[pos]), t.currentEpoch) + } + didResolve = true + } + return value, n, didResolve, err + case hashNode: + child, err := t.resolveAndTrack(n, key[:pos]) + if err != nil { + return nil, n, true, err + } + + if child, ok := child.(*fullNode); ok { + if err = t.resolveEpochMeta(child, epoch, key[:pos]); err != nil { + return nil, n, true, err + } + } + value, newnode, _, err := t.tryLocalRevive(child, key, pos, epoch) + return value, newnode, true, err + default: + panic(fmt.Sprintf("%T: invalid node: %v", origNode, origNode)) + } +} diff --git a/trie/trie_test.go b/trie/trie_test.go index c35f1609ee..b59b0fb3e5 100644 --- a/trie/trie_test.go +++ b/trie/trie_test.go @@ -1008,7 +1008,7 @@ func TestRevive(t *testing.T) { for _, prefixKey := range prefixKeys { // Generate proof var proof proofList - err := trie.ProvePath(key, prefixKey, &proof) + err := trie.ProveByPath(key, prefixKey, &proof) assert.NoError(t, err) // Expire trie @@ -1019,7 +1019,7 @@ func TestRevive(t *testing.T) { assert.NoError(t, err) // Revive trie - _, err = trie.TryRevive(keybytesToHex(key), proofCache.CacheNubs()) + _, err = trie.TryRevive(key, proofCache.CacheNubs()) assert.NoError(t, err, "TryRevive failed, key %x, prefixKey %x, val %x", key, prefixKey, val) // Verifiy value exists after revive @@ -1053,7 +1053,7 @@ func TestReviveCustom(t *testing.T) { prefixKeys := getFullNodePrefixKeys(trie, key) for _, prefixKey := range prefixKeys { var proofList proofList - err := trie.ProvePath(key, prefixKey, &proofList) + err := trie.ProveByPath(key, prefixKey, &proofList) assert.NoError(t, err) trie.ExpireByPrefix(prefixKey) @@ -1063,7 +1063,7 @@ func TestReviveCustom(t *testing.T) { assert.NoError(t, err) // Revive trie - _, err = trie.TryRevive(keybytesToHex(key), proofCache.cacheNubs) + _, err = trie.TryRevive(key, proofCache.cacheNubs) assert.NoError(t, err, "TryRevive failed, key %x, prefixKey %x, val %x", key, prefixKey, val) res := trie.MustGet(key) From e4235beb6a5ea5312f5fff1a4ee97f50ea4f0bbe Mon Sep 17 00:00:00 2001 From: asyukii Date: Wed, 27 Sep 2023 16:56:03 +0800 Subject: [PATCH 38/65] core/state: subfetcher revive state in batch mode --- core/state/state_expiry.go | 71 +++++++++++++++++++++++++++++++++++ core/state/state_object.go | 2 +- core/state/trie_prefetcher.go | 16 +++++--- 3 files changed, 83 insertions(+), 6 deletions(-) diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index 232c4d0582..38458cca1b 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -54,6 +54,77 @@ func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, stateRoot common.Ha return reviveStorageTrie(addr, tr, proofs[0], key) } +// batchFetchExpiredStorageFromRemote request expired state from remote full state node with a list of keys and prefixes. +func batchFetchExpiredFromRemote(fullDB ethdb.FullStateDB, stateRoot common.Hash, addr common.Address, root common.Hash, tr Trie, prefixKeys [][]byte, keys []common.Hash) ([]map[string][]byte, error) { + + ret := make([]map[string][]byte, len(keys)) + prefixKeysStr := make([]string, len(prefixKeys)) + keysStr := make([]string, len(keys)) + + if EnableLocalRevive { + var expiredKeys []common.Hash + var expiredPrefixKeys [][]byte + for i, key := range keys { + val, err := tr.TryLocalRevive(addr, key.Bytes()) + log.Debug("fetchExpiredStorageFromRemote TryLocalRevive", "addr", addr, "key", key, "val", val, "err", err) + if _, ok := err.(*trie.MissingNodeError); !ok { + return nil, err + } + switch err.(type) { + case *trie.MissingNodeError: + expiredKeys = append(expiredKeys, key) + expiredPrefixKeys = append(expiredPrefixKeys, prefixKeys[i]) + case nil: + kv := make(map[string][]byte, 1) + kv[key.String()] = val + ret = append(ret, kv) + default: + return nil, err + } + } + + for i, prefix := range expiredPrefixKeys { + prefixKeysStr[i] = common.Bytes2Hex(prefix) + } + for i, key := range expiredKeys { + keysStr[i] = common.Bytes2Hex(key[:]) + } + + } else { + for i, prefix := range prefixKeys { + prefixKeysStr[i] = common.Bytes2Hex(prefix) + } + + for i, key := range keys { + keysStr[i] = common.Bytes2Hex(key[:]) + } + } + + // cannot revive locally, fetch remote proof + proofs, err := fullDB.GetStorageReviveProof(stateRoot, addr, root, prefixKeysStr, keysStr) + log.Debug("fetchExpiredStorageFromRemote GetStorageReviveProof", "addr", addr, "keys", keysStr, "prefixKeys", prefixKeysStr, "proofs", len(proofs), "err", err) + if err != nil { + return nil, err + } + + if len(proofs) == 0 { + log.Error("cannot find any revive proof from remoteDB", "addr", addr, "keys", keysStr, "prefixKeys", prefixKeysStr) + return nil, fmt.Errorf("cannot find any revive proof from remoteDB") + } + + for i, proof := range proofs { + // kvs, err := reviveStorageTrie(addr, tr, proof, common.HexToHash(keysStr[i])) // TODO(asyukii): this logically should work but it doesn't because of some reason, will need to investigate + kvs, err := reviveStorageTrie(addr, tr, proof, common.HexToHash(proof.Key)) + if err != nil { + log.Error("reviveStorageTrie failed", "addr", addr, "key", keys[i], "err", err) + continue + } + ret = append(ret, kvs) + } + + return ret, nil +} + // reviveStorageTrie revive trie's expired state from proof func reviveStorageTrie(addr common.Address, tr Trie, proof types.ReviveStorageProof, targetKey common.Hash) (map[string][]byte, error) { defer func(start time.Time) { diff --git a/core/state/state_object.go b/core/state/state_object.go index ac3a164e35..be9b2dcbdc 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -484,7 +484,7 @@ func (s *stateObject) updateTrie() (Trie, error) { continue } if _, err = fetchExpiredStorageFromRemote(s.db.fullStateDB, s.db.originalRoot, s.address, s.data.Root, tr, enErr.Path, key); err != nil { - s.db.setError(fmt.Errorf("state object pendingFutureReviveState fetchExpiredStorageFromRemote err, contract: %v, key: %v, err: %v", s.address, key, err)) + s.db.setError(fmt.Errorf("state object pendingFutureReviveState fetchExpiredStorageFromRemote err, contract: %v, key: %v, path: %v, err: %v", s.address, key, enErr.Path, err)) } } } diff --git a/core/state/trie_prefetcher.go b/core/state/trie_prefetcher.go index 56103e0c79..f1102e262e 100644 --- a/core/state/trie_prefetcher.go +++ b/core/state/trie_prefetcher.go @@ -561,6 +561,8 @@ func (sf *subfetcher) loop() { sf.tasks = nil sf.lock.Unlock() + reviveKeys := make([]common.Hash, 0, len(tasks)) + revivePaths := make([][]byte, 0, len(tasks)) // Prefetch any tasks until the loop is interrupted for i, task := range tasks { select { @@ -587,11 +589,8 @@ func (sf *subfetcher) loop() { // handle expired state if sf.enableStateExpiry { if exErr, match := err.(*trie2.ExpiredNodeError); match { - key := common.BytesToHash(task) - _, err = fetchExpiredStorageFromRemote(sf.fullStateDB, sf.state, sf.addr, sf.root, sf.trie, exErr.Path, key) - if err != nil { - log.Error("subfetcher fetchExpiredStorageFromRemote err", "addr", sf.addr, "path", exErr.Path, "err", err) - } + reviveKeys = append(reviveKeys, common.BytesToHash(task)) + revivePaths = append(revivePaths, exErr.Path) } } } @@ -601,6 +600,13 @@ func (sf *subfetcher) loop() { } } + if len(reviveKeys) != 0 { + _, err = batchFetchExpiredFromRemote(sf.fullStateDB, sf.state, sf.addr, sf.root, sf.trie, revivePaths, reviveKeys) + if err != nil { + log.Error("subfetcher batchFetchExpiredFromRemote err", "addr", sf.addr, "state", sf.state, "revivePaths", revivePaths, "reviveKeys", reviveKeys, "err", err) + } + } + case ch := <-sf.copy: // Somebody wants a copy of the current trie, grant them ch <- sf.db.CopyTrie(sf.trie) From 634d1173e810a8895986e6996bbfd06f92b7ac19 Mon Sep 17 00:00:00 2001 From: asyukii Date: Wed, 27 Sep 2023 16:56:24 +0800 Subject: [PATCH 39/65] trie: add unit tests --- trie/trie_test.go | 132 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 2 deletions(-) diff --git a/trie/trie_test.go b/trie/trie_test.go index b59b0fb3e5..dfff72b857 100644 --- a/trie/trie_test.go +++ b/trie/trie_test.go @@ -1023,7 +1023,8 @@ func TestRevive(t *testing.T) { assert.NoError(t, err, "TryRevive failed, key %x, prefixKey %x, val %x", key, prefixKey, val) // Verifiy value exists after revive - v := trie.MustGet(key) + v, err := trie.Get(key) + assert.NoError(t, err, "Get failed, key %x, prefixKey %x, val %x", key, prefixKey, val) assert.Equal(t, val, v, "value mismatch, got %x, exp %x, key %x, prefixKey %x", v, val, key, prefixKey) // Verify root hash @@ -1066,7 +1067,8 @@ func TestReviveCustom(t *testing.T) { _, err = trie.TryRevive(key, proofCache.cacheNubs) assert.NoError(t, err, "TryRevive failed, key %x, prefixKey %x, val %x", key, prefixKey, val) - res := trie.MustGet(key) + res, err := trie.Get(key) + assert.NoError(t, err, "Get failed, key %x, prefixKey %x, val %x", key, prefixKey, val) assert.Equal(t, val, res, "value mismatch, got %x, exp %x, key %x, prefixKey %x", res, val, key, prefixKey) // Verify root hash @@ -1079,6 +1081,132 @@ func TestReviveCustom(t *testing.T) { } } +// TestReviveBadProof tests that a trie cannot be revived from a bad proof +func TestReviveBadProof(t *testing.T) { + + dataA := map[string]string{ + "abcd": "A", "abce": "B", "abde": "C", "abdf": "D", + "defg": "E", "defh": "F", "degh": "G", "degi": "H", + } + + dataB := map[string]string{ + "qwer": "A", "qwet": "B", "qwrt": "C", "qwry": "D", + "abcd": "E", "abce": "F", "abde": "G", "abdf": "H", + } + + trieA := createCustomTrie(dataA, 0) + trieB := createCustomTrie(dataB, 0) + + var proofB proofList + + err := trieB.ProveByPath([]byte("abcd"), nil, &proofB) + assert.NoError(t, err) + + // Expire trie A + trieA.ExpireByPrefix(nil) + + // Construct MPTProofCache + proofCache := makeRawMPTProofCache(nil, proofB) + + // VerifyProof + err = proofCache.VerifyProof() + assert.NoError(t, err) + + // Revive trie + _, err = trieA.TryRevive([]byte("abcd"), proofCache.cacheNubs) + assert.Error(t, err) + + // Verify value does exists after revive + val, err := trieA.Get([]byte("abcd")) + assert.NoError(t, err, "Get failed, key %x, val %x", []byte("abcd"), val) + assert.NotEqual(t, []byte("A"), val) +} + +// TestReviveBadProofAfterUpdate tests that after reviving a path and +// then update the value, old proof should be invalid +func TestReviveBadProofAfterUpdate(t *testing.T) { + trie, vals := nonRandomTrieWithExpiry(100) + + for _, kv := range vals { + key := kv.k + val := kv.v + prefixKeys := getFullNodePrefixKeys(trie, key) + for _, prefixKey := range prefixKeys { + // Generate proof + var proof proofList + err := trie.ProveByPath(key, prefixKey, &proof) + assert.NoError(t, err) + + // Expire trie + trie.ExpireByPrefix(prefixKey) + + proofCache := makeRawMPTProofCache(prefixKey, proof) + err = proofCache.VerifyProof() + assert.NoError(t, err) + + // Revive trie + _, err = trie.TryRevive(key, proofCache.CacheNubs()) + assert.NoError(t, err, "TryRevive failed, key %x, prefixKey %x, val %x", key, prefixKey, val) + + // Verify value exists after revive + v, err := trie.Get(key) + assert.NoError(t, err, "Get failed, key %x, prefixKey %x, val %x", key, prefixKey, val) + assert.Equal(t, val, v, "value mismatch, got %x, exp %x, key %x, prefixKey %x", v, val, key, prefixKey) + + trie.Update(key, []byte("new value")) + v, err = trie.Get(key) + assert.NoError(t, err, "Get failed, key %x, prefixKey %x, val %x", key, prefixKey, val) + assert.Equal(t, []byte("new value"), v, "value mismatch, got %x, exp %x, key %x, prefixKey %x", v, val, key, prefixKey) + + _, err = trie.TryRevive(key, proofCache.CacheNubs()) + assert.NoError(t, err, "TryRevive failed, key %x, prefixKey %x, val %x", key, prefixKey, val) + + v, err = trie.Get(key) + assert.NoError(t, err, "Get failed, key %x, prefixKey %x, val %x", key, prefixKey, val) + assert.Equal(t, []byte("new value"), v, "value mismatch, got %x, exp %x, key %x, prefixKey %x", v, val, key, prefixKey) + + // Reset trie + trie, _ = nonRandomTrieWithExpiry(100) + } + } +} + +func TestPartialReviveFullProof(t *testing.T) { + data := map[string]string{ + "abcd": "A", "abce": "B", "abde": "C", "abdf": "D", + "defg": "E", "defh": "F", "degh": "G", "degi": "H", + } + + trie := createCustomTrie(data, 10) + key := []byte("abcd") + val := []byte("A") + + // Get proof + var proof proofList + err := trie.ProveByPath(key, nil, &proof) + assert.NoError(t, err) + + // Expire trie + err = trie.ExpireByPrefix([]byte{6, 1}) + assert.NoError(t, err) + + // Construct MPTProofCache + proofCache := makeRawMPTProofCache(nil, proof) + + // Verify proof + err = proofCache.VerifyProof() + assert.NoError(t, err) + + // Revive trie + _, err = trie.TryRevive(key, proofCache.cacheNubs) + assert.NoError(t, err) + + // Validate trie + resVal, err := trie.Get(key) + assert.NoError(t, err) + assert.Equal(t, val, resVal) +} + func createCustomTrie(data map[string]string, epoch types.StateEpoch) *Trie { db := NewDatabase(rawdb.NewMemoryDatabase(), nil) trie := NewEmpty(db) From d9e7568fc3db2c7992e3bac9600676e1114c93cf Mon Sep 17 00:00:00 2001 From: asyukii Date: Mon, 2 Oct 2023 10:32:51 +0800 Subject: [PATCH 40/65] api: add cache for storage revive proof remove log --- ethdb/fullstatedb.go | 6 +++--- internal/ethapi/api.go | 28 +++++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/ethdb/fullstatedb.go b/ethdb/fullstatedb.go index 61f591da30..373c2d734b 100644 --- a/ethdb/fullstatedb.go +++ b/ethdb/fullstatedb.go @@ -72,7 +72,7 @@ func (f *FullStateRPCServer) GetStorageReviveProof(stateRoot common.Hash, accoun uncachedKeys := make([]string, 0, len(keys)) ret := make([]types.ReviveStorageProof, 0, len(keys)) for i, key := range keys { - val, ok := f.cache.Get(proofCacheKey(account, root, prefixKeys[i], key)) + val, ok := f.cache.Get(ProofCacheKey(account, root, prefixKeys[i], key)) log.Debug("GetStorageReviveProof hit cache", "account", account, "key", key, "ok", ok) if !ok { uncachedPrefixKeys = append(uncachedPrefixKeys, prefixKeys[i]) @@ -98,14 +98,14 @@ func (f *FullStateRPCServer) GetStorageReviveProof(stateRoot common.Hash, accoun // add to cache for _, proof := range proofs { - f.cache.Add(proofCacheKey(account, root, proof.PrefixKey, proof.Key), proof) + f.cache.Add(ProofCacheKey(account, root, proof.PrefixKey, proof.Key), proof) } ret = append(ret, proofs...) return ret, err } -func proofCacheKey(account common.Address, root common.Hash, prefix, key string) string { +func ProofCacheKey(account common.Address, root common.Hash, prefix, key string) string { buf := bytes.NewBuffer(make([]byte, 0, 67+len(prefix)+len(key))) buf.Write(account[:]) buf.WriteByte('$') diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 44405c3171..d0ceb2c55a 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -45,16 +45,21 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth/tracers/logger" + "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/trie" + lru "github.com/hashicorp/golang-lru" "github.com/tyler-smith/go-bip39" ) -const UnHealthyTimeout = 5 * time.Second +const ( + UnHealthyTimeout = 5 * time.Second + APICache = 10000 +) // max is a helper function which returns the larger of the two given integers. func max(a, b int64) int64 { @@ -624,12 +629,20 @@ func (s *PersonalAccountAPI) Unpair(ctx context.Context, url string, pin string) // BlockChainAPI provides an API to access Ethereum blockchain data. type BlockChainAPI struct { - b Backend + b Backend + cache *lru.Cache } // NewBlockChainAPI creates a new Ethereum blockchain API. func NewBlockChainAPI(b Backend) *BlockChainAPI { - return &BlockChainAPI{b} + cache, err := lru.New(APICache) + if err != nil { + return nil + } + return &BlockChainAPI{ + b: b, + cache: cache, + } } // ChainId is the EIP-155 replay-protection chain id for the current Ethereum chain config. @@ -845,6 +858,14 @@ func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, stateRoot com var proof proofList prefixKey := prefixKeys[i] + + // Check if request has been cached + val, ok := s.cache.Get(ethdb.ProofCacheKey(address, root, storagePrefixKeys[i], storageKeys[i])) + if ok { + storageProof[i] = val.(types.ReviveStorageProof) + continue + } + if err := storageTrie.ProveByPath(crypto.Keccak256(key.Bytes()), prefixKey, &proof); err != nil { return nil, err } @@ -853,6 +874,7 @@ func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, stateRoot com PrefixKey: storagePrefixKeys[i], Proof: proof, } + s.cache.Add(ethdb.ProofCacheKey(address, root, storagePrefixKeys[i], storageKeys[i]), storageProof[i]) } return &types.ReviveResult{ From 2bc3c9cdac70c0d1823e69801dea229dbdb18e86 Mon Sep 17 00:00:00 2001 From: asyukii Date: Tue, 3 Oct 2023 15:02:33 +0800 Subject: [PATCH 41/65] chore(cmd/geth): enable state expiry only with PBSS --- cmd/geth/config.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 764a3fbadf..a9f16531c3 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -32,6 +32,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/scwallet" "github.com/ethereum/go-ethereum/accounts/usbwallet" "github.com/ethereum/go-ethereum/cmd/utils" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/internal/flags" @@ -272,8 +273,14 @@ func applyMetricConfig(ctx *cli.Context, cfg *gethConfig) { } func applyStateExpiryConfig(ctx *cli.Context, cfg *gethConfig) { + if ctx.IsSet(utils.StateExpiryEnableFlag.Name) { - cfg.Eth.StateExpiryEnable = ctx.Bool(utils.StateExpiryEnableFlag.Name) + enableStateExpiry := ctx.Bool(utils.StateExpiryEnableFlag.Name) + if enableStateExpiry && ctx.IsSet(utils.StateSchemeFlag.Name) && ctx.String(utils.StateSchemeFlag.Name) == rawdb.HashScheme { + log.Warn("State expiry is not supported with hash scheme. Disabling state expiry") + enableStateExpiry = false + } + cfg.Eth.StateExpiryEnable = enableStateExpiry } if ctx.IsSet(utils.StateExpiryFullStateEndpointFlag.Name) { cfg.Eth.StateExpiryFullStateEndpoint = ctx.String(utils.StateExpiryFullStateEndpointFlag.Name) From c3a31d7a2592acf53471734a8cbbf39bbe55f3c0 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Sun, 8 Oct 2023 10:44:59 +0800 Subject: [PATCH 42/65] pruner: opt size statistic; trie/inspect: opt inspect in PBSS mode; --- core/blockchain.go | 2 +- core/blockchain_insert.go | 5 +-- core/rawdb/database.go | 6 +++- core/state/pruner/pruner.go | 36 ++++++++++++++++----- core/state/snapshot/snapshot_expire.go | 10 +++--- core/state/snapshot/snapshot_expire_test.go | 2 +- trie/inspect_trie.go | 7 ++-- trie/trie.go | 22 +++++++++---- 8 files changed, 62 insertions(+), 28 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index f040fc5652..2baf0f35a1 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -2147,7 +2147,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) stats.usedGas += usedGas dirty, _ := bc.triedb.Size() - stats.report(chain, it.index, dirty, setHead) + stats.report(chain, it.index, dirty, setHead, bc.chainConfig) if !setHead { // After merge we expect few side chains. Simply count diff --git a/core/blockchain_insert.go b/core/blockchain_insert.go index ffe2d6501c..ab231e2a08 100644 --- a/core/blockchain_insert.go +++ b/core/blockchain_insert.go @@ -17,6 +17,7 @@ package core import ( + "github.com/ethereum/go-ethereum/params" "time" "github.com/ethereum/go-ethereum/common" @@ -39,7 +40,7 @@ const statsReportLimit = 8 * time.Second // report prints statistics if some number of blocks have been processed // or more than a few seconds have passed since the last message. -func (st *insertStats) report(chain []*types.Block, index int, dirty common.StorageSize, setHead bool) { +func (st *insertStats) report(chain []*types.Block, index int, dirty common.StorageSize, setHead bool, config *params.ChainConfig) { // Fetch the timings for the batch var ( now = mclock.Now() @@ -56,7 +57,7 @@ func (st *insertStats) report(chain []*types.Block, index int, dirty common.Stor // Assemble the log context and send it to the logger context := []interface{}{ - "number", end.Number(), "hash", end.Hash(), "miner", end.Coinbase(), + "number", end.Number(), "hash", end.Hash(), "miner", end.Coinbase(), "stateEpoch", types.GetStateEpoch(config, end.Number()), "blocks", st.processed, "txs", txs, "mgas", float64(st.usedGas) / 1000000, "elapsed", common.PrettyDuration(elapsed), "mgasps", float64(st.usedGas) * 1000 / float64(elapsed), } diff --git a/core/rawdb/database.go b/core/rawdb/database.go index d6d1d65d59..11e9580ad1 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -630,6 +630,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { accountSnaps stat storageSnaps stat snapJournal stat + trieJournal stat preimages stat bloomBits stat cliqueSnaps stat @@ -713,6 +714,8 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { epochMetaMetaSize.Add(size) case bytes.Equal(key, snapshotJournalKey): snapJournal.Add(size) + case bytes.Equal(key, trieJournalKey): + trieJournal.Add(size) case bytes.Equal(key, epochMetaSnapshotJournalKey): epochMetaSnapJournalSize.Add(size) case bytes.HasPrefix(key, EpochMetaPlainStatePrefix) && len(key) >= (len(EpochMetaPlainStatePrefix)+common.HashLength): @@ -724,7 +727,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { lastPivotKey, fastTrieProgressKey, snapshotDisabledKey, SnapshotRootKey, snapshotGeneratorKey, snapshotRecoveryKey, txIndexTailKey, fastTxLookupLimitKey, uncleanShutdownKey, badBlockKey, transitionStatusKey, skeletonSyncStatusKey, - persistentStateIDKey, trieJournalKey, snapshotSyncStatusKey, + persistentStateIDKey, snapshotSyncStatusKey, } { if bytes.Equal(key, meta) { metadata.Add(size) @@ -757,6 +760,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { {"Key-Value store", "Path trie state lookups", stateLookups.Size(), stateLookups.Count()}, {"Key-Value store", "Path trie account nodes", accountTries.Size(), accountTries.Count()}, {"Key-Value store", "Path trie storage nodes", storageTries.Size(), storageTries.Count()}, + {"Key-Value store", "Path trie snap journal", trieJournal.Size(), trieJournal.Count()}, {"Key-Value store", "Trie preimages", preimages.Size(), preimages.Count()}, {"Key-Value store", "Account snapshot", accountSnaps.Size(), accountSnaps.Count()}, {"Key-Value store", "Storage snapshot", storageSnaps.Size(), storageSnaps.Count()}, diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go index 5102b8f3d8..f7ad97d7e1 100644 --- a/core/state/pruner/pruner.go +++ b/core/state/pruner/pruner.go @@ -29,6 +29,7 @@ import ( "path/filepath" "strings" "sync" + "sync/atomic" "time" "github.com/prometheus/tsdb/fileutil" @@ -740,8 +741,13 @@ func (p *Pruner) ExpiredPrune(height *big.Int, root common.Hash) error { // here are some issues when just delete it from hash-based storage, because it's shared kv in hbss // but it's ok for pbss. func asyncScanExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch types.StateEpoch, expireContractCh chan *snapshot.ContractItem, pruneExpiredInDisk chan *trie.NodeInfo) error { + var ( + trieCount atomic.Uint64 + start = time.Now() + logged = time.Now() + ) for item := range expireContractCh { - log.Info("start scan trie expired state", "addr", item.Addr, "root", item.Root) + log.Info("start scan trie expired state", "addrHash", item.Addr, "root", item.Root) tr, err := trie.New(&trie.ID{ StateRoot: stateRoot, Owner: item.Addr, @@ -752,11 +758,16 @@ func asyncScanExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch type return err } tr.SetEpoch(epoch) - if err = tr.PruneExpired(pruneExpiredInDisk); err != nil { + if err = tr.PruneExpired(pruneExpiredInDisk, &trieCount); err != nil { log.Error("asyncScanExpiredInTrie, PruneExpired err", "id", item, "err", err) return err } + if time.Since(logged) > 8*time.Second { + log.Info("Pruning expired states", "trieNodes", trieCount.Load()) + logged = time.Now() + } } + log.Info("Scan expired states", "trieNodes", trieCount.Load(), "elapsed", common.PrettyDuration(time.Since(start))) close(pruneExpiredInDisk) return nil } @@ -780,32 +791,39 @@ func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, pruneExpiredInDisk ch addr := info.Addr // delete trie kv trieCount++ - trieSize += common.StorageSize(len(info.Key) + 32) switch scheme { case rawdb.PathScheme: + val := rawdb.ReadTrieNode(diskdb, addr, info.Path, info.Hash, rawdb.PathScheme) + trieSize += common.StorageSize(len(val) + 33 + len(info.Path)) rawdb.DeleteTrieNode(batch, addr, info.Path, info.Hash, rawdb.PathScheme) case rawdb.HashScheme: // hbss has shared kv, so using bloom to filter them out. if !bloom.Contain(info.Hash.Bytes()) { + val := rawdb.ReadTrieNode(diskdb, addr, info.Path, info.Hash, rawdb.HashScheme) + trieSize += common.StorageSize(len(val) + 33) rawdb.DeleteTrieNode(batch, addr, info.Path, info.Hash, rawdb.HashScheme) } } // delete epoch meta if info.IsBranch { epochMetaCount++ - epochMetaSize += common.StorageSize(32 + len(info.Path) + 32) + val := rawdb.ReadEpochMetaPlainState(diskdb, addr, string(info.Path)) + epochMetaSize += common.StorageSize(33 + len(info.Path) + len(val)) rawdb.DeleteEpochMetaPlainState(batch, addr, string(info.Path)) } // replace snapshot kv only epoch if info.IsLeaf { snapCount++ - snapSize += common.StorageSize(32) - if err := snapshot.ShrinkExpiredLeaf(batch, addr, info.Key, info.Epoch, scheme); err != nil { + size, err := snapshot.ShrinkExpiredLeaf(batch, diskdb, addr, info.Key, info.Epoch, scheme) + if err != nil { log.Error("ShrinkExpiredLeaf err", "addr", addr, "key", info.Key, "err", err) } + snapSize += common.StorageSize(size) } if batch.ValueSize() >= ethdb.IdealBatchSize { - batch.Write() + if err := batch.Write(); err != nil { + log.Error("asyncPruneExpiredStorageInDisk, batch write err", "err", err) + } batch.Reset() } if time.Since(logged) > 8*time.Second { @@ -816,7 +834,9 @@ func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, pruneExpiredInDisk ch } } if batch.ValueSize() > 0 { - batch.Write() + if err := batch.Write(); err != nil { + log.Error("asyncPruneExpiredStorageInDisk, batch write err", "err", err) + } batch.Reset() } log.Info("Pruned expired states", "trieNodes", trieCount, "trieSize", trieSize, diff --git a/core/state/snapshot/snapshot_expire.go b/core/state/snapshot/snapshot_expire.go index a4be6a1f84..29520160b0 100644 --- a/core/state/snapshot/snapshot_expire.go +++ b/core/state/snapshot/snapshot_expire.go @@ -8,17 +8,19 @@ import ( ) // ShrinkExpiredLeaf tool function for snapshot kv prune -func ShrinkExpiredLeaf(db ethdb.KeyValueWriter, accountHash common.Hash, storageHash common.Hash, epoch types.StateEpoch, scheme string) error { +func ShrinkExpiredLeaf(writer ethdb.KeyValueWriter, reader ethdb.KeyValueReader, accountHash common.Hash, storageHash common.Hash, epoch types.StateEpoch, scheme string) (int64, error) { switch scheme { case rawdb.HashScheme: //cannot prune snapshot in hbss, because it will used for trie prune, but it's ok in pbss. case rawdb.PathScheme: + val := rawdb.ReadStorageSnapshot(reader, accountHash, storageHash) valWithEpoch := NewValueWithEpoch(epoch, nil) enc, err := EncodeValueToRLPBytes(valWithEpoch) if err != nil { - return err + return 0, err } - rawdb.WriteStorageSnapshot(db, accountHash, storageHash, enc) + rawdb.WriteStorageSnapshot(writer, accountHash, storageHash, enc) + return int64(65 + len(val)), nil } - return nil + return 0, nil } diff --git a/core/state/snapshot/snapshot_expire_test.go b/core/state/snapshot/snapshot_expire_test.go index 50067c53b3..ac20e9c90f 100644 --- a/core/state/snapshot/snapshot_expire_test.go +++ b/core/state/snapshot/snapshot_expire_test.go @@ -21,7 +21,7 @@ func TestShrinkExpiredLeaf(t *testing.T) { db := memorydb.New() rawdb.WriteStorageSnapshot(db, accountHash, storageHash1, encodeSnapVal(NewRawValue([]byte("val1")))) - err := ShrinkExpiredLeaf(db, accountHash, storageHash1, types.StateEpoch0, rawdb.PathScheme) + _, err := ShrinkExpiredLeaf(db, db, accountHash, storageHash1, types.StateEpoch0, rawdb.PathScheme) assert.NoError(t, err) assert.Equal(t, encodeSnapVal(NewValueWithEpoch(types.StateEpoch0, nil)), rawdb.ReadStorageSnapshot(db, accountHash, storageHash1)) diff --git a/trie/inspect_trie.go b/trie/inspect_trie.go index 818ad63cdd..d00c6fde75 100644 --- a/trie/inspect_trie.go +++ b/trie/inspect_trie.go @@ -229,19 +229,16 @@ func (inspect *Inspector) ConcurrentTraversal(theTrie *Trie, theTrieTreeStat *Tr case *shortNode: path = append(path, current.Key...) inspect.ConcurrentTraversal(theTrie, theTrieTreeStat, current.Val, height+1, path) - path = path[:len(path)-len(current.Key)] case *fullNode: for idx, child := range current.Children { if child == nil { continue } - childPath := path - childPath = append(childPath, byte(idx)) if len(inspect.concurrentQueue)*2 < cap(inspect.concurrentQueue) { inspect.wg.Add(1) - go inspect.SubConcurrentTraversal(theTrie, theTrieTreeStat, child, height+1, childPath) + go inspect.SubConcurrentTraversal(theTrie, theTrieTreeStat, child, height+1, copyNewSlice(path, []byte{byte(idx)})) } else { - inspect.ConcurrentTraversal(theTrie, theTrieTreeStat, child, height+1, childPath) + inspect.ConcurrentTraversal(theTrie, theTrieTreeStat, child, height+1, append(path, byte(idx))) } } case hashNode: diff --git a/trie/trie.go b/trie/trie.go index 6a74636b45..f6b747f467 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -26,6 +26,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/trie/trienode" + "sync/atomic" ) var ( @@ -1430,7 +1431,7 @@ type NodeInfo struct { } // PruneExpired traverses the storage trie and prunes all expired nodes. -func (t *Trie) PruneExpired(pruneItemCh chan *NodeInfo) error { +func (t *Trie) PruneExpired(pruneItemCh chan *NodeInfo, stats *atomic.Uint64) error { if !t.enableExpiry { return nil @@ -1444,7 +1445,7 @@ func (t *Trie) PruneExpired(pruneItemCh chan *NodeInfo) error { if pruneErr := t.recursePruneExpiredNode(n, path, epoch, pruneItemCh); pruneErr != nil { log.Error("recursePruneExpiredNode err", "Path", path, "err", pruneErr) } - }) + }, stats) if err != nil { return err } @@ -1452,7 +1453,7 @@ func (t *Trie) PruneExpired(pruneItemCh chan *NodeInfo) error { return nil } -func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, pruner func(n node, path []byte, epoch types.StateEpoch)) error { +func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, pruner func(n node, path []byte, epoch types.StateEpoch), stats *atomic.Uint64) error { // Upon reaching expired node, it will recursively traverse downwards to all the child nodes // and collect their hashes. Then, the corresponding key-value pairs will be deleted from the // database by batches. @@ -1463,16 +1464,22 @@ func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, p switch n := n.(type) { case *shortNode: - err := t.findExpiredSubTree(n.Val, append(path, n.Key...), epoch, pruner) + if stats != nil { + stats.Add(1) + } + err := t.findExpiredSubTree(n.Val, append(path, n.Key...), epoch, pruner, stats) if err != nil { return err } return nil case *fullNode: + if stats != nil { + stats.Add(1) + } var err error // Go through every child and recursively delete expired nodes for i, child := range n.Children { - err = t.findExpiredSubTree(child, append(path, byte(i)), epoch, pruner) + err = t.findExpiredSubTree(child, append(path, byte(i)), epoch, pruner, stats) if err != nil { return err } @@ -1481,6 +1488,9 @@ func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, p case hashNode: resolve, err := t.resolveAndTrack(n, path) + if _, ok := err.(*MissingNodeError); ok { + return nil + } if err != nil { return err } @@ -1488,7 +1498,7 @@ func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, p return err } - return t.findExpiredSubTree(resolve, path, epoch, pruner) + return t.findExpiredSubTree(resolve, path, epoch, pruner, stats) case valueNode: return nil case nil: From bdc2fc95e4bef70d492da2557fe7ec5d7c43453b Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Mon, 9 Oct 2023 11:01:39 +0800 Subject: [PATCH 43/65] flags: refactor state expiry config; --- cmd/geth/config.go | 17 ----- cmd/geth/main.go | 4 ++ cmd/geth/snapshot.go | 5 +- cmd/utils/flags.go | 104 ++++++++++++++++++++++++++++- core/blockchain.go | 41 +++++++----- core/blockchain_insert.go | 8 ++- core/blockchain_reader.go | 6 +- core/rawdb/accessors_epoch_meta.go | 9 +++ core/rawdb/schema.go | 3 + core/state/iterator.go | 4 +- core/state/pruner/pruner.go | 2 +- core/state/state_expiry.go | 27 ++++++-- core/state/state_object.go | 18 ++--- core/state/statedb.go | 49 ++++++++------ core/state/trie_prefetcher.go | 69 ++++++++----------- core/types/state_epoch.go | 11 ++- core/types/state_expiry.go | 103 ++++++++++++++++++++++++++++ eth/backend.go | 3 +- eth/ethconfig/config.go | 4 +- eth/state_accessor.go | 8 +-- params/config.go | 7 +- 21 files changed, 359 insertions(+), 143 deletions(-) create mode 100644 core/types/state_expiry.go diff --git a/cmd/geth/config.go b/cmd/geth/config.go index a9f16531c3..b1744c8040 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -32,7 +32,6 @@ import ( "github.com/ethereum/go-ethereum/accounts/scwallet" "github.com/ethereum/go-ethereum/accounts/usbwallet" "github.com/ethereum/go-ethereum/cmd/utils" - "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/internal/flags" @@ -158,7 +157,6 @@ func makeConfigNode(ctx *cli.Context) (*node.Node, gethConfig) { cfg.Ethstats.URL = ctx.String(utils.EthStatsURLFlag.Name) } applyMetricConfig(ctx, &cfg) - applyStateExpiryConfig(ctx, &cfg) return stack, cfg } @@ -272,21 +270,6 @@ func applyMetricConfig(ctx *cli.Context, cfg *gethConfig) { } } -func applyStateExpiryConfig(ctx *cli.Context, cfg *gethConfig) { - - if ctx.IsSet(utils.StateExpiryEnableFlag.Name) { - enableStateExpiry := ctx.Bool(utils.StateExpiryEnableFlag.Name) - if enableStateExpiry && ctx.IsSet(utils.StateSchemeFlag.Name) && ctx.String(utils.StateSchemeFlag.Name) == rawdb.HashScheme { - log.Warn("State expiry is not supported with hash scheme. Disabling state expiry") - enableStateExpiry = false - } - cfg.Eth.StateExpiryEnable = enableStateExpiry - } - if ctx.IsSet(utils.StateExpiryFullStateEndpointFlag.Name) { - cfg.Eth.StateExpiryFullStateEndpoint = ctx.String(utils.StateExpiryFullStateEndpointFlag.Name) - } -} - func deprecated(field string) bool { switch field { case "ethconfig.Config.EVMInterpreter": diff --git a/cmd/geth/main.go b/cmd/geth/main.go index abbe56a38e..6332506689 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -219,6 +219,10 @@ var ( stateExpiryFlags = []cli.Flag{ utils.StateExpiryEnableFlag, utils.StateExpiryFullStateEndpointFlag, + utils.StateExpiryStateEpoch1BlockFlag, + utils.StateExpiryStateEpoch2BlockFlag, + utils.StateExpiryStateEpochPeriodFlag, + utils.StateExpiryEnableLocalReviveFlag, } ) diff --git a/cmd/geth/snapshot.go b/cmd/geth/snapshot.go index 86b12b3533..78d1e6a9e1 100644 --- a/cmd/geth/snapshot.go +++ b/cmd/geth/snapshot.go @@ -436,13 +436,12 @@ func pruneState(ctx *cli.Context) error { Preimages: cfg.Eth.Preimages, StateHistory: cfg.Eth.StateHistory, StateScheme: cfg.Eth.StateScheme, - EnableStateExpiry: cfg.Eth.StateExpiryEnable, - RemoteEndPoint: cfg.Eth.StateExpiryFullStateEndpoint, + StateExpiryCfg: cfg.Eth.StateExpiryCfg, } prunerconfig := pruner.Config{ Datadir: stack.ResolvePath(""), BloomSize: ctx.Uint64(utils.BloomFilterSizeFlag.Name), - EnableStateExpiry: cfg.Eth.StateExpiryEnable, + EnableStateExpiry: cfg.Eth.StateExpiryCfg.EnableExpiry(), ChainConfig: chainConfig, CacheConfig: cacheConfig, } diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 32068879e7..5f61cd5566 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -23,6 +23,8 @@ import ( "encoding/hex" "errors" "fmt" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" "math" "math/big" "net" @@ -1130,6 +1132,26 @@ var ( Usage: "set state expiry remote full state rpc endpoint, every expired state will fetch from remote", Category: flags.StateExpiryCategory, } + StateExpiryStateEpoch1BlockFlag = &cli.Uint64Flag{ + Name: "state-expiry.epoch1", + Usage: "set state expiry epoch1 block number", + Category: flags.StateExpiryCategory, + } + StateExpiryStateEpoch2BlockFlag = &cli.Uint64Flag{ + Name: "state-expiry.epoch2", + Usage: "set state expiry epoch2 block number", + Category: flags.StateExpiryCategory, + } + StateExpiryStateEpochPeriodFlag = &cli.Uint64Flag{ + Name: "state-expiry.period", + Usage: "set state expiry epoch period after epoch2", + Category: flags.StateExpiryCategory, + } + StateExpiryEnableLocalReviveFlag = &cli.BoolFlag{ + Name: "state-expiry.localrevive", + Usage: "if enable local revive", + Category: flags.StateExpiryCategory, + } ) func init() { @@ -1945,13 +1967,18 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { cfg.StateHistory = ctx.Uint64(StateHistoryFlag.Name) } // Parse state scheme, abort the process if it's not compatible. - chaindb := tryMakeReadOnlyDatabase(ctx, stack) + chaindb := MakeChainDatabase(ctx, stack, false, false) scheme, err := ParseStateScheme(ctx, chaindb) - chaindb.Close() if err != nil { Fatalf("%v", err) } cfg.StateScheme = scheme + seCfg, err := ParseStateExpiryConfig(ctx, chaindb, scheme) + if err != nil { + Fatalf("%v", err) + } + cfg.StateExpiryCfg = seCfg + chaindb.Close() // Parse transaction history flag, if user is still using legacy config // file with 'TxLookupLimit' configured, copy the value to 'TransactionHistory'. @@ -2526,6 +2553,79 @@ func ParseStateScheme(ctx *cli.Context, disk ethdb.Database) (string, error) { return "", fmt.Errorf("incompatible state scheme, stored: %s, provided: %s", stored, scheme) } +func ParseStateExpiryConfig(ctx *cli.Context, disk ethdb.Database, scheme string) (*types.StateExpiryConfig, error) { + enc := rawdb.ReadStateExpiryCfg(disk) + var stored *types.StateExpiryConfig + if len(enc) > 0 { + var cfg types.StateExpiryConfig + if err := rlp.DecodeBytes(enc, &cfg); err != nil { + return nil, err + } + stored = &cfg + } + newCfg := &types.StateExpiryConfig{StateScheme: scheme} + if ctx.IsSet(StateExpiryEnableFlag.Name) { + newCfg.Enable = ctx.Bool(StateExpiryEnableFlag.Name) + } + if ctx.IsSet(StateExpiryFullStateEndpointFlag.Name) { + newCfg.FullStateEndpoint = ctx.String(StateExpiryFullStateEndpointFlag.Name) + } + + // some config will use stored default + if ctx.IsSet(StateExpiryStateEpoch1BlockFlag.Name) { + newCfg.StateEpoch1Block = ctx.Uint64(StateExpiryStateEpoch1BlockFlag.Name) + } else if stored != nil { + newCfg.StateEpoch1Block = stored.StateEpoch1Block + } + if ctx.IsSet(StateExpiryStateEpoch2BlockFlag.Name) { + newCfg.StateEpoch2Block = ctx.Uint64(StateExpiryStateEpoch2BlockFlag.Name) + } else if stored != nil { + newCfg.StateEpoch2Block = stored.StateEpoch2Block + } + if ctx.IsSet(StateExpiryStateEpochPeriodFlag.Name) { + newCfg.StateEpochPeriod = ctx.Uint64(StateExpiryStateEpochPeriodFlag.Name) + } else if stored != nil { + newCfg.StateEpochPeriod = stored.StateEpochPeriod + } + if ctx.IsSet(StateExpiryEnableLocalReviveFlag.Name) { + newCfg.EnableLocalRevive = ctx.Bool(StateExpiryEnableLocalReviveFlag.Name) + } + + // override prune level + newCfg.PruneLevel = types.StateExpiryPruneLevel1 + switch newCfg.StateScheme { + case rawdb.HashScheme: + // TODO(0xbundler): will stop support HBSS later. + newCfg.PruneLevel = types.StateExpiryPruneLevel0 + case rawdb.PathScheme: + newCfg.PruneLevel = types.StateExpiryPruneLevel1 + default: + return nil, fmt.Errorf("not support the state scheme: %v", newCfg.StateScheme) + } + + if err := newCfg.Validation(); err != nil { + return nil, err + } + if err := stored.CheckCompatible(newCfg); err != nil { + return nil, err + } + + log.Info("Apply State Expiry", "cfg", newCfg) + if !newCfg.Enable { + return newCfg, nil + } + + // save it into db + enc, err := rlp.EncodeToBytes(newCfg) + if err != nil { + return nil, err + } + if err = rawdb.WriteStateExpiryCfg(disk, enc); err != nil { + return nil, err + } + return newCfg, nil +} + // MakeTrieDatabase constructs a trie database based on the configured scheme. func MakeTrieDatabase(ctx *cli.Context, disk ethdb.Database, preimage bool, readOnly bool) *trie.Database { config := &trie.Config{ diff --git a/core/blockchain.go b/core/blockchain.go index 2baf0f35a1..dab27b7827 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -160,8 +160,7 @@ type CacheConfig struct { SnapshotWait bool // Wait for snapshot construction on startup. TODO(karalabe): This is a dirty hack for testing, nuke it // state expiry feature - EnableStateExpiry bool - RemoteEndPoint string + StateExpiryCfg *types.StateExpiryConfig } // TriedbConfig derives the configures for trie database. @@ -170,7 +169,7 @@ func (c *CacheConfig) TriedbConfig() *trie.Config { Cache: c.TrieCleanLimit, Preimages: c.Preimages, NoTries: c.NoTries, - EnableStateExpiry: c.EnableStateExpiry, + EnableStateExpiry: c.StateExpiryCfg.EnableExpiry(), } if c.StateScheme == rawdb.HashScheme { config.HashDB = &hashdb.Config{ @@ -300,8 +299,8 @@ type BlockChain struct { doubleSignMonitor *monitor.DoubleSignMonitor // state expiry feature - enableStateExpiry bool - fullStateDB ethdb.FullStateDB + stateExpiryCfg *types.StateExpiryConfig + fullStateDB ethdb.FullStateDB } // NewBlockChain returns a fully initialised block chain using information @@ -374,10 +373,10 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis bc.processor = NewStateProcessor(chainConfig, bc, engine) var err error - if cacheConfig.EnableStateExpiry { - log.Info("enable state expiry feature", "RemoteEndPoint", cacheConfig.RemoteEndPoint) - bc.enableStateExpiry = true - bc.fullStateDB, err = ethdb.NewFullStateRPCServer(cacheConfig.RemoteEndPoint) + if cacheConfig.StateExpiryCfg.EnableExpiry() { + log.Info("enable state expiry feature", "RemoteEndPoint", cacheConfig.StateExpiryCfg.FullStateEndpoint) + bc.stateExpiryCfg = cacheConfig.StateExpiryCfg + bc.fullStateDB, err = ethdb.NewFullStateRPCServer(cacheConfig.StateExpiryCfg.FullStateEndpoint) if err != nil { return nil, err } @@ -618,7 +617,15 @@ func (bc *BlockChain) cacheBlock(hash common.Hash, block *types.Block) { } func (bc *BlockChain) EnableStateExpiry() bool { - return bc.enableStateExpiry + return bc.stateExpiryCfg.EnableExpiry() +} + +func (bc *BlockChain) EnableStateExpiryLocalRevive() bool { + if bc.EnableStateExpiry() { + return bc.stateExpiryCfg.EnableLocalRevive + } + + return false } func (bc *BlockChain) FullStateDB() ethdb.FullStateDB { @@ -1046,8 +1053,8 @@ func (bc *BlockChain) StateAtWithSharedPool(root, startAtBlockHash common.Hash, if err != nil { return nil, err } - if bc.enableStateExpiry { - stateDB.InitStateExpiryFeature(bc.chainConfig, bc.fullStateDB, startAtBlockHash, height) + if bc.EnableStateExpiry() { + stateDB.InitStateExpiryFeature(bc.stateExpiryCfg, bc.fullStateDB, startAtBlockHash, height) } return stateDB, err } @@ -2050,8 +2057,8 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) return it.index, err } bc.updateHighestVerifiedHeader(block.Header()) - if bc.enableStateExpiry { - statedb.InitStateExpiryFeature(bc.chainConfig, bc.fullStateDB, parent.Hash(), block.Number()) + if bc.EnableStateExpiry() { + statedb.InitStateExpiryFeature(bc.stateExpiryCfg, bc.fullStateDB, parent.Hash(), block.Number()) } // Enable prefetching to pull in trie node paths while processing transactions @@ -2062,8 +2069,8 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) // do Prefetch in a separate goroutine to avoid blocking the critical path // 1.do state prefetch for snapshot cache throwaway := statedb.CopyDoPrefetch() - if throwaway != nil && bc.enableStateExpiry { - throwaway.InitStateExpiryFeature(bc.chainConfig, bc.fullStateDB, parent.Hash(), block.Number()) + if throwaway != nil && bc.EnableStateExpiry() { + throwaway.InitStateExpiryFeature(bc.stateExpiryCfg, bc.fullStateDB, parent.Hash(), block.Number()) } go bc.prefetcher.Prefetch(block, throwaway, &bc.vmConfig, interruptCh) @@ -2147,7 +2154,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) stats.usedGas += usedGas dirty, _ := bc.triedb.Size() - stats.report(chain, it.index, dirty, setHead, bc.chainConfig) + stats.report(chain, it.index, dirty, setHead, bc.stateExpiryCfg) if !setHead { // After merge we expect few side chains. Simply count diff --git a/core/blockchain_insert.go b/core/blockchain_insert.go index ab231e2a08..9a225afad3 100644 --- a/core/blockchain_insert.go +++ b/core/blockchain_insert.go @@ -17,7 +17,6 @@ package core import ( - "github.com/ethereum/go-ethereum/params" "time" "github.com/ethereum/go-ethereum/common" @@ -40,7 +39,7 @@ const statsReportLimit = 8 * time.Second // report prints statistics if some number of blocks have been processed // or more than a few seconds have passed since the last message. -func (st *insertStats) report(chain []*types.Block, index int, dirty common.StorageSize, setHead bool, config *params.ChainConfig) { +func (st *insertStats) report(chain []*types.Block, index int, dirty common.StorageSize, setHead bool, config *types.StateExpiryConfig) { // Fetch the timings for the batch var ( now = mclock.Now() @@ -57,10 +56,13 @@ func (st *insertStats) report(chain []*types.Block, index int, dirty common.Stor // Assemble the log context and send it to the logger context := []interface{}{ - "number", end.Number(), "hash", end.Hash(), "miner", end.Coinbase(), "stateEpoch", types.GetStateEpoch(config, end.Number()), + "number", end.Number(), "hash", end.Hash(), "miner", end.Coinbase(), "blocks", st.processed, "txs", txs, "mgas", float64(st.usedGas) / 1000000, "elapsed", common.PrettyDuration(elapsed), "mgasps", float64(st.usedGas) * 1000 / float64(elapsed), } + if config.EnableExpiry() { + context = append(context, []interface{}{"stateEpoch", types.GetStateEpoch(config, end.Number())}...) + } if timestamp := time.Unix(int64(end.Time()), 0); time.Since(timestamp) > time.Minute { context = append(context, []interface{}{"age", common.PrettyAge(timestamp)}...) } diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index 540a70ad00..2369d8a7da 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -356,8 +356,8 @@ func (bc *BlockChain) StateAt(startAtRoot common.Hash, startAtBlockHash common.H if err != nil { return nil, err } - if bc.enableStateExpiry { - sdb.InitStateExpiryFeature(bc.chainConfig, bc.fullStateDB, startAtBlockHash, expectHeight) + if bc.EnableStateExpiry() { + sdb.InitStateExpiryFeature(bc.stateExpiryCfg, bc.fullStateDB, startAtBlockHash, expectHeight) } return sdb, err } @@ -365,6 +365,8 @@ func (bc *BlockChain) StateAt(startAtRoot common.Hash, startAtBlockHash common.H // Config retrieves the chain's fork configuration. func (bc *BlockChain) Config() *params.ChainConfig { return bc.chainConfig } +func (bc *BlockChain) StateExpiryConfig() *types.StateExpiryConfig { return bc.stateExpiryCfg } + // Engine retrieves the blockchain's consensus engine. func (bc *BlockChain) Engine() consensus.Engine { return bc.engine } diff --git a/core/rawdb/accessors_epoch_meta.go b/core/rawdb/accessors_epoch_meta.go index 39c33ad034..9c72f36dc9 100644 --- a/core/rawdb/accessors_epoch_meta.go +++ b/core/rawdb/accessors_epoch_meta.go @@ -45,6 +45,15 @@ func DeleteEpochMetaPlainState(db ethdb.KeyValueWriter, addr common.Hash, path s return db.Delete(epochMetaPlainStateKey(addr, path)) } +func ReadStateExpiryCfg(db ethdb.Reader) []byte { + val, _ := db.Get(stateExpiryCfgKey) + return val +} + +func WriteStateExpiryCfg(db ethdb.KeyValueWriter, val []byte) error { + return db.Put(stateExpiryCfgKey, val) +} + func epochMetaPlainStateKey(addr common.Hash, path string) []byte { key := make([]byte, len(EpochMetaPlainStatePrefix)+len(addr)+len(path)) copy(key[:], EpochMetaPlainStatePrefix) diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 779f5570ba..99498174d4 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -110,6 +110,9 @@ var ( // epochMetaPlainStateMeta save disk layer meta data epochMetaPlainStateMeta = []byte("epochMetaPlainStateMeta") + // stateExpiryCfgKey save state expiry persistence config + stateExpiryCfgKey = []byte("stateExpiryCfgKey") + // 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 diff --git a/core/state/iterator.go b/core/state/iterator.go index 7cb212ff15..24fe4a77d5 100644 --- a/core/state/iterator.go +++ b/core/state/iterator.go @@ -127,8 +127,8 @@ func (it *nodeIterator) step() error { if err != nil { return err } - if it.state.enableStateExpiry { - dataTrie.SetEpoch(it.state.epoch) + if it.state.EnableExpire() { + dataTrie.SetEpoch(it.state.Epoch()) } it.dataIt, err = dataTrie.NodeIterator(nil) if err != nil { diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go index f7ad97d7e1..1d34025328 100644 --- a/core/state/pruner/pruner.go +++ b/core/state/pruner/pruner.go @@ -695,7 +695,7 @@ func (p *Pruner) ExpiredPrune(height *big.Int, root common.Hash) error { var ( pruneExpiredTrieCh = make(chan *snapshot.ContractItem, 100000) pruneExpiredInDiskCh = make(chan *trie.NodeInfo, 100000) - epoch = types.GetStateEpoch(p.config.ChainConfig, height) + epoch = types.GetStateEpoch(p.config.CacheConfig.StateExpiryCfg, height) rets = make([]error, 3) tasksWG sync.WaitGroup ) diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index 38458cca1b..8afce61b01 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -14,13 +14,26 @@ import ( var ( reviveStorageTrieTimer = metrics.NewRegisteredTimer("state/revivetrie/rt", nil) - EnableLocalRevive = false // indicate if using local revive ) +// stateExpiryMeta it contains all state expiry meta for target block +type stateExpiryMeta struct { + enableStateExpiry bool + enableLocalRevive bool + fullStateDB ethdb.FullStateDB + epoch types.StateEpoch + originalRoot common.Hash + originalHash common.Hash +} + +func defaultStateExpiryMeta() *stateExpiryMeta { + return &stateExpiryMeta{enableStateExpiry: false} +} + // fetchExpiredStorageFromRemote request expired state from remote full state node; -func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, stateRoot common.Hash, addr common.Address, root common.Hash, tr Trie, prefixKey []byte, key common.Hash) (map[string][]byte, error) { +func fetchExpiredStorageFromRemote(meta *stateExpiryMeta, addr common.Address, root common.Hash, tr Trie, prefixKey []byte, key common.Hash) (map[string][]byte, error) { log.Debug("fetching expired storage from remoteDB", "addr", addr, "prefix", prefixKey, "key", key) - if EnableLocalRevive { + if meta.enableLocalRevive { // if there need revive expired state, try to revive locally, when the node is not being pruned, just renew the epoch val, err := tr.TryLocalRevive(addr, key.Bytes()) log.Debug("fetchExpiredStorageFromRemote TryLocalRevive", "addr", addr, "key", key, "val", val, "err", err) @@ -40,7 +53,7 @@ func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, stateRoot common.Ha } // cannot revive locally, fetch remote proof - proofs, err := fullDB.GetStorageReviveProof(stateRoot, addr, root, []string{common.Bytes2Hex(prefixKey)}, []string{common.Bytes2Hex(key[:])}) + proofs, err := meta.fullStateDB.GetStorageReviveProof(meta.originalRoot, addr, root, []string{common.Bytes2Hex(prefixKey)}, []string{common.Bytes2Hex(key[:])}) log.Debug("fetchExpiredStorageFromRemote GetStorageReviveProof", "addr", addr, "key", key, "proofs", len(proofs), "err", err) if err != nil { return nil, err @@ -55,13 +68,13 @@ func fetchExpiredStorageFromRemote(fullDB ethdb.FullStateDB, stateRoot common.Ha } // batchFetchExpiredStorageFromRemote request expired state from remote full state node with a list of keys and prefixes. -func batchFetchExpiredFromRemote(fullDB ethdb.FullStateDB, stateRoot common.Hash, addr common.Address, root common.Hash, tr Trie, prefixKeys [][]byte, keys []common.Hash) ([]map[string][]byte, error) { +func batchFetchExpiredFromRemote(expiryMeta *stateExpiryMeta, addr common.Address, root common.Hash, tr Trie, prefixKeys [][]byte, keys []common.Hash) ([]map[string][]byte, error) { ret := make([]map[string][]byte, len(keys)) prefixKeysStr := make([]string, len(prefixKeys)) keysStr := make([]string, len(keys)) - if EnableLocalRevive { + if expiryMeta.enableLocalRevive { var expiredKeys []common.Hash var expiredPrefixKeys [][]byte for i, key := range keys { @@ -101,7 +114,7 @@ func batchFetchExpiredFromRemote(fullDB ethdb.FullStateDB, stateRoot common.Hash } // cannot revive locally, fetch remote proof - proofs, err := fullDB.GetStorageReviveProof(stateRoot, addr, root, prefixKeysStr, keysStr) + proofs, err := expiryMeta.fullStateDB.GetStorageReviveProof(expiryMeta.originalRoot, addr, root, prefixKeysStr, keysStr) log.Debug("fetchExpiredStorageFromRemote GetStorageReviveProof", "addr", addr, "keys", keysStr, "prefixKeys", prefixKeysStr, "proofs", len(proofs), "err", err) if err != nil { return nil, err diff --git a/core/state/state_object.go b/core/state/state_object.go index be9b2dcbdc..9575bb5208 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -174,8 +174,8 @@ func (s *stateObject) getTrie() (Trie, error) { if err != nil { return nil, err } - if s.db.enableStateExpiry { - tr.SetEpoch(s.db.epoch) + if s.db.EnableExpire() { + tr.SetEpoch(s.db.Epoch()) } s.trie = tr } @@ -463,7 +463,7 @@ func (s *stateObject) updateTrie() (Trie, error) { // it must hit in cache value := s.GetState(key) dirtyStorage[key] = common.TrimLeftZeroes(value[:]) - log.Debug("updateTrie access state", "contract", s.address, "key", key, "epoch", s.db.epoch) + log.Debug("updateTrie access state", "contract", s.address, "key", key, "epoch", s.db.Epoch()) } } @@ -483,7 +483,7 @@ func (s *stateObject) updateTrie() (Trie, error) { s.db.setError(fmt.Errorf("state object pendingFutureReviveState err, contract: %v, key: %v, err: %v", s.address, key, err)) continue } - if _, err = fetchExpiredStorageFromRemote(s.db.fullStateDB, s.db.originalRoot, s.address, s.data.Root, tr, enErr.Path, key); err != nil { + if _, err = fetchExpiredStorageFromRemote(s.db.expiryMeta, s.address, s.data.Root, tr, enErr.Path, key); err != nil { s.db.setError(fmt.Errorf("state object pendingFutureReviveState fetchExpiredStorageFromRemote err, contract: %v, key: %v, path: %v, err: %v", s.address, key, enErr.Path, err)) } } @@ -529,7 +529,7 @@ func (s *stateObject) updateTrie() (Trie, error) { var snapshotVal []byte // Encoding []byte cannot fail, ok to ignore the error. if s.db.EnableExpire() { - snapshotVal, _ = snapshot.EncodeValueToRLPBytes(snapshot.NewValueWithEpoch(s.db.epoch, value)) + snapshotVal, _ = snapshot.EncodeValueToRLPBytes(snapshot.NewValueWithEpoch(s.db.Epoch(), value)) } else { snapshotVal, _ = rlp.EncodeToBytes(value) } @@ -815,7 +815,7 @@ func (s *stateObject) accessState(key common.Hash) { return } - if s.db.epoch > s.originStorageEpoch[key] { + if s.db.Epoch() > s.originStorageEpoch[key] { count := s.pendingAccessedState[key] s.pendingAccessedState[key] = count + 1 } @@ -860,7 +860,7 @@ func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash, prefixKey = enErr.Path } - kvs, err := fetchExpiredStorageFromRemote(s.db.fullStateDB, s.db.originalRoot, s.address, s.data.Root, tr, prefixKey, key) + kvs, err := fetchExpiredStorageFromRemote(s.db.expiryMeta, s.address, s.data.Root, tr, prefixKey, key) if err != nil { return nil, err } @@ -894,12 +894,12 @@ func (s *stateObject) getExpirySnapStorage(key common.Hash) ([]byte, error, erro } s.originStorageEpoch[key] = val.GetEpoch() - if !types.EpochExpired(val.GetEpoch(), s.db.epoch) { + if !types.EpochExpired(val.GetEpoch(), s.db.Epoch()) { return val.GetVal(), nil, nil } // if found value not been pruned, just return, local revive later - if EnableLocalRevive && len(val.GetVal()) > 0 { + if s.db.EnableLocalRevive() && len(val.GetVal()) > 0 { s.futureReviveState(key) log.Debug("getExpirySnapStorage GetVal", "addr", s.address, "key", key, "val", hex.EncodeToString(val.GetVal())) return val.GetVal(), nil, nil diff --git a/core/state/statedb.go b/core/state/statedb.go index 7acddabc9f..142e434e59 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -149,10 +149,7 @@ type StateDB struct { nextRevisionId int // state expiry feature - enableStateExpiry bool // default disable - epoch types.StateEpoch // epoch indicate stateDB start at which block's target epoch - fullStateDB ethdb.FullStateDB // RemoteFullStateNode - originalHash common.Hash + expiryMeta *stateExpiryMeta // Measurements gathered during execution for debugging purposes // MetricsMux should be used in more places, but will affect on performance, so following meteration is not accruate @@ -206,7 +203,7 @@ func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) accessList: newAccessList(), transientStorage: newTransientStorage(), hasher: crypto.NewKeccakState(), - epoch: types.StateEpoch0, + expiryMeta: defaultStateExpiryMeta(), } if sdb.snaps != nil { @@ -248,15 +245,20 @@ func (s *StateDB) TransferPrefetcher(prev *StateDB) { // InitStateExpiryFeature it must set in initial, reset later will cause wrong result // Attention: startAtBlockHash corresponding to stateDB's originalRoot, expectHeight is the epoch indicator. -func (s *StateDB) InitStateExpiryFeature(config *params.ChainConfig, remote ethdb.FullStateDB, startAtBlockHash common.Hash, expectHeight *big.Int) *StateDB { +func (s *StateDB) InitStateExpiryFeature(config *types.StateExpiryConfig, remote ethdb.FullStateDB, startAtBlockHash common.Hash, expectHeight *big.Int) *StateDB { if config == nil || expectHeight == nil || remote == nil { panic("cannot init state expiry stateDB with nil config/height/remote") } - s.enableStateExpiry = true - s.fullStateDB = remote - s.epoch = types.GetStateEpoch(config, expectHeight) - s.originalHash = startAtBlockHash - log.Debug("StateDB enable state expiry feature", "expectHeight", expectHeight, "startAtBlockHash", startAtBlockHash, "epoch", s.epoch) + epoch := types.GetStateEpoch(config, expectHeight) + s.expiryMeta = &stateExpiryMeta{ + enableStateExpiry: config.Enable, + enableLocalRevive: config.EnableLocalRevive, + fullStateDB: remote, + epoch: epoch, + originalRoot: s.originalRoot, + originalHash: startAtBlockHash, + } + log.Debug("StateDB enable state expiry feature", "expectHeight", expectHeight, "startAtBlockHash", startAtBlockHash, "epoch", epoch) return s } @@ -280,7 +282,7 @@ func (s *StateDB) StartPrefetcher(namespace string) { } else { s.prefetcher = newTriePrefetcher(s.db, s.originalRoot, common.Hash{}, namespace) } - s.prefetcher.InitStateExpiryFeature(s.epoch, s.originalHash, s.fullStateDB) + s.prefetcher.InitStateExpiryFeature(s.expiryMeta) } } @@ -394,7 +396,7 @@ func (s *StateDB) AddLog(log *types.Log) { } // GetLogs returns the logs matching the specified transaction hash, and annotates -// them with the given blockNumber and blockHash. +// them with the given blockNumber and originalHash. func (s *StateDB) GetLogs(hash common.Hash, blockNumber uint64, blockHash common.Hash) []*types.Log { logs := s.logs[hash] for _, l := range logs { @@ -995,10 +997,7 @@ func (s *StateDB) copyInternal(doPrefetch bool) *StateDB { hasher: crypto.NewKeccakState(), // state expiry copy - epoch: s.epoch, - enableStateExpiry: s.enableStateExpiry, - fullStateDB: s.fullStateDB, - originalHash: s.originalHash, + expiryMeta: s.expiryMeta, // In order for the block producer to be able to use and make additions // to the snapshot tree, we need to copy that as well. Otherwise, any @@ -1075,7 +1074,7 @@ func (s *StateDB) copyInternal(doPrefetch bool) *StateDB { state.transientStorage = s.transientStorage.Copy() state.prefetcher = s.prefetcher - if !s.enableStateExpiry && s.prefetcher != nil && !doPrefetch { + if !s.EnableExpire() && s.prefetcher != nil && !doPrefetch { // If there's a prefetcher running, make an inactive copy of it that can // only access data but does not actively preload (since the user will not // know that they need to explicitly terminate an active copy). @@ -1391,8 +1390,8 @@ func (s *StateDB) deleteStorage(addr common.Address, addrHash common.Hash, root if _, ok := tr.(*trie.EmptyTrie); ok { return false, nil, nil, nil } - if s.enableStateExpiry { - tr.SetEpoch(s.epoch) + if s.EnableExpire() { + tr.SetEpoch(s.Epoch()) } it, err := tr.NodeIterator(nil) if err != nil { @@ -1927,7 +1926,15 @@ func (s *StateDB) AddSlotToAccessList(addr common.Address, slot common.Hash) { } func (s *StateDB) EnableExpire() bool { - return s.enableStateExpiry + return s.expiryMeta.enableStateExpiry +} + +func (s *StateDB) Epoch() types.StateEpoch { + return s.expiryMeta.epoch +} + +func (s *StateDB) EnableLocalRevive() bool { + return s.expiryMeta.enableLocalRevive } // AddressInAccessList returns true if the given address is in the access list. diff --git a/core/state/trie_prefetcher.go b/core/state/trie_prefetcher.go index f1102e262e..62c48bc45b 100644 --- a/core/state/trie_prefetcher.go +++ b/core/state/trie_prefetcher.go @@ -17,8 +17,6 @@ package state import ( - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/ethdb" "sync" "sync/atomic" @@ -67,10 +65,7 @@ type triePrefetcher struct { prefetchChan chan *prefetchMsg // no need to wait for return // state expiry feature - enableStateExpiry bool // default disable - epoch types.StateEpoch // epoch indicate stateDB start at which block's target epoch - fullStateDB ethdb.FullStateDB // RemoteFullStateNode - blockHash common.Hash + expiryMeta *stateExpiryMeta deliveryMissMeter metrics.Meter accountLoadMeter metrics.Meter @@ -116,6 +111,8 @@ func newTriePrefetcher(db Database, root, rootParent common.Hash, namespace stri accountStaleDupMeter: metrics.GetOrRegisterMeter(prefix+"/accountst/dup", nil), accountStaleSkipMeter: metrics.GetOrRegisterMeter(prefix+"/accountst/skip", nil), accountStaleWasteMeter: metrics.GetOrRegisterMeter(prefix+"/accountst/waste", nil), + + expiryMeta: defaultStateExpiryMeta(), } go p.mainLoop() return p @@ -132,8 +129,8 @@ func (p *triePrefetcher) mainLoop() { fetcher := p.fetchers[id] if fetcher == nil { fetcher = newSubfetcher(p.db, p.root, pMsg.owner, pMsg.root, pMsg.addr) - if p.enableStateExpiry { - fetcher.initStateExpiryFeature(p.epoch, p.blockHash, p.fullStateDB) + if p.expiryMeta.enableStateExpiry { + fetcher.initStateExpiryFeature(p.expiryMeta) } fetcher.start() p.fetchersMutex.Lock() @@ -218,11 +215,8 @@ func (p *triePrefetcher) mainLoop() { } // InitStateExpiryFeature it must call in initial period. -func (p *triePrefetcher) InitStateExpiryFeature(epoch types.StateEpoch, blockHash common.Hash, fullStateDB ethdb.FullStateDB) { - p.enableStateExpiry = true - p.epoch = epoch - p.fullStateDB = fullStateDB - p.blockHash = blockHash +func (p *triePrefetcher) InitStateExpiryFeature(expiryMeta *stateExpiryMeta) { + p.expiryMeta = expiryMeta } // close iterates over all the subfetchers, aborts any that were left spinning @@ -376,10 +370,7 @@ type subfetcher struct { trie Trie // Trie being populated with nodes // state expiry feature - enableStateExpiry bool // default disable - epoch types.StateEpoch // epoch indicate stateDB start at which block's target epoch - fullStateDB ethdb.FullStateDB // RemoteFullStateNode - blockHash common.Hash + expiryMeta *stateExpiryMeta tasks [][]byte // Items queued up for retrieval lock sync.Mutex // Lock protecting the task queue @@ -401,16 +392,17 @@ type subfetcher struct { // particular root hash. func newSubfetcher(db Database, state common.Hash, owner common.Hash, root common.Hash, addr common.Address) *subfetcher { sf := &subfetcher{ - db: db, - state: state, - owner: owner, - root: root, - addr: addr, - wake: make(chan struct{}, 1), - stop: make(chan struct{}), - term: make(chan struct{}), - copy: make(chan chan Trie), - seen: make(map[string]struct{}), + db: db, + state: state, + owner: owner, + root: root, + addr: addr, + wake: make(chan struct{}, 1), + stop: make(chan struct{}), + term: make(chan struct{}), + copy: make(chan chan Trie), + seen: make(map[string]struct{}), + expiryMeta: defaultStateExpiryMeta(), } return sf } @@ -420,11 +412,8 @@ func (sf *subfetcher) start() { } // InitStateExpiryFeature it must call in initial period. -func (sf *subfetcher) initStateExpiryFeature(epoch types.StateEpoch, blockHash common.Hash, fullStateDB ethdb.FullStateDB) { - sf.enableStateExpiry = true - sf.epoch = epoch - sf.fullStateDB = fullStateDB - sf.blockHash = blockHash +func (sf *subfetcher) initStateExpiryFeature(expiryMeta *stateExpiryMeta) { + sf.expiryMeta = expiryMeta } // schedule adds a batch of trie keys to the queue to prefetch. @@ -470,8 +459,8 @@ func (sf *subfetcher) scheduleParallel(keys [][]byte) { keysLeftSize := len(keysLeft) for i := 0; i*parallelTriePrefetchCapacity < keysLeftSize; i++ { child := newSubfetcher(sf.db, sf.state, sf.owner, sf.root, sf.addr) - if sf.enableStateExpiry { - child.initStateExpiryFeature(sf.epoch, sf.blockHash, sf.fullStateDB) + if sf.expiryMeta.enableStateExpiry { + child.initStateExpiryFeature(sf.expiryMeta) } child.start() sf.paraChildren = append(sf.paraChildren, child) @@ -526,8 +515,8 @@ func (sf *subfetcher) loop() { trie, err = sf.db.OpenTrie(sf.root) } else { trie, err = sf.db.OpenStorageTrie(sf.state, sf.addr, sf.root) - if err == nil && sf.enableStateExpiry { - trie.SetEpoch(sf.epoch) + if err == nil && sf.expiryMeta.enableStateExpiry { + trie.SetEpoch(sf.expiryMeta.epoch) } } if err != nil { @@ -547,8 +536,8 @@ func (sf *subfetcher) loop() { } else { // address is useless sf.trie, err = sf.db.OpenStorageTrie(sf.state, sf.addr, sf.root) - if err == nil && sf.enableStateExpiry { - trie.SetEpoch(sf.epoch) + if err == nil && sf.expiryMeta.enableStateExpiry { + trie.SetEpoch(sf.expiryMeta.epoch) } } if err != nil { @@ -587,7 +576,7 @@ func (sf *subfetcher) loop() { } else { _, err := sf.trie.GetStorage(sf.addr, task) // handle expired state - if sf.enableStateExpiry { + if sf.expiryMeta.enableStateExpiry { if exErr, match := err.(*trie2.ExpiredNodeError); match { reviveKeys = append(reviveKeys, common.BytesToHash(task)) revivePaths = append(revivePaths, exErr.Path) @@ -601,7 +590,7 @@ func (sf *subfetcher) loop() { } if len(reviveKeys) != 0 { - _, err = batchFetchExpiredFromRemote(sf.fullStateDB, sf.state, sf.addr, sf.root, sf.trie, revivePaths, reviveKeys) + _, err = batchFetchExpiredFromRemote(sf.expiryMeta, sf.addr, sf.root, sf.trie, revivePaths, reviveKeys) if err != nil { log.Error("subfetcher batchFetchExpiredFromRemote err", "addr", sf.addr, "state", sf.state, "revivePaths", revivePaths, "reviveKeys", reviveKeys, "err", err) } diff --git a/core/types/state_epoch.go b/core/types/state_epoch.go index b03b96ea18..7846b9e051 100644 --- a/core/types/state_epoch.go +++ b/core/types/state_epoch.go @@ -4,7 +4,6 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/params" ) const ( @@ -22,7 +21,7 @@ type StateEpoch uint16 // ClaudeBlock indicates start state epoch1. // ElwoodBlock indicates start state epoch2 and start epoch rotate by StateEpochPeriod. // When N>=2 and epochN started, epoch(N-2)'s state will expire. -func GetStateEpoch(config *params.ChainConfig, blockNumber *big.Int) StateEpoch { +func GetStateEpoch(config *StateExpiryConfig, blockNumber *big.Int) StateEpoch { if blockNumber == nil || config == nil { return StateEpoch0 } @@ -30,10 +29,10 @@ func GetStateEpoch(config *params.ChainConfig, blockNumber *big.Int) StateEpoch epoch1Block := epochPeriod epoch2Block := new(big.Int).Add(epoch1Block, epochPeriod) - if config.Parlia != nil && config.Parlia.StateEpochPeriod != 0 { - epochPeriod = new(big.Int).SetUint64(config.Parlia.StateEpochPeriod) - epoch1Block = new(big.Int).SetUint64(config.Parlia.StateEpoch1Block) - epoch2Block = new(big.Int).SetUint64(config.Parlia.StateEpoch2Block) + if config != nil { + epochPeriod = new(big.Int).SetUint64(config.StateEpochPeriod) + epoch1Block = new(big.Int).SetUint64(config.StateEpoch1Block) + epoch2Block = new(big.Int).SetUint64(config.StateEpoch2Block) } if isBlockReached(blockNumber, epoch2Block) { ret := new(big.Int).Sub(blockNumber, epoch2Block) diff --git a/core/types/state_expiry.go b/core/types/state_expiry.go new file mode 100644 index 0000000000..1f5856b9ff --- /dev/null +++ b/core/types/state_expiry.go @@ -0,0 +1,103 @@ +package types + +import ( + "errors" + "fmt" + "github.com/ethereum/go-ethereum/log" + "strings" +) + +const ( + StateExpiryPruneLevel0 = iota // StateExpiryPruneLevel0 is for HBSS, in HBSS we cannot prune any expired snapshot, it need rebuild trie for old tire node prune, it also cannot prune any shared trie node too. + StateExpiryPruneLevel1 // StateExpiryPruneLevel1 is the default level, it left some expired snapshot meta for performance friendly. + StateExpiryPruneLevel2 // StateExpiryPruneLevel2 will prune all expired snapshot kvs and trie nodes, but it will access more times in tire when execution. TODO(0xbundler): will support it later +) + +type StateExpiryConfig struct { + Enable bool + FullStateEndpoint string + StateScheme string + PruneLevel uint8 + StateEpoch1Block uint64 + StateEpoch2Block uint64 + StateEpochPeriod uint64 + EnableLocalRevive bool +} + +func (s *StateExpiryConfig) EnableExpiry() bool { + if s == nil { + return false + } + return s.Enable +} + +func (s *StateExpiryConfig) Validation() error { + if s == nil || !s.Enable { + return nil + } + + s.FullStateEndpoint = strings.TrimSpace(s.FullStateEndpoint) + if s.StateEpoch1Block == 0 || + s.StateEpoch2Block == 0 || + s.StateEpochPeriod == 0 { + return errors.New("StateEpoch1Block or StateEpoch2Block or StateEpochPeriod cannot be 0") + } + + if s.StateEpoch1Block >= s.StateEpoch2Block { + return errors.New("StateEpoch1Block cannot >= StateEpoch2Block") + } + + if s.StateEpochPeriod < DefaultStateEpochPeriod { + log.Warn("The State Expiry state period is too small and may result in frequent expiration affecting performance", + "input", s.StateEpochPeriod, "default", DefaultStateEpochPeriod) + } + + return nil +} + +func (s *StateExpiryConfig) CheckCompatible(newCfg *StateExpiryConfig) error { + if s == nil || newCfg == nil { + return nil + } + + if s.Enable && !newCfg.Enable { + return errors.New("disable state expiry is dangerous after enabled, expired state may pruned") + } + + if err := s.CheckStateEpochCompatible(newCfg.StateEpoch1Block, newCfg.StateEpoch2Block, newCfg.StateEpochPeriod); err != nil { + return err + } + + if s.StateScheme != newCfg.StateScheme { + return errors.New("StateScheme is incompatible") + } + + if s.PruneLevel != newCfg.PruneLevel { + return errors.New("state expiry PruneLevel is incompatible") + } + + return nil +} + +func (s *StateExpiryConfig) CheckStateEpochCompatible(StateEpoch1Block, StateEpoch2Block, StateEpochPeriod uint64) error { + if s == nil { + return nil + } + + if s.StateEpoch1Block != StateEpoch1Block || + s.StateEpoch2Block != StateEpoch2Block || + s.StateEpochPeriod != StateEpochPeriod { + return fmt.Errorf("state Epoch info is incompatible, StateEpoch1Block: [%v|%v], StateEpoch2Block: [%v|%v], StateEpochPeriod: [%v|%v]", + s.StateEpoch1Block, StateEpoch1Block, s.StateEpoch2Block, StateEpoch2Block, s.StateEpochPeriod, StateEpochPeriod) + } + + return nil +} + +func (s *StateExpiryConfig) String() string { + if !s.Enable { + return "State Expiry Disable" + } + return fmt.Sprintf("Enable State Expiry, RemoteEndpoint: %v, StateEpoch: [%v|%v|%v], StateScheme: %v, PruneLevel: %v", + s.FullStateEndpoint, s.StateEpoch1Block, s.StateEpoch2Block, s.StateEpochPeriod, s.StateScheme, s.PruneLevel) +} diff --git a/eth/backend.go b/eth/backend.go index 6cca4141fe..2f191539bb 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -215,8 +215,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { Preimages: config.Preimages, StateHistory: config.StateHistory, StateScheme: config.StateScheme, - EnableStateExpiry: config.StateExpiryEnable, - RemoteEndPoint: config.StateExpiryFullStateEndpoint, + StateExpiryCfg: config.StateExpiryCfg, } ) bcOps := make([]core.BlockChainOption, 0) diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index c2a5c94d3b..091e7c65ca 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -19,6 +19,7 @@ package ethconfig import ( "errors" + "github.com/ethereum/go-ethereum/core/types" "time" "github.com/ethereum/go-ethereum/common" @@ -104,8 +105,7 @@ type Config struct { DisablePeerTxBroadcast bool // state expiry configs - StateExpiryEnable bool - StateExpiryFullStateEndpoint string + StateExpiryCfg *types.StateExpiryConfig // This can be set to list of enrtree:// URLs which will be queried for // for nodes to connect to. diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 445a57fc4d..0a9b9c985f 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -72,7 +72,7 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u database = state.NewDatabaseWithConfig(eth.chainDb, trie.HashDefaults) if statedb, err = state.New(block.Root(), database, nil); err == nil { if eth.blockchain.EnableStateExpiry() { - statedb.InitStateExpiryFeature(eth.blockchain.Config(), eth.blockchain.FullStateDB(), block.Hash(), block.Number()) + statedb.InitStateExpiryFeature(eth.blockchain.StateExpiryConfig(), eth.blockchain.FullStateDB(), block.Hash(), block.Number()) } log.Info("Found disk backend for state trie", "root", block.Root(), "number", block.Number()) return statedb, noopReleaser, nil @@ -102,7 +102,7 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u statedb, err = state.New(current.Root(), database, nil) if err == nil { if eth.blockchain.EnableStateExpiry() { - statedb.InitStateExpiryFeature(eth.blockchain.Config(), eth.blockchain.FullStateDB(), current.Hash(), block.Number()) + statedb.InitStateExpiryFeature(eth.blockchain.StateExpiryConfig(), eth.blockchain.FullStateDB(), current.Hash(), block.Number()) } return statedb, noopReleaser, nil } @@ -124,7 +124,7 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u statedb, err = state.New(current.Root(), database, nil) if err == nil { if eth.blockchain.EnableStateExpiry() { - statedb.InitStateExpiryFeature(eth.blockchain.Config(), eth.blockchain.FullStateDB(), current.Hash(), block.Number()) + statedb.InitStateExpiryFeature(eth.blockchain.StateExpiryConfig(), eth.blockchain.FullStateDB(), current.Hash(), block.Number()) } break } @@ -176,7 +176,7 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u return nil, nil, fmt.Errorf("state reset after block %d failed: %v", current.NumberU64(), err) } if eth.blockchain.EnableStateExpiry() { - statedb.InitStateExpiryFeature(eth.blockchain.Config(), eth.blockchain.FullStateDB(), current.Hash(), new(big.Int).Add(current.Number(), common.Big1)) + statedb.InitStateExpiryFeature(eth.blockchain.StateExpiryConfig(), eth.blockchain.FullStateDB(), current.Hash(), new(big.Int).Add(current.Number(), common.Big1)) } // Hold the state reference and also drop the parent state // to prevent accumulating too many nodes in memory. diff --git a/params/config.go b/params/config.go index b4a8cc050e..fc1f40e6de 100644 --- a/params/config.go +++ b/params/config.go @@ -512,11 +512,8 @@ func (c *CliqueConfig) String() string { // ParliaConfig is the consensus engine configs for proof-of-staked-authority based sealing. type ParliaConfig struct { - Period uint64 `json:"period"` // Number of seconds between blocks to enforce - Epoch uint64 `json:"epoch"` // Epoch length to update validatorSet - StateEpoch1Block uint64 `json:"stateEpoch1Block"` - StateEpoch2Block uint64 `json:"stateEpoch2Block"` - StateEpochPeriod uint64 `json:"stateEpochPeriod"` + Period uint64 `json:"period"` // Number of seconds between blocks to enforce + Epoch uint64 `json:"epoch"` // Epoch length to update validatorSet } // String implements the stringer interface, returning the consensus engine details. From b9a07a187d5b18e9dd36555194733e45902ca911 Mon Sep 17 00:00:00 2001 From: asyukii Date: Thu, 5 Oct 2023 15:41:25 +0800 Subject: [PATCH 44/65] feat(snap): add state expiry support to snap sync add server related for snap sync edit --- core/state/state_expiry.go | 10 +- core/state/sync.go | 29 +++++ eth/backend.go | 1 + eth/downloader/downloader.go | 41 ++++++- eth/downloader/statesync.go | 38 +++++- eth/handler.go | 6 +- eth/protocols/snap/handler.go | 54 +++++++- eth/protocols/snap/sync.go | 104 ++++++++++++++-- eth/protocols/snap/sync_test.go | 75 ++++++++++-- trie/proof.go | 19 +++ trie/stacktrie.go | 210 ++++++++++++++++++++++++++++++-- trie/sync.go | 74 ++++++++--- trie/trie.go | 8 ++ trie/trie_reader.go | 11 +- 14 files changed, 620 insertions(+), 60 deletions(-) diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index 8afce61b01..fbbd45f9a4 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -64,7 +64,7 @@ func fetchExpiredStorageFromRemote(meta *stateExpiryMeta, addr common.Address, r return nil, fmt.Errorf("cannot find any revive proof from remoteDB") } - return reviveStorageTrie(addr, tr, proofs[0], key) + return ReviveStorageTrie(addr, tr, proofs[0], key) } // batchFetchExpiredStorageFromRemote request expired state from remote full state node with a list of keys and prefixes. @@ -126,8 +126,8 @@ func batchFetchExpiredFromRemote(expiryMeta *stateExpiryMeta, addr common.Addres } for i, proof := range proofs { - // kvs, err := reviveStorageTrie(addr, tr, proof, common.HexToHash(keysStr[i])) // TODO(asyukii): this logically should work but it doesn't because of some reason, will need to investigate - kvs, err := reviveStorageTrie(addr, tr, proof, common.HexToHash(proof.Key)) + // kvs, err := ReviveStorageTrie(addr, tr, proof, common.HexToHash(keysStr[i])) // TODO(asyukii): this logically should work but it doesn't because of some reason, will need to investigate + kvs, err := ReviveStorageTrie(addr, tr, proof, common.HexToHash(proof.Key)) if err != nil { log.Error("reviveStorageTrie failed", "addr", addr, "key", keys[i], "err", err) continue @@ -138,8 +138,8 @@ func batchFetchExpiredFromRemote(expiryMeta *stateExpiryMeta, addr common.Addres return ret, nil } -// reviveStorageTrie revive trie's expired state from proof -func reviveStorageTrie(addr common.Address, tr Trie, proof types.ReviveStorageProof, targetKey common.Hash) (map[string][]byte, error) { +// ReviveStorageTrie revive trie's expired state from proof +func ReviveStorageTrie(addr common.Address, tr Trie, proof types.ReviveStorageProof, targetKey common.Hash) (map[string][]byte, error) { defer func(start time.Time) { reviveStorageTrieTimer.Update(time.Since(start)) }(time.Now()) diff --git a/core/state/sync.go b/core/state/sync.go index 61097c6462..1b288b304e 100644 --- a/core/state/sync.go +++ b/core/state/sync.go @@ -55,3 +55,32 @@ func NewStateSync(root common.Hash, database ethdb.KeyValueReader, onLeaf func(k syncer = trie.NewSync(root, database, onAccount, scheme) return syncer } + +func NewStateSyncWithExpiry(root common.Hash, database ethdb.KeyValueReader, onLeaf func(keys [][]byte, leaf []byte) error, scheme string, epoch types.StateEpoch) *trie.Sync { + // Register the storage slot callback if the external callback is specified. + var onSlot func(keys [][]byte, path []byte, leaf []byte, parent common.Hash, parentPath []byte) error + if onLeaf != nil { + onSlot = func(keys [][]byte, path []byte, leaf []byte, parent common.Hash, parentPath []byte) error { + return onLeaf(keys, leaf) + } + } + // Register the account callback to connect the state trie and the storage + // trie belongs to the contract. + var syncer *trie.Sync + onAccount := func(keys [][]byte, path []byte, leaf []byte, parent common.Hash, parentPath []byte) error { + if onLeaf != nil { + if err := onLeaf(keys, leaf); err != nil { + return err + } + } + var obj types.StateAccount + if err := rlp.Decode(bytes.NewReader(leaf), &obj); err != nil { + return err + } + syncer.AddSubTrie(obj.Root, path, parent, parentPath, onSlot) + syncer.AddCodeEntry(common.BytesToHash(obj.CodeHash), path, parent, parentPath) + return nil + } + syncer = trie.NewSyncWithEpoch(root, database, onAccount, scheme, epoch) + return syncer +} diff --git a/eth/backend.go b/eth/backend.go index 2f191539bb..d2977fed36 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -276,6 +276,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { DirectBroadcast: config.DirectBroadcast, DisablePeerTxBroadcast: config.DisablePeerTxBroadcast, PeerSet: peers, + EnableStateExpiry: config.StateExpiryCfg.Enable, }); err != nil { return nil, err } diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go index 14d68844eb..1fe92de837 100644 --- a/eth/downloader/downloader.go +++ b/eth/downloader/downloader.go @@ -131,6 +131,8 @@ type Downloader struct { SnapSyncer *snap.Syncer // TODO(karalabe): make private! hack for now stateSyncStart chan *stateSync + enableStateExpiry bool + // Cancellation and termination cancelPeer string // Identifier of the peer currently being used as the master (cancel on drop) cancelCh chan struct{} // Channel to cancel mid-flight syncs @@ -207,6 +209,10 @@ type BlockChain interface { // TrieDB retrieves the low level trie database used for interacting // with trie nodes. TrieDB() *trie.Database + + Config() *params.ChainConfig + + StateExpiryConfig() *types.StateExpiryConfig } type DownloadOption func(downloader *Downloader) *Downloader @@ -235,6 +241,30 @@ func New(stateDb ethdb.Database, mux *event.TypeMux, chain BlockChain, lightchai return dl } +func NewWithExpiry(stateDb ethdb.Database, mux *event.TypeMux, chain BlockChain, lightchain LightChain, enableStateExpiry bool, dropPeer peerDropFn, options ...DownloadOption) *Downloader { + if lightchain == nil { + lightchain = chain + } + dl := &Downloader{ + stateDB: stateDb, + mux: mux, + queue: newQueue(blockCacheMaxItems, blockCacheInitialItems), + peers: newPeerSet(), + blockchain: chain, + lightchain: lightchain, + dropPeer: dropPeer, + enableStateExpiry: enableStateExpiry, + headerProcCh: make(chan *headerTask, 1), + quitCh: make(chan struct{}), + SnapSyncer: snap.NewSyncerWithStateExpiry(stateDb, chain.TrieDB().Scheme(), enableStateExpiry), + stateSyncStart: make(chan *stateSync), + syncStartBlock: chain.CurrentSnapBlock().Number.Uint64(), + } + + go dl.stateFetcher() + return dl +} + // Progress retrieves the synchronisation boundaries, specifically the origin // block where synchronisation started at (may have failed/suspended); the block // or header sync is currently at; and the latest known block which the sync targets. @@ -1464,10 +1494,14 @@ func (d *Downloader) importBlockResults(results []*fetchResult) error { func (d *Downloader) processSnapSyncContent() error { // Start syncing state of the reported head block. This should get us most of // the state of the pivot block. + var epoch types.StateEpoch + d.pivotLock.RLock() - sync := d.syncState(d.pivotHeader.Root) + sync := d.syncStateWithEpoch(d.pivotHeader.Root, epoch) d.pivotLock.RUnlock() + epoch = types.GetStateEpoch(d.blockchain.StateExpiryConfig(), new(big.Int).SetUint64(d.pivotHeader.Number.Uint64())) + defer func() { // The `sync` object is replaced every time the pivot moves. We need to // defer close the very last active one, hence the lazy evaluation vs. @@ -1516,11 +1550,12 @@ func (d *Downloader) processSnapSyncContent() error { d.pivotLock.RLock() pivot := d.pivotHeader d.pivotLock.RUnlock() + epoch = types.GetStateEpoch(d.blockchain.StateExpiryConfig(), new(big.Int).SetUint64(pivot.Number.Uint64())) if oldPivot == nil { if pivot.Root != sync.root { sync.Cancel() - sync = d.syncState(pivot.Root) + sync = d.syncStateWithEpoch(pivot.Root, epoch) go closeOnErr(sync) } @@ -1558,7 +1593,7 @@ func (d *Downloader) processSnapSyncContent() error { // If new pivot block found, cancel old state retrieval and restart if oldPivot != P { sync.Cancel() - sync = d.syncState(P.Header.Root) + sync = d.syncStateWithEpoch(P.Header.Root, types.GetStateEpoch(d.blockchain.StateExpiryConfig(), new(big.Int).SetUint64(P.Header.Number.Uint64()))) go closeOnErr(sync) oldPivot = P diff --git a/eth/downloader/statesync.go b/eth/downloader/statesync.go index 501af63ed5..e7d9952ff2 100644 --- a/eth/downloader/statesync.go +++ b/eth/downloader/statesync.go @@ -20,6 +20,7 @@ import ( "sync" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" ) @@ -40,6 +41,22 @@ func (d *Downloader) syncState(root common.Hash) *stateSync { return s } +func (d *Downloader) syncStateWithEpoch(root common.Hash, epoch types.StateEpoch) *stateSync { + // Create the state sync + s := newStateSyncWithEpoch(d, root, epoch) + select { + case d.stateSyncStart <- s: + // If we tell the statesync to restart with a new root, we also need + // to wait for it to actually also start -- when old requests have timed + // out or been delivered + <-s.started + case <-d.quitCh: + s.err = errCancelStateFetch + close(s.done) + } + return s +} + // stateFetcher manages the active state sync and accepts requests // on its behalf. func (d *Downloader) stateFetcher() { @@ -77,8 +94,10 @@ func (d *Downloader) runStateSync(s *stateSync) *stateSync { // stateSync schedules requests for downloading a particular state trie defined // by a given state root. type stateSync struct { - d *Downloader // Downloader instance to access and manage current peerset - root common.Hash // State root currently being synced + d *Downloader // Downloader instance to access and manage current peerset + root common.Hash // State root currently being synced + epoch types.StateEpoch + enableStateExpiry bool started chan struct{} // Started is signalled once the sync loop starts cancel chan struct{} // Channel to signal a termination request @@ -99,11 +118,26 @@ func newStateSync(d *Downloader, root common.Hash) *stateSync { } } +func newStateSyncWithEpoch(d *Downloader, root common.Hash, epoch types.StateEpoch) *stateSync { + return &stateSync{ + d: d, + root: root, + epoch: epoch, + enableStateExpiry: true, + cancel: make(chan struct{}), + done: make(chan struct{}), + started: make(chan struct{}), + } +} + // run starts the task assignment and response processing loop, blocking until // it finishes, and finally notifying any goroutines waiting for the loop to // finish. func (s *stateSync) run() { close(s.started) + if s.enableStateExpiry { + s.d.SnapSyncer.UpdateEpoch(s.epoch) + } s.err = s.d.SnapSyncer.Sync(s.root, s.cancel) close(s.done) } diff --git a/eth/handler.go b/eth/handler.go index d081c76266..72bfd8993e 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -123,6 +123,7 @@ type handlerConfig struct { DirectBroadcast bool DisablePeerTxBroadcast bool PeerSet *peerSet + EnableStateExpiry bool } type handler struct { @@ -134,6 +135,8 @@ type handler struct { acceptTxs atomic.Bool // Flag whether we're considered synchronised (enables transaction processing) directBroadcast bool + enableStateExpiry bool + database ethdb.Database txpool txPool votepool votePool @@ -196,6 +199,7 @@ func newHandler(config *handlerConfig) (*handler, error) { peersPerIP: make(map[string]int), requiredBlocks: config.RequiredBlocks, directBroadcast: config.DirectBroadcast, + enableStateExpiry: config.EnableStateExpiry, quitSync: make(chan struct{}), handlerDoneCh: make(chan struct{}), handlerStartCh: make(chan struct{}), @@ -249,7 +253,7 @@ func newHandler(config *handlerConfig) (*handler, error) { downloadOptions = append(downloadOptions, success) */ - h.downloader = downloader.New(config.Database, h.eventMux, h.chain, nil, h.removePeer, downloadOptions...) + h.downloader = downloader.NewWithExpiry(config.Database, h.eventMux, h.chain, nil, config.EnableStateExpiry, h.removePeer, downloadOptions...) // Construct the fetcher (short sync) validator := func(header *types.Header) error { diff --git a/eth/protocols/snap/handler.go b/eth/protocols/snap/handler.go index b2fd03766e..0eaf6a28e4 100644 --- a/eth/protocols/snap/handler.go +++ b/eth/protocols/snap/handler.go @@ -23,7 +23,10 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" + "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/ethdb" "github.com/ethereum/go-ethereum/light" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" @@ -381,13 +384,26 @@ func ServiceGetStorageRangesQuery(chain *core.BlockChain, req *GetStorageRangesP storage []*StorageData last common.Hash abort bool + sv snapshot.SnapValue + hash common.Hash + slot []byte + enc []byte ) for it.Next() { if size >= hardLimit { abort = true break } - hash, slot := it.Hash(), common.CopyBytes(it.Slot()) + hash, enc = it.Hash(), common.CopyBytes(it.Slot()) + if len(enc) > 0 { + sv, err = snapshot.DecodeValueFromRLPBytes(enc) + if err != nil || sv == nil { + log.Warn("Failed to decode storage slot", "err", err) + return nil, nil + } + } + + slot = sv.GetVal() // Track the returned interval for the Merkle proofs last = hash @@ -429,13 +445,23 @@ func ServiceGetStorageRangesQuery(chain *core.BlockChain, req *GetStorageRangesP } proof := light.NewNodeSet() if err := stTrie.Prove(origin[:], proof); err != nil { - log.Warn("Failed to prove storage range", "origin", req.Origin, "err", err) - return nil, nil + if enErr, ok := err.(*trie.ExpiredNodeError); ok { + err := reviveAndGetProof(chain.FullStateDB(), stTrie, req.Root, common.BytesToAddress(account[:]), acc.Root, enErr.Path, origin, proof) + if err != nil { + log.Warn("Failed to prove storage range", "origin", origin, "err", err) + return nil, nil + } + } } if last != (common.Hash{}) { if err := stTrie.Prove(last[:], proof); err != nil { - log.Warn("Failed to prove storage range", "last", last, "err", err) - return nil, nil + if enErr, ok := err.(*trie.ExpiredNodeError); ok { + err := reviveAndGetProof(chain.FullStateDB(), stTrie, req.Root, common.BytesToAddress(account[:]), acc.Root, enErr.Path, last, proof) + if err != nil { + log.Warn("Failed to prove storage range", "origin", origin, "err", err) + return nil, nil + } + } } } for _, blob := range proof.NodeList() { @@ -567,6 +593,24 @@ func ServiceGetTrieNodesQuery(chain *core.BlockChain, req *GetTrieNodesPacket, s return nodes, nil } +func reviveAndGetProof(fullStateDB ethdb.FullStateDB, tr *trie.StateTrie, stateRoot common.Hash, account common.Address, root common.Hash, prefixKey []byte, key common.Hash, proofDb *light.NodeSet) error { + proofs, err := fullStateDB.GetStorageReviveProof(stateRoot, account, root, []string{common.Bytes2Hex(prefixKey)}, []string{common.Bytes2Hex(key[:])}) + if err != nil || len(proofs) == 0 { + return err + } + + _, err = state.ReviveStorageTrie(account, tr, proofs[0], key) + if err != nil { + return err + } + + if err := tr.Prove(key[:], proofDb); err != nil { + return err + } + + return nil +} + // NodeInfo represents a short summary of the `snap` sub-protocol metadata // known about the host peer. type NodeInfo struct{} diff --git a/eth/protocols/snap/sync.go b/eth/protocols/snap/sync.go index f56a9480b9..3e56bf054e 100644 --- a/eth/protocols/snap/sync.go +++ b/eth/protocols/snap/sync.go @@ -34,6 +34,7 @@ import ( "github.com/ethereum/go-ethereum/common/math" "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/crypto" "github.com/ethereum/go-ethereum/ethdb" @@ -408,6 +409,7 @@ type SyncPeer interface { // - The peer remains connected, but does not deliver a response in time // - The peer delivers a stale response after a previous timeout // - The peer delivers a refusal to serve the requested state + type Syncer struct { db ethdb.KeyValueStore // Database to store the trie nodes into (and dedup) scheme string // Node scheme used in node database @@ -423,6 +425,9 @@ type Syncer struct { peerDrop *event.Feed // Event feed to react to peers dropping rates *msgrate.Trackers // Message throughput rates for peers + enableStateExpiry bool + epoch types.StateEpoch + // Request tracking during syncing phase statelessPeers map[string]struct{} // Peers that failed to deliver state data accountIdlers map[string]struct{} // Peers that aren't serving account requests @@ -509,6 +514,39 @@ func NewSyncer(db ethdb.KeyValueStore, scheme string) *Syncer { } } +func NewSyncerWithStateExpiry(db ethdb.KeyValueStore, scheme string, enableStateExpiry bool) *Syncer { + return &Syncer{ + db: db, + scheme: scheme, + + peers: make(map[string]SyncPeer), + peerJoin: new(event.Feed), + peerDrop: new(event.Feed), + rates: msgrate.NewTrackers(log.New("proto", "snap")), + update: make(chan struct{}, 1), + + enableStateExpiry: enableStateExpiry, + + accountIdlers: make(map[string]struct{}), + storageIdlers: make(map[string]struct{}), + bytecodeIdlers: make(map[string]struct{}), + + accountReqs: make(map[uint64]*accountRequest), + storageReqs: make(map[uint64]*storageRequest), + bytecodeReqs: make(map[uint64]*bytecodeRequest), + + trienodeHealIdlers: make(map[string]struct{}), + bytecodeHealIdlers: make(map[string]struct{}), + + trienodeHealReqs: make(map[uint64]*trienodeHealRequest), + bytecodeHealReqs: make(map[uint64]*bytecodeHealRequest), + trienodeHealThrottle: maxTrienodeHealThrottle, // Tune downward instead of insta-filling with junk + stateWriter: db.NewBatch(), + + extProgress: new(SyncProgress), + } +} + // Register injects a new data source into the syncer's peerset. func (s *Syncer) Register(peer SyncPeer) error { // Make sure the peer is not registered yet @@ -572,10 +610,16 @@ func (s *Syncer) Unregister(id string) error { func (s *Syncer) Sync(root common.Hash, cancel chan struct{}) error { // Move the trie root from any previous value, revert stateless markers for // any peers and initialize the syncer if it was not yet run + var scheduler *trie.Sync s.lock.Lock() s.root = root + if s.enableStateExpiry { + scheduler = state.NewStateSyncWithExpiry(root, s.db, s.onHealState, s.scheme, s.epoch) + } else { + scheduler = state.NewStateSync(root, s.db, s.onHealState, s.scheme) + } s.healer = &healTask{ - scheduler: state.NewStateSync(root, s.db, s.onHealState, s.scheme), + scheduler: scheduler, trieTasks: make(map[string]common.Hash), codeTasks: make(map[common.Hash]struct{}), } @@ -717,6 +761,10 @@ func (s *Syncer) Sync(root common.Hash, cancel chan struct{}) error { } } +func (s *Syncer) UpdateEpoch(epoch types.StateEpoch) { + s.epoch = epoch +} + // loadSyncStatus retrieves a previously aborted sync status from the database, // or generates a fresh one if none is available. func (s *Syncer) loadSyncStatus() { @@ -2008,14 +2056,24 @@ func (s *Syncer) processStorageResponse(res *storageResponse) { s.storageBytes += common.StorageSize(len(key) + len(value)) }, } + var genTrie *trie.StackTrie + if s.enableStateExpiry { + genTrie = trie.NewStackTrieWithStateExpiry(func(owner common.Hash, path []byte, hash common.Hash, val []byte) { + rawdb.WriteTrieNode(batch, owner, path, hash, val, s.scheme) + }, func(owner common.Hash, path []byte, blob []byte) { + rawdb.WriteEpochMetaPlainState(batch, owner, string(path), blob) + }, account, s.epoch) + } else { + genTrie = trie.NewStackTrieWithOwner(func(owner common.Hash, path []byte, hash common.Hash, val []byte) { + rawdb.WriteTrieNode(batch, owner, path, hash, val, s.scheme) + }, account) + } tasks = append(tasks, &storageTask{ Next: common.Hash{}, Last: r.End(), root: acc.Root, genBatch: batch, - genTrie: trie.NewStackTrieWithOwner(func(owner common.Hash, path []byte, hash common.Hash, val []byte) { - rawdb.WriteTrieNode(batch, owner, path, hash, val, s.scheme) - }, account), + genTrie: genTrie, }) for r.Next() { batch := ethdb.HookedBatch{ @@ -2024,14 +2082,23 @@ func (s *Syncer) processStorageResponse(res *storageResponse) { s.storageBytes += common.StorageSize(len(key) + len(value)) }, } + if s.enableStateExpiry { + genTrie = trie.NewStackTrieWithStateExpiry(func(owner common.Hash, path []byte, hash common.Hash, val []byte) { + rawdb.WriteTrieNode(batch, owner, path, hash, val, s.scheme) + }, func(owner common.Hash, path []byte, blob []byte) { + rawdb.WriteEpochMetaPlainState(batch, owner, string(path), blob) + }, account, s.epoch) + } else { + genTrie = trie.NewStackTrieWithOwner(func(owner common.Hash, path []byte, hash common.Hash, val []byte) { + rawdb.WriteTrieNode(batch, owner, path, hash, val, s.scheme) + }, account) + } tasks = append(tasks, &storageTask{ Next: r.Start(), Last: r.End(), root: acc.Root, genBatch: batch, - genTrie: trie.NewStackTrieWithOwner(func(owner common.Hash, path []byte, hash common.Hash, val []byte) { - rawdb.WriteTrieNode(batch, owner, path, hash, val, s.scheme) - }, account), + genTrie: genTrie, }) } for _, task := range tasks { @@ -2076,9 +2143,18 @@ func (s *Syncer) processStorageResponse(res *storageResponse) { slots += len(res.hashes[i]) if i < len(res.hashes)-1 || res.subTask == nil { - tr := trie.NewStackTrieWithOwner(func(owner common.Hash, path []byte, hash common.Hash, val []byte) { - rawdb.WriteTrieNode(batch, owner, path, hash, val, s.scheme) - }, account) + var tr *trie.StackTrie + if s.enableStateExpiry { + tr = trie.NewStackTrieWithStateExpiry(func(owner common.Hash, path []byte, hash common.Hash, val []byte) { + rawdb.WriteTrieNode(batch, owner, path, hash, val, s.scheme) + }, func(owner common.Hash, path []byte, blob []byte) { + rawdb.WriteEpochMetaPlainState(batch, owner, string(path), blob) + }, account, s.epoch) + } else { + tr = trie.NewStackTrieWithOwner(func(owner common.Hash, path []byte, hash common.Hash, val []byte) { + rawdb.WriteTrieNode(batch, owner, path, hash, val, s.scheme) + }, account) + } for j := 0; j < len(res.hashes[i]); j++ { tr.Update(res.hashes[i][j][:], res.slots[i][j]) } @@ -2088,7 +2164,13 @@ func (s *Syncer) processStorageResponse(res *storageResponse) { // outdated during the sync, but it can be fixed later during the // snapshot generation. for j := 0; j < len(res.hashes[i]); j++ { - rawdb.WriteStorageSnapshot(batch, account, res.hashes[i][j], res.slots[i][j]) + var snapVal []byte + if s.enableStateExpiry { + snapVal, _ = snapshot.EncodeValueToRLPBytes(snapshot.NewValueWithEpoch(s.epoch, res.slots[i][j])) + } else { + snapVal, _ = rlp.EncodeToBytes(res.slots[i][j]) + } + rawdb.WriteStorageSnapshot(batch, account, res.hashes[i][j], snapVal) // If we're storing large contracts, generate the trie nodes // on the fly to not trash the gluing points diff --git a/eth/protocols/snap/sync_test.go b/eth/protocols/snap/sync_test.go index 1514ad4e13..25cbef9d28 100644 --- a/eth/protocols/snap/sync_test.go +++ b/eth/protocols/snap/sync_test.go @@ -640,6 +640,16 @@ func setupSyncer(scheme string, peers ...*testPeer) *Syncer { return syncer } +func setupSyncerWithExpiry(scheme string, expiry bool, peers ...*testPeer) *Syncer { + stateDb := rawdb.NewMemoryDatabase() + syncer := NewSyncerWithStateExpiry(stateDb, scheme, expiry) + for _, peer := range peers { + syncer.Register(peer) + peer.remote = syncer + } + return syncer +} + // TestSync tests a basic sync with one peer func TestSync(t *testing.T) { t.Parallel() @@ -750,6 +760,7 @@ func TestSyncWithStorage(t *testing.T) { testSyncWithStorage(t, rawdb.HashScheme) testSyncWithStorage(t, rawdb.PathScheme) + testSyncWithStorageStateExpiry(t, rawdb.PathScheme) } func testSyncWithStorage(t *testing.T, scheme string) { @@ -762,7 +773,7 @@ func testSyncWithStorage(t *testing.T, scheme string) { }) } ) - nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 3, 3000, true, false) + nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 3, 3000, true, false, false) mkSource := func(name string) *testPeer { source := newTestPeer(name, t, term) @@ -781,6 +792,35 @@ func testSyncWithStorage(t *testing.T, scheme string) { verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) } +func testSyncWithStorageStateExpiry(t *testing.T, scheme string) { + var ( + once sync.Once + cancel = make(chan struct{}) + term = func() { + once.Do(func() { + close(cancel) + }) + } + ) + nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 3, 3000, true, false, true) + mkSource := func(name string) *testPeer { + source := newTestPeer(name, t, term) + source.accountTrie = sourceAccountTrie.Copy() + source.accountValues = elems + source.setStorageTries(storageTries) + source.storageValues = storageElems + return source + } + syncer := setupSyncerWithExpiry(nodeScheme, true, mkSource("sourceA")) + syncer.UpdateEpoch(10) + done := checkStall(t, term) + if err := syncer.Sync(sourceAccountTrie.Hash(), cancel); err != nil { + t.Fatalf("sync failed: %v", err) + } + close(done) + verifyTrie(scheme, syncer.db, sourceAccountTrie.Hash(), t) +} + // TestMultiSyncManyUseless contains one good peer, and many which doesn't return anything valuable at all func TestMultiSyncManyUseless(t *testing.T) { t.Parallel() @@ -799,7 +839,7 @@ func testMultiSyncManyUseless(t *testing.T, scheme string) { }) } ) - nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false) + nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false) mkSource := func(name string, noAccount, noStorage, noTrieNode bool) *testPeer { source := newTestPeer(name, t, term) @@ -853,7 +893,7 @@ func testMultiSyncManyUselessWithLowTimeout(t *testing.T, scheme string) { }) } ) - nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false) + nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false) mkSource := func(name string, noAccount, noStorage, noTrieNode bool) *testPeer { source := newTestPeer(name, t, term) @@ -912,7 +952,7 @@ func testMultiSyncManyUnresponsive(t *testing.T, scheme string) { }) } ) - nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false) + nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false) mkSource := func(name string, noAccount, noStorage, noTrieNode bool) *testPeer { source := newTestPeer(name, t, term) @@ -1215,7 +1255,7 @@ func testSyncBoundaryStorageTrie(t *testing.T, scheme string) { }) } ) - nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 10, 1000, false, true) + nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 10, 1000, false, true, false) mkSource := func(name string) *testPeer { source := newTestPeer(name, t, term) @@ -1257,7 +1297,7 @@ func testSyncWithStorageAndOneCappedPeer(t *testing.T, scheme string) { }) } ) - nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 300, 1000, false, false) + nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 300, 1000, false, false, false) mkSource := func(name string, slow bool) *testPeer { source := newTestPeer(name, t, term) @@ -1304,7 +1344,7 @@ func testSyncWithStorageAndCorruptPeer(t *testing.T, scheme string) { }) } ) - nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false) + nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false) mkSource := func(name string, handler storageHandlerFunc) *testPeer { source := newTestPeer(name, t, term) @@ -1348,7 +1388,7 @@ func testSyncWithStorageAndNonProvingPeer(t *testing.T, scheme string) { }) } ) - nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false) + nodeScheme, sourceAccountTrie, elems, storageTries, storageElems := makeAccountTrieWithStorage(scheme, 100, 3000, true, false, false) mkSource := func(name string, handler storageHandlerFunc) *testPeer { source := newTestPeer(name, t, term) @@ -1608,16 +1648,22 @@ func makeAccountTrieWithStorageWithUniqueStorage(scheme string, accounts, slots } // makeAccountTrieWithStorage spits out a trie, along with the leafs -func makeAccountTrieWithStorage(scheme string, accounts, slots int, code, boundary bool) (string, *trie.Trie, []*kv, map[common.Hash]*trie.Trie, map[common.Hash][]*kv) { +func makeAccountTrieWithStorage(scheme string, accounts, slots int, code, boundary bool, expiry bool) (string, *trie.Trie, []*kv, map[common.Hash]*trie.Trie, map[common.Hash][]*kv) { var ( - db = trie.NewDatabase(rawdb.NewMemoryDatabase(), newDbConfig(scheme)) - accTrie = trie.NewEmpty(db) + db *trie.Database + accTrie *trie.Trie entries []*kv storageRoots = make(map[common.Hash]common.Hash) storageTries = make(map[common.Hash]*trie.Trie) storageEntries = make(map[common.Hash][]*kv) nodes = trienode.NewMergedNodeSet() ) + if expiry { + db = trie.NewDatabase(rawdb.NewMemoryDatabase(), newDbConfigWithExpiry(scheme, expiry)) + } else { + db = trie.NewDatabase(rawdb.NewMemoryDatabase(), newDbConfig(scheme)) + } + accTrie = trie.NewEmpty(db) // Create n accounts in the trie for i := uint64(1); i <= uint64(accounts); i++ { key := key32(i) @@ -1897,3 +1943,10 @@ func newDbConfig(scheme string) *trie.Config { } return &trie.Config{PathDB: pathdb.Defaults} } + +func newDbConfigWithExpiry(scheme string, expiry bool) *trie.Config { + if scheme == rawdb.HashScheme { + return &trie.Config{} + } + return &trie.Config{PathDB: pathdb.Defaults, EnableStateExpiry: expiry} +} diff --git a/trie/proof.go b/trie/proof.go index 7e62c4a8b1..f4c784c143 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -37,6 +37,9 @@ import ( // nodes of the longest existing prefix of the key (at least the root node), ending // with the node that proves the absence of the key. func (t *Trie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error { + + var nodeEpoch types.StateEpoch + // Short circuit if the trie is already committed and not usable. if t.committed { return ErrCommitted @@ -48,7 +51,14 @@ func (t *Trie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error { tn = t.root ) key = keybytesToHex(key) + + if t.enableExpiry { + nodeEpoch = t.getRootEpoch() + } for len(key) > 0 && tn != nil { + if t.enableExpiry && t.epochExpired(tn, nodeEpoch) { + return NewExpiredNodeError(prefix, nodeEpoch) + } switch n := tn.(type) { case *shortNode: if len(key) < len(n.Key) || !bytes.Equal(n.Key, key[:len(n.Key)]) { @@ -61,6 +71,9 @@ func (t *Trie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error { } nodes = append(nodes, n) case *fullNode: + if t.enableExpiry { + nodeEpoch = n.GetChildEpoch(int(key[0])) + } tn = n.Children[key[0]] prefix = append(prefix, key[0]) key = key[1:] @@ -80,6 +93,12 @@ func (t *Trie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error { // clean cache or the database, they are all in their own // copy and safe to use unsafe decoder. tn = mustDecodeNodeUnsafe(n, blob) + + if child, ok := tn.(*fullNode); t.enableExpiry && ok { + if err = t.resolveEpochMeta(child, nodeEpoch, prefix); err != nil { + return err + } + } default: panic(fmt.Sprintf("%T: invalid node: %v", tn, tn)) } diff --git a/trie/stacktrie.go b/trie/stacktrie.go index ee1ce28291..aed56a2241 100644 --- a/trie/stacktrie.go +++ b/trie/stacktrie.go @@ -27,6 +27,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie/epochmeta" ) var ErrCommitDisabled = errors.New("no database for committing") @@ -40,6 +42,7 @@ var stPool = sync.Pool{ // NodeWriteFunc is used to provide all information of a dirty node for committing // so that callers can flush nodes into database with desired scheme. type NodeWriteFunc = func(owner common.Hash, path []byte, hash common.Hash, blob []byte) +type NodeMetaWriteFunc = func(owner common.Hash, path []byte, blob []byte) func stackTrieFromPool(writeFn NodeWriteFunc, owner common.Hash) *StackTrie { st := stPool.Get().(*StackTrie) @@ -48,6 +51,16 @@ func stackTrieFromPool(writeFn NodeWriteFunc, owner common.Hash) *StackTrie { return st } +func stackTrieFromPoolWithExpiry(writeFn NodeWriteFunc, writeMetaFn NodeMetaWriteFunc, owner common.Hash, epoch types.StateEpoch) *StackTrie { + st := stPool.Get().(*StackTrie) + st.owner = owner + st.writeFn = writeFn + st.writeMetaFn = writeMetaFn + st.epoch = epoch + st.enableStateExpiry = true + return st +} + func returnToPool(st *StackTrie) { st.Reset() stPool.Put(st) @@ -57,12 +70,16 @@ func returnToPool(st *StackTrie) { // in order. Once it determines that a subtree will no longer be inserted // into, it will hash it and free up the memory it uses. type StackTrie struct { - owner common.Hash // the owner of the trie - nodeType uint8 // node type (as in branch, ext, leaf) - val []byte // value contained by this node if it's a leaf - key []byte // key chunk covered by this (leaf|ext) node - children [16]*StackTrie // list of children (for branch and exts) - writeFn NodeWriteFunc // function for committing nodes, can be nil + owner common.Hash // the owner of the trie + nodeType uint8 // node type (as in branch, ext, leaf) + val []byte // value contained by this node if it's a leaf + key []byte // key chunk covered by this (leaf|ext) node + children [16]*StackTrie // list of children (for branch and exts) + enableStateExpiry bool // whether to enable state expiry + epoch types.StateEpoch + epochMap [16]types.StateEpoch + writeFn NodeWriteFunc // function for committing nodes, can be nil + writeMetaFn NodeMetaWriteFunc // function for committing epoch metadata, can be nil } // NewStackTrie allocates and initializes an empty trie. @@ -83,6 +100,17 @@ func NewStackTrieWithOwner(writeFn NodeWriteFunc, owner common.Hash) *StackTrie } } +func NewStackTrieWithStateExpiry(writeFn NodeWriteFunc, writeMetaFn NodeMetaWriteFunc, owner common.Hash, epoch types.StateEpoch) *StackTrie { + return &StackTrie{ + owner: owner, + nodeType: emptyNode, + epoch: epoch, + enableStateExpiry: true, + writeFn: writeFn, + writeMetaFn: writeMetaFn, + } +} + // NewFromBinary initialises a serialized stacktrie with the given db. func NewFromBinary(data []byte, writeFn NodeWriteFunc) (*StackTrie, error) { var st StackTrie @@ -208,6 +236,10 @@ func (st *StackTrie) Update(key, value []byte) error { if len(value) == 0 { panic("deletion not supported") } + if st.enableStateExpiry { + st.insertWithEpoch(k[:len(k)-1], value, nil, st.epoch) + return nil + } st.insert(k[:len(k)-1], value, nil) return nil } @@ -387,6 +419,154 @@ func (st *StackTrie) insert(key, value []byte, prefix []byte) { } } +func (st *StackTrie) insertWithEpoch(key, value []byte, prefix []byte, epoch types.StateEpoch) { + switch st.nodeType { + case branchNode: /* Branch */ + idx := int(key[0]) + + // Unresolve elder siblings + for i := idx - 1; i >= 0; i-- { + if st.children[i] != nil { + if st.children[i].nodeType != hashedNode { + st.children[i].hash(append(prefix, byte(i))) + } + break + } + } + + // Add new child + if st.children[idx] == nil { + st.children[idx] = newLeaf(st.owner, key[1:], value, st.writeFn) + } else { + st.children[idx].insertWithEpoch(key[1:], value, append(prefix, key[0]), epoch) + } + st.epochMap[idx] = epoch + + case extNode: /* Ext */ + // Compare both key chunks and see where they differ + diffidx := st.getDiffIndex(key) + + // Check if chunks are identical. If so, recurse into + // the child node. Otherwise, the key has to be split + // into 1) an optional common prefix, 2) the fullnode + // representing the two differing path, and 3) a leaf + // for each of the differentiated subtrees. + if diffidx == len(st.key) { + // Ext key and key segment are identical, recurse into + // the child node. + st.children[0].insertWithEpoch(key[diffidx:], value, append(prefix, key[:diffidx]...), epoch) + return + } + // Save the original part. Depending if the break is + // at the extension's last byte or not, create an + // intermediate extension or use the extension's child + // node directly. + var n *StackTrie + if diffidx < len(st.key)-1 { + // Break on the non-last byte, insert an intermediate + // extension. The path prefix of the newly-inserted + // extension should also contain the different byte. + n = newExt(st.owner, st.key[diffidx+1:], st.children[0], st.writeFn) + n.hash(append(prefix, st.key[:diffidx+1]...)) + } else { + // Break on the last byte, no need to insert + // an extension node: reuse the current node. + // The path prefix of the original part should + // still be same. + n = st.children[0] + n.hash(append(prefix, st.key...)) + } + var p *StackTrie + if diffidx == 0 { + // the break is on the first byte, so + // the current node is converted into + // a branch node. + st.children[0] = nil + p = st + st.nodeType = branchNode + } else { + // the common prefix is at least one byte + // long, insert a new intermediate branch + // node. + st.children[0] = stackTrieFromPoolWithExpiry(st.writeFn, st.writeMetaFn, st.owner, st.epoch) + st.children[0].nodeType = branchNode + p = st.children[0] + } + // Create a leaf for the inserted part + o := newLeaf(st.owner, key[diffidx+1:], value, st.writeFn) + + // Insert both child leaves where they belong: + origIdx := st.key[diffidx] + newIdx := key[diffidx] + p.children[origIdx] = n + p.children[newIdx] = o + st.key = st.key[:diffidx] + p.epochMap[origIdx] = epoch + p.epochMap[newIdx] = epoch + + case leafNode: /* Leaf */ + // Compare both key chunks and see where they differ + diffidx := st.getDiffIndex(key) + + // Overwriting a key isn't supported, which means that + // the current leaf is expected to be split into 1) an + // optional extension for the common prefix of these 2 + // keys, 2) a fullnode selecting the path on which the + // keys differ, and 3) one leaf for the differentiated + // component of each key. + if diffidx >= len(st.key) { + panic("Trying to insert into existing key") + } + + // Check if the split occurs at the first nibble of the + // chunk. In that case, no prefix extnode is necessary. + // Otherwise, create that + var p *StackTrie + if diffidx == 0 { + // Convert current leaf into a branch + st.nodeType = branchNode + p = st + st.children[0] = nil + } else { + // Convert current node into an ext, + // and insert a child branch node. + st.nodeType = extNode + st.children[0] = NewStackTrieWithStateExpiry(st.writeFn, st.writeMetaFn, st.owner, st.epoch) + st.children[0].nodeType = branchNode + p = st.children[0] + } + + // Create the two child leaves: one containing the original + // value and another containing the new value. The child leaf + // is hashed directly in order to free up some memory. + origIdx := st.key[diffidx] + p.children[origIdx] = newLeaf(st.owner, st.key[diffidx+1:], st.val, st.writeFn) + p.children[origIdx].hash(append(prefix, st.key[:diffidx+1]...)) + + newIdx := key[diffidx] + p.children[newIdx] = newLeaf(st.owner, key[diffidx+1:], value, st.writeFn) + + p.epochMap[origIdx] = epoch + p.epochMap[newIdx] = epoch + + // Finally, cut off the key part that has been passed + // over to the children. + st.key = st.key[:diffidx] + st.val = nil + + case emptyNode: /* Empty */ + st.nodeType = leafNode + st.key = key + st.val = value + + case hashedNode: + panic("trying to insert into hash") + + default: + panic("invalid type") + } +} + // hash converts st into a 'hashedNode', if possible. Possible outcomes: // // 1. The rlp-encoded value was >= 32 bytes: @@ -469,6 +649,7 @@ func (st *StackTrie) hashRec(hasher *hasher, path []byte) { panic("invalid node type") } + prevNodeType := st.nodeType st.nodeType = hashedNode st.key = st.key[:0] if len(encodedNode) < 32 { @@ -481,6 +662,12 @@ func (st *StackTrie) hashRec(hasher *hasher, path []byte) { st.val = hasher.hashData(encodedNode) if st.writeFn != nil { st.writeFn(st.owner, path, common.BytesToHash(st.val), encodedNode) + if st.enableStateExpiry && prevNodeType == branchNode && st.writeMetaFn != nil { + epochMeta := epochmeta.NewBranchNodeEpochMeta(st.epochMap) + buf := rlp.NewEncoderBuffer(nil) + epochMeta.Encode(buf) + st.writeMetaFn(st.owner, path, buf.ToBytes()) + } } } @@ -514,6 +701,9 @@ func (st *StackTrie) Commit() (h common.Hash, err error) { if st.writeFn == nil { return common.Hash{}, ErrCommitDisabled } + if st.enableStateExpiry && st.writeMetaFn == nil { + return common.Hash{}, ErrCommitDisabled + } hasher := newHasher(false) defer returnHasherToPool(hasher) @@ -529,6 +719,12 @@ func (st *StackTrie) Commit() (h common.Hash, err error) { hasher.sha.Write(st.val) hasher.sha.Read(h[:]) - st.writeFn(st.owner, nil, h, st.val) + st.writeFn(st.owner, nil, h, st.val) // func(owner common.Hash, path []byte, hash common.Hash, blob []byte) + if st.enableStateExpiry && st.nodeType == branchNode && st.writeMetaFn != nil { + epochMeta := epochmeta.NewBranchNodeEpochMeta(st.epochMap) + buf := rlp.NewEncoderBuffer(nil) + epochMeta.Encode(buf) + st.writeMetaFn(st.owner, nil, buf.ToBytes()) + } return h, nil } diff --git a/trie/sync.go b/trie/sync.go index 35d36f6e04..c6f0da5e86 100644 --- a/trie/sync.go +++ b/trie/sync.go @@ -27,6 +27,8 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/trie/epochmeta" ) // ErrNotRequested is returned by the trie sync when it's requested to process a @@ -93,9 +95,10 @@ type LeafCallback func(keys [][]byte, path []byte, leaf []byte, parent common.Ha // nodeRequest represents a scheduled or already in-flight trie node retrieval request. type nodeRequest struct { - hash common.Hash // Hash of the trie node to retrieve - path []byte // Merkle path leading to this node for prioritization - data []byte // Data content of the node, cached until all subtrees complete + hash common.Hash // Hash of the trie node to retrieve + path []byte // Merkle path leading to this node for prioritization + data []byte // Data content of the node, cached until all subtrees complete + epochMap [16]types.StateEpoch parent *nodeRequest // Parent state node referencing this entry deps int // Number of dependencies before allowed to commit this node @@ -125,10 +128,11 @@ type CodeSyncResult struct { // syncMemBatch is an in-memory buffer of successfully downloaded but not yet // persisted data items. type syncMemBatch struct { - nodes map[string][]byte // In-memory membatch of recently completed nodes - hashes map[string]common.Hash // Hashes of recently completed nodes - codes map[common.Hash][]byte // In-memory membatch of recently completed codes - size uint64 // Estimated batch-size of in-memory data. + nodes map[string][]byte // In-memory membatch of recently completed nodes + hashes map[string]common.Hash // Hashes of recently completed nodes + epochMaps map[string][16]types.StateEpoch + codes map[common.Hash][]byte // In-memory membatch of recently completed codes + size uint64 // Estimated batch-size of in-memory data. } // newSyncMemBatch allocates a new memory-buffer for not-yet persisted trie nodes. @@ -156,13 +160,15 @@ func (batch *syncMemBatch) hasCode(hash common.Hash) bool { // unknown trie hashes to retrieve, accepts node data associated with said hashes // and reconstructs the trie step by step until all is done. type Sync struct { - scheme string // Node scheme descriptor used in database. - database ethdb.KeyValueReader // Persistent database to check for existing entries - membatch *syncMemBatch // Memory buffer to avoid frequent database writes - nodeReqs map[string]*nodeRequest // Pending requests pertaining to a trie node path - codeReqs map[common.Hash]*codeRequest // Pending requests pertaining to a code hash - queue *prque.Prque[int64, any] // Priority queue with the pending requests - fetches map[int]int // Number of active fetches per trie node depth + scheme string // Node scheme descriptor used in database. + database ethdb.KeyValueReader // Persistent database to check for existing entries + membatch *syncMemBatch // Memory buffer to avoid frequent database writes + nodeReqs map[string]*nodeRequest // Pending requests pertaining to a trie node path + codeReqs map[common.Hash]*codeRequest // Pending requests pertaining to a code hash + queue *prque.Prque[int64, any] // Priority queue with the pending requests + fetches map[int]int // Number of active fetches per trie node depth + enableStateExpiry bool + epoch types.StateEpoch } // NewSync creates a new trie data download scheduler. @@ -180,6 +186,22 @@ func NewSync(root common.Hash, database ethdb.KeyValueReader, callback LeafCallb return ts } +func NewSyncWithEpoch(root common.Hash, database ethdb.KeyValueReader, callback LeafCallback, scheme string, epoch types.StateEpoch) *Sync { + ts := &Sync{ + scheme: scheme, + database: database, + membatch: newSyncMemBatch(), + nodeReqs: make(map[string]*nodeRequest), + codeReqs: make(map[common.Hash]*codeRequest), + queue: prque.New[int64, any](nil), // Ugh, can contain both string and hash, whyyy + fetches: make(map[int]int), + epoch: epoch, + enableStateExpiry: true, + } + ts.AddSubTrie(root, nil, common.Hash{}, nil, callback) + return ts +} + // AddSubTrie registers a new trie to the sync code, rooted at the designated // parent for completion tracking. The given path is a unique node path in // hex format and contain all the parent path if it's layered trie node. @@ -328,6 +350,14 @@ func (s *Sync) ProcessNode(result NodeSyncResult) error { } req.data = result.Data + if fn, ok := node.(*fullNode); s.enableStateExpiry && ok { + for i := 0; i < 16; i++ { + if fn.Children[i] != nil { + req.epochMap[i] = s.epoch + } + } + } + // Create and schedule a request for all the children nodes requests, err := s.children(req, node) if err != nil { @@ -351,6 +381,14 @@ func (s *Sync) Commit(dbw ethdb.Batch) error { for path, value := range s.membatch.nodes { owner, inner := ResolvePath([]byte(path)) rawdb.WriteTrieNode(dbw, owner, inner, s.membatch.hashes[path], value, s.scheme) + if s.enableStateExpiry { + if s.membatch.epochMaps[path] != [16]types.StateEpoch{} { + epochMeta := epochmeta.NewBranchNodeEpochMeta(s.membatch.epochMaps[path]) + buf := rlp.NewEncoderBuffer(nil) + epochMeta.Encode(buf) + rawdb.WriteEpochMetaPlainState(dbw, owner, string(inner), buf.ToBytes()) + } + } } for hash, value := range s.membatch.codes { rawdb.WriteCode(dbw, hash, value) @@ -509,10 +547,18 @@ func (s *Sync) commitNodeRequest(req *nodeRequest) error { // Write the node content to the membatch s.membatch.nodes[string(req.path)] = req.data s.membatch.hashes[string(req.path)] = req.hash + if req.epochMap != [16]types.StateEpoch{} { + s.membatch.epochMaps[string(req.path)] = req.epochMap + } // The size tracking refers to the db-batch, not the in-memory data. // Therefore, we ignore the req.Path, and account only for the hash+data // which eventually is written to db. s.membatch.size += common.HashLength + uint64(len(req.data)) + for _, epoch := range req.epochMap { + if epoch != 0 { + s.membatch.size += 16 + } + } delete(s.nodeReqs, string(req.path)) s.fetches[len(req.path)]-- diff --git a/trie/trie.go b/trie/trie.go index f6b747f467..33568d25ed 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -1566,3 +1566,11 @@ func (t *Trie) recursePruneExpiredNode(n node, path []byte, epoch types.StateEpo panic(fmt.Sprintf("invalid node type: %T", n)) } } + +func (t *Trie) UpdateRootEpoch(epoch types.StateEpoch) { + t.rootEpoch = epoch +} + +func (t *Trie) UpdateCurrentEpoch(epoch types.StateEpoch) { + t.currentEpoch = epoch +} diff --git a/trie/trie_reader.go b/trie/trie_reader.go index 924d379bbf..ee7a5dec28 100644 --- a/trie/trie_reader.go +++ b/trie/trie_reader.go @@ -51,11 +51,20 @@ type trieReader struct { // newTrieReader initializes the trie reader with the given node reader. func newTrieReader(stateRoot, owner common.Hash, db *Database) (*trieReader, error) { + var err error + if stateRoot == (common.Hash{}) || stateRoot == types.EmptyRootHash { if stateRoot == (common.Hash{}) { log.Error("Zero state root hash!") } - return &trieReader{owner: owner}, nil + tr := &trieReader{owner: owner} + if db.snapTree != nil { + tr.emdb, err = epochmeta.NewEpochMetaDatabase(db.snapTree, new(big.Int), stateRoot) + if err != nil { + return nil, err + } + } + return tr, nil } reader, err := db.Reader(stateRoot) if err != nil { From 0772f61a160676b55da5656377aabc7f26451c89 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Tue, 10 Oct 2023 20:25:28 +0800 Subject: [PATCH 45/65] pruner: fix some prune bugs; --- cmd/geth/chaincmd.go | 9 ++++++++- cmd/geth/config.go | 2 +- cmd/geth/consolecmd.go | 2 +- cmd/geth/dbcmd.go | 17 +++++++++++++++-- cmd/geth/snapshot.go | 10 ++++++++-- cmd/geth/verkle.go | 4 ++-- core/state/pruner/pruner.go | 22 +++++++++++++++++++++- core/types/state_expiry.go | 4 ++-- trie/epochmeta/database.go | 3 +++ trie/epochmeta/difflayer.go | 4 ++++ trie/trie.go | 2 +- trie/trie_reader.go | 1 + 12 files changed, 67 insertions(+), 13 deletions(-) diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 2d3c7d0d0d..59246d2c37 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -56,6 +56,7 @@ var ( Flags: flags.Merge([]cli.Flag{ utils.CachePreimagesFlag, utils.StateSchemeFlag, + utils.StateExpiryEnableFlag, }, utils.DatabasePathFlags), Description: ` The init command initializes a new genesis block and definition for the network. @@ -86,7 +87,8 @@ It expects the genesis file as argument.`, Name: "dumpgenesis", Usage: "Dumps genesis block JSON configuration to stdout", ArgsUsage: "", - Flags: append([]cli.Flag{utils.DataDirFlag}, utils.NetworkFlags...), + Flags: append([]cli.Flag{utils.DataDirFlag, + utils.StateExpiryEnableFlag}, utils.NetworkFlags...), Description: ` The dumpgenesis command prints the genesis configuration of the network preset if one is set. Otherwise it prints the genesis from the datadir.`, @@ -121,6 +123,7 @@ if one is set. Otherwise it prints the genesis from the datadir.`, utils.TransactionHistoryFlag, utils.StateSchemeFlag, utils.StateHistoryFlag, + utils.StateExpiryEnableFlag, }, utils.DatabasePathFlags), Description: ` The import command imports blocks from an RLP-encoded form. The form can be one file @@ -138,6 +141,7 @@ processing will proceed even if an individual RLP-file import failure occurs.`, utils.CacheFlag, utils.SyncModeFlag, utils.StateSchemeFlag, + utils.StateExpiryEnableFlag, }, utils.DatabasePathFlags), Description: ` Requires a first argument of the file to write to. @@ -154,6 +158,7 @@ be gzipped.`, Flags: flags.Merge([]cli.Flag{ utils.CacheFlag, utils.SyncModeFlag, + utils.StateExpiryEnableFlag, }, utils.DatabasePathFlags), Description: ` The import-preimages command imports hash preimages from an RLP encoded stream. @@ -168,6 +173,7 @@ It's deprecated, please use "geth db import" instead. Flags: flags.Merge([]cli.Flag{ utils.CacheFlag, utils.SyncModeFlag, + utils.StateExpiryEnableFlag, }, utils.DatabasePathFlags), Description: ` The export-preimages command exports hash preimages to an RLP encoded stream. @@ -188,6 +194,7 @@ It's deprecated, please use "geth db export" instead. utils.StartKeyFlag, utils.DumpLimitFlag, utils.StateSchemeFlag, + utils.StateExpiryEnableFlag, }, utils.DatabasePathFlags), Description: ` This command dumps out the state for a given block (or latest, if none provided). diff --git a/cmd/geth/config.go b/cmd/geth/config.go index b1744c8040..7c0381b88f 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -49,7 +49,7 @@ var ( Name: "dumpconfig", Usage: "Export configuration values in a TOML format", ArgsUsage: "", - Flags: flags.Merge(nodeFlags, rpcFlags), + Flags: flags.Merge(nodeFlags, rpcFlags, []cli.Flag{utils.StateExpiryEnableFlag}), Description: `Export configuration values in TOML format (to stdout by default).`, } diff --git a/cmd/geth/consolecmd.go b/cmd/geth/consolecmd.go index 526ede9619..e5d1e3503f 100644 --- a/cmd/geth/consolecmd.go +++ b/cmd/geth/consolecmd.go @@ -33,7 +33,7 @@ var ( Action: localConsole, Name: "console", Usage: "Start an interactive JavaScript environment", - Flags: flags.Merge(nodeFlags, rpcFlags, consoleFlags), + Flags: flags.Merge(nodeFlags, rpcFlags, consoleFlags, []cli.Flag{utils.StateExpiryEnableFlag}), Description: ` The Geth console is an interactive shell for the JavaScript runtime environment which exposes a node admin interface as well as the Ðapp JavaScript API. diff --git a/cmd/geth/dbcmd.go b/cmd/geth/dbcmd.go index 86e8154394..729ea56257 100644 --- a/cmd/geth/dbcmd.go +++ b/cmd/geth/dbcmd.go @@ -50,7 +50,7 @@ var ( Name: "removedb", Usage: "Remove blockchain and state databases", ArgsUsage: "", - Flags: utils.DatabasePathFlags, + Flags: flags.Merge(utils.DatabasePathFlags, []cli.Flag{utils.StateExpiryEnableFlag}), Description: ` Remove blockchain and state databases`, } @@ -86,6 +86,7 @@ Remove blockchain and state databases`, ArgsUsage: " ", Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, + utils.StateExpiryEnableFlag, }, utils.NetworkFlags, utils.DatabasePathFlags), Usage: "Inspect the storage size for each type of data in the database", Description: `This commands iterates the entire database. If the optional 'prefix' and 'start' arguments are provided, then the iteration is limited to the given subset of data.`, @@ -96,6 +97,7 @@ Remove blockchain and state databases`, ArgsUsage: " ", Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, + utils.StateExpiryEnableFlag, }, utils.DatabasePathFlags), Usage: "Inspect the MPT tree of the account and contract.", Description: `This commands iterates the entrie WorldState.`, @@ -104,7 +106,7 @@ Remove blockchain and state databases`, Action: checkStateContent, Name: "check-state-content", ArgsUsage: "", - Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags), + Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags, []cli.Flag{utils.StateExpiryEnableFlag}), Usage: "Verify that state data is cryptographically correct", Description: `This command iterates the entire database for 32-byte keys, looking for rlp-encoded trie nodes. For each trie node encountered, it checks that the key corresponds to the keccak256(value). If this is not true, this indicates @@ -155,6 +157,7 @@ a data corruption.`, Usage: "Print leveldb statistics", Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, + utils.StateExpiryEnableFlag, }, utils.NetworkFlags, utils.DatabasePathFlags), } dbCompactCmd = &cli.Command{ @@ -165,6 +168,7 @@ a data corruption.`, utils.SyncModeFlag, utils.CacheFlag, utils.CacheDatabaseFlag, + utils.StateExpiryEnableFlag, }, utils.NetworkFlags, utils.DatabasePathFlags), Description: `This command performs a database compaction. WARNING: This operation may take a very long time to finish, and may cause database @@ -177,6 +181,7 @@ corruption if it is aborted during execution'!`, ArgsUsage: "", Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, + utils.StateExpiryEnableFlag, }, utils.NetworkFlags, utils.DatabasePathFlags), Description: "This command looks up the specified database key from the database.", } @@ -187,6 +192,7 @@ corruption if it is aborted during execution'!`, ArgsUsage: "", Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, + utils.StateExpiryEnableFlag, }, utils.NetworkFlags, utils.DatabasePathFlags), Description: `This command deletes the specified database key from the database. WARNING: This is a low-level operation which may cause database corruption!`, @@ -198,6 +204,7 @@ WARNING: This is a low-level operation which may cause database corruption!`, ArgsUsage: " ", Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, + utils.StateExpiryEnableFlag, }, utils.NetworkFlags, utils.DatabasePathFlags), Description: `This command sets a given database key to the given value. WARNING: This is a low-level operation which may cause database corruption!`, @@ -210,6 +217,7 @@ WARNING: This is a low-level operation which may cause database corruption!`, Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, utils.StateSchemeFlag, + utils.StateExpiryEnableFlag, }, utils.NetworkFlags, utils.DatabasePathFlags), Description: "This command looks up the specified database key from the database.", } @@ -220,6 +228,7 @@ WARNING: This is a low-level operation which may cause database corruption!`, ArgsUsage: " ", Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, + utils.StateExpiryEnableFlag, }, utils.NetworkFlags, utils.DatabasePathFlags), Description: "This command displays information about the freezer index.", } @@ -230,6 +239,7 @@ WARNING: This is a low-level operation which may cause database corruption!`, ArgsUsage: " ", Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, + utils.StateExpiryEnableFlag, }, utils.NetworkFlags, utils.DatabasePathFlags), Description: "Exports the specified chain data to an RLP encoded stream, optionally gzip-compressed.", } @@ -249,6 +260,7 @@ WARNING: This is a low-level operation which may cause database corruption!`, Usage: "Shows metadata about the chain status.", Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, + utils.StateExpiryEnableFlag, }, utils.NetworkFlags, utils.DatabasePathFlags), Description: "Shows metadata about the chain status.", } @@ -257,6 +269,7 @@ WARNING: This is a low-level operation which may cause database corruption!`, Name: "inspect-reserved-oldest-blocks", Flags: []cli.Flag{ utils.DataDirFlag, + utils.StateExpiryEnableFlag, }, Usage: "Inspect the ancientStore information", Description: `This commands will read current offset from kvdb, which is the current offset and starting BlockNumber diff --git a/cmd/geth/snapshot.go b/cmd/geth/snapshot.go index 78d1e6a9e1..946173eaf6 100644 --- a/cmd/geth/snapshot.go +++ b/cmd/geth/snapshot.go @@ -87,6 +87,7 @@ WARNING: it's only supported in hash mode(--state.scheme=hash)". utils.BlockAmountReserved, utils.TriesInMemoryFlag, utils.CheckSnapshotWithMPT, + utils.StateExpiryEnableFlag, }, Description: ` geth offline prune-block for block data in ancientdb. @@ -107,6 +108,7 @@ so it's very necessary to do block data prune, this feature will handle it. Action: verifyState, Flags: flags.Merge([]cli.Flag{ utils.StateSchemeFlag, + utils.StateExpiryEnableFlag, }, utils.NetworkFlags, utils.DatabasePathFlags), Description: ` geth snapshot verify-state @@ -125,6 +127,7 @@ In other words, this command does the snapshot to trie conversion. Flags: []cli.Flag{ utils.DataDirFlag, utils.AncientFlag, + utils.StateExpiryEnableFlag, }, Description: ` will prune all historical trie state data except genesis block. @@ -143,7 +146,7 @@ the trie clean cache with default directory will be deleted. Usage: "Check that there is no 'dangling' snap storage", ArgsUsage: "", Action: checkDanglingStorage, - Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags), + Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags, []cli.Flag{utils.StateExpiryEnableFlag}), Description: ` geth snapshot check-dangling-storage traverses the snap storage data, and verifies that all snapshot storage data has a corresponding account. @@ -154,7 +157,7 @@ data, and verifies that all snapshot storage data has a corresponding account. Usage: "Check all snapshot layers for the a specific account", ArgsUsage: "
", Action: checkAccount, - Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags), + Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags, []cli.Flag{utils.StateExpiryEnableFlag}), Description: ` geth snapshot inspect-account
checks all snapshot layers and prints out information about the specified address. @@ -167,6 +170,7 @@ information about the specified address. Action: traverseState, Flags: flags.Merge([]cli.Flag{ utils.StateSchemeFlag, + utils.StateExpiryEnableFlag, }, utils.NetworkFlags, utils.DatabasePathFlags), Description: ` geth snapshot traverse-state @@ -184,6 +188,7 @@ It's also usable without snapshot enabled. Action: traverseRawState, Flags: flags.Merge([]cli.Flag{ utils.StateSchemeFlag, + utils.StateExpiryEnableFlag, }, utils.NetworkFlags, utils.DatabasePathFlags), Description: ` geth snapshot traverse-rawstate @@ -207,6 +212,7 @@ It's also usable without snapshot enabled. utils.DumpLimitFlag, utils.TriesInMemoryFlag, utils.StateSchemeFlag, + utils.StateExpiryEnableFlag, }, utils.NetworkFlags, utils.DatabasePathFlags), Description: ` This command is semantically equivalent to 'geth dump', but uses the snapshots diff --git a/cmd/geth/verkle.go b/cmd/geth/verkle.go index e3953ed5b7..97851b0f85 100644 --- a/cmd/geth/verkle.go +++ b/cmd/geth/verkle.go @@ -45,7 +45,7 @@ var ( Usage: "verify the conversion of a MPT into a verkle tree", ArgsUsage: "", Action: verifyVerkle, - Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags), + Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags, []cli.Flag{utils.StateExpiryEnableFlag}), Description: ` geth verkle verify This command takes a root commitment and attempts to rebuild the tree. @@ -56,7 +56,7 @@ This command takes a root commitment and attempts to rebuild the tree. Usage: "Dump a verkle tree to a DOT file", ArgsUsage: " [ ...]", Action: expandVerkle, - Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags), + Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags, []cli.Flag{utils.StateExpiryEnableFlag}), Description: ` geth verkle dump [ ...] This command will produce a dot file representing the tree, rooted at . diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go index 1d34025328..c7f3037b6b 100644 --- a/core/state/pruner/pruner.go +++ b/core/state/pruner/pruner.go @@ -767,7 +767,7 @@ func asyncScanExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch type logged = time.Now() } } - log.Info("Scan expired states", "trieNodes", trieCount.Load(), "elapsed", common.PrettyDuration(time.Since(start))) + log.Info("Scan unexpired states", "trieNodes", trieCount.Load(), "elapsed", common.PrettyDuration(time.Since(start))) close(pruneExpiredInDisk) return nil } @@ -842,6 +842,26 @@ func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, pruneExpiredInDisk ch log.Info("Pruned expired states", "trieNodes", trieCount, "trieSize", trieSize, "SnapKV", snapCount, "SnapKVSize", snapSize, "EpochMeta", epochMetaCount, "EpochMetaSize", epochMetaSize, "elapsed", common.PrettyDuration(time.Since(start))) + // Start compactions, will remove the deleted data from the disk immediately. + // Note for small pruning, the compaction is skipped. + if trieCount+snapCount+epochMetaCount >= rangeCompactionThreshold { + cstart := time.Now() + for b := 0x00; b <= 0xf0; b += 0x10 { + var ( + start = []byte{byte(b)} + end = []byte{byte(b + 0x10)} + ) + if b == 0xf0 { + end = nil + } + log.Info("Compacting database", "range", fmt.Sprintf("%#x-%#x", start, end), "elapsed", common.PrettyDuration(time.Since(cstart))) + if err := diskdb.Compact(start, end); err != nil { + log.Error("Database compaction failed", "error", err) + return err + } + } + log.Info("Database compaction finished", "elapsed", common.PrettyDuration(time.Since(cstart))) + } return nil } diff --git a/core/types/state_expiry.go b/core/types/state_expiry.go index 1f5856b9ff..3945e4c6f1 100644 --- a/core/types/state_expiry.go +++ b/core/types/state_expiry.go @@ -98,6 +98,6 @@ func (s *StateExpiryConfig) String() string { if !s.Enable { return "State Expiry Disable" } - return fmt.Sprintf("Enable State Expiry, RemoteEndpoint: %v, StateEpoch: [%v|%v|%v], StateScheme: %v, PruneLevel: %v", - s.FullStateEndpoint, s.StateEpoch1Block, s.StateEpoch2Block, s.StateEpochPeriod, s.StateScheme, s.PruneLevel) + return fmt.Sprintf("Enable State Expiry, RemoteEndpoint: %v, StateEpoch: [%v|%v|%v], StateScheme: %v, PruneLevel: %v, EnableLocalRevive: %v", + s.FullStateEndpoint, s.StateEpoch1Block, s.StateEpoch2Block, s.StateEpochPeriod, s.StateScheme, s.PruneLevel, s.EnableLocalRevive) } diff --git a/trie/epochmeta/database.go b/trie/epochmeta/database.go index 1fbd581f00..161eac61a0 100644 --- a/trie/epochmeta/database.go +++ b/trie/epochmeta/database.go @@ -45,6 +45,7 @@ func DecodeFullNodeEpochMeta(enc []byte) (*BranchNodeEpochMeta, error) { return &n, nil } +// TODO(0xbundler): modify it as reader type Storage interface { Get(addr common.Hash, path string) ([]byte, error) Delete(addr common.Hash, path string) error @@ -81,6 +82,7 @@ func NewEpochMetaDatabase(tree *SnapshotTree, number *big.Int, blockRoot common. func (s *StorageRW) Get(addr common.Hash, path string) ([]byte, error) { s.lock.RLock() defer s.lock.RUnlock() + // TODO(0xbundler): remove cache sub, exist := s.dirties[addr] if exist { if val, ok := sub[path]; ok { @@ -88,6 +90,7 @@ func (s *StorageRW) Get(addr common.Hash, path string) ([]byte, error) { } } + // TODO(0xbundler): metrics hit count return s.snap.EpochMeta(addr, path) } diff --git a/trie/epochmeta/difflayer.go b/trie/epochmeta/difflayer.go index f314c79c55..2ee34080de 100644 --- a/trie/epochmeta/difflayer.go +++ b/trie/epochmeta/difflayer.go @@ -324,6 +324,7 @@ func loadDiffLayers(db ethdb.KeyValueStore, diskLayer *diskLayer) (map[common.Ha return layers, children, nil } +// TODO(0xbundler): add bloom filter? type diffLayer struct { blockNumber *big.Int blockRoot common.Hash @@ -348,8 +349,10 @@ func (s *diffLayer) Root() common.Hash { } func (s *diffLayer) EpochMeta(addrHash common.Hash, path string) ([]byte, error) { + // TODO(0xbundler): remove lock? s.lock.RLock() defer s.lock.RUnlock() + // TODO(0xbundler): difflayer cache hit rate. cm, exist := s.nodeSet[addrHash] if exist { if ret, ok := cm[path]; ok { @@ -465,6 +468,7 @@ func (s *diskLayer) EpochMeta(addr common.Hash, path string) ([]byte, error) { s.lock.RLock() defer s.lock.RUnlock() + // TODO(0xbundler): disklayer cache hit rate. cacheKey := cacheKey(addr, path) cached, exist := s.cache.Get(cacheKey) if exist { diff --git a/trie/trie.go b/trie/trie.go index 33568d25ed..0398d569d4 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -1479,7 +1479,7 @@ func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, p var err error // Go through every child and recursively delete expired nodes for i, child := range n.Children { - err = t.findExpiredSubTree(child, append(path, byte(i)), epoch, pruner, stats) + err = t.findExpiredSubTree(child, append(path, byte(i)), n.GetChildEpoch(i), pruner, stats) if err != nil { return err } diff --git a/trie/trie_reader.go b/trie/trie_reader.go index ee7a5dec28..926221f507 100644 --- a/trie/trie_reader.go +++ b/trie/trie_reader.go @@ -134,6 +134,7 @@ func (r *trieReader) epochMeta(path []byte) (*epochmeta.BranchNodeEpochMeta, err } if len(blob) == 0 { // set default epoch map + // TODO(0xbundler): remove mem alloc? return epochmeta.NewBranchNodeEpochMeta([16]types.StateEpoch{}), nil } meta, err := epochmeta.DecodeFullNodeEpochMeta(blob) From 2032bfa63a03daebb6fc9beaaa31b2a4d556d39e Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Wed, 11 Oct 2023 16:22:23 +0800 Subject: [PATCH 46/65] trie/epochmeta: add bloom filter, opt lock; --- core/blockchain.go | 10 +- core/state/pruner/pruner.go | 127 +++++- core/state/snapshot/snapshot_value.go | 10 - trie/database.go | 3 +- trie/epochmeta/database.go | 114 +----- trie/epochmeta/database_test.go | 61 +-- trie/epochmeta/difflayer.go | 564 +++++--------------------- trie/epochmeta/difflayer_test.go | 119 +----- trie/epochmeta/disklayer.go | 151 +++++++ trie/epochmeta/snapshot.go | 345 ++++++++++++++++ trie/epochmeta/snapshot_test.go | 120 ++++++ trie/trie.go | 33 +- trie/trie_reader.go | 22 +- 13 files changed, 911 insertions(+), 768 deletions(-) create mode 100644 trie/epochmeta/disklayer.go create mode 100644 trie/epochmeta/snapshot.go create mode 100644 trie/epochmeta/snapshot_test.go diff --git a/core/blockchain.go b/core/blockchain.go index dab27b7827..c69a23b97b 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1256,9 +1256,9 @@ func (bc *BlockChain) Stop() { } } - epochMetaSnapTree := bc.triedb.EpochMetaSnapTree() - if epochMetaSnapTree != nil { - if err := epochMetaSnapTree.Journal(); err != nil { + epochMetaSnap := bc.triedb.EpochMetaSnapTree() + if epochMetaSnap != nil { + if err := epochMetaSnap.Journal(); err != nil { log.Error("Failed to journal epochMetaSnapTree", "err", err) } } @@ -1659,6 +1659,10 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. // Full but not archive node, do proper garbage collection triedb.Reference(block.Root(), common.Hash{}) // metadata reference to keep trie alive bc.triegc.Push(block.Root(), -int64(block.NumberU64())) + err := triedb.CommitEpochMeta(block.Root()) + if err != nil { + return err + } if current := block.NumberU64(); current > bc.triesInMemory { // If we exceeded our memory allowance, flush matured singleton nodes to disk diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go index c7f3037b6b..9c9637ee02 100644 --- a/core/state/pruner/pruner.go +++ b/core/state/pruner/pruner.go @@ -23,6 +23,7 @@ import ( "errors" "fmt" "github.com/ethereum/go-ethereum/params" + bloomfilter "github.com/holiman/bloomfilter/v2" "math" "math/big" "os" @@ -378,6 +379,7 @@ func (p *BlockPruner) backUpOldDb(name string, cache, handles int, namespace str log.Error("Failed to open ancient database", "err=", err) return err } + defer chainDb.Close() log.Info("chainDB opened successfully") @@ -670,7 +672,17 @@ func (p *Pruner) Prune(root common.Hash) error { return err } - if err = p.ExpiredPrune(p.chainHeader.Number, root); err != nil { + // find target header + header := p.chainHeader + for header != nil && header.Number.Uint64() >= 0 && header.Root != root { + header = rawdb.ReadHeader(p.db, header.ParentHash, header.Number.Uint64()-1) + } + if header == nil || header.Root != root { + return fmt.Errorf("cannot find target block root, chainHeader: %v:%v:%v, targetRoot: %v", + p.chainHeader.Number, p.chainHeader.Hash(), p.chainHeader.Root, root) + } + + if err = p.ExpiredPrune(header.Number, root); err != nil { return err } @@ -690,26 +702,39 @@ func (p *Pruner) ExpiredPrune(height *big.Int, root common.Hash) error { height = p.flattenBlock.Number root = p.flattenBlock.Root } - log.Info("start prune expired state", "height", height, "root", root, "scheme", p.config.CacheConfig.StateScheme) var ( - pruneExpiredTrieCh = make(chan *snapshot.ContractItem, 100000) + bloom *bloomfilter.Filter + epoch = types.GetStateEpoch(p.config.CacheConfig.StateExpiryCfg, height) + trieDB = trie.NewDatabase(p.db, p.config.CacheConfig.TriedbConfig()) + err error + ) + log.Info("start prune expired state", "height", height, "root", root, "scheme", p.config.CacheConfig.StateScheme, "epoch", epoch) + + // if using HBSS, must tag all unexpired trie prevent shared trie delete + if rawdb.HashScheme == p.config.CacheConfig.StateScheme { + bloom, err = p.unExpiredBloomTag(trieDB, epoch, root) + if err != nil { + return err + } + } + + var ( + scanExpiredTrieCh = make(chan *snapshot.ContractItem, 100000) pruneExpiredInDiskCh = make(chan *trie.NodeInfo, 100000) - epoch = types.GetStateEpoch(p.config.CacheConfig.StateExpiryCfg, height) rets = make([]error, 3) tasksWG sync.WaitGroup ) - trieDB := trie.NewDatabase(p.db, p.config.CacheConfig.TriedbConfig()) tasksWG.Add(2) go func() { defer tasksWG.Done() - rets[0] = asyncScanExpiredInTrie(trieDB, root, epoch, pruneExpiredTrieCh, pruneExpiredInDiskCh) + rets[0] = asyncScanExpiredInTrie(trieDB, root, epoch, scanExpiredTrieCh, pruneExpiredInDiskCh) }() go func() { defer tasksWG.Done() - rets[1] = asyncPruneExpiredStorageInDisk(p.db, pruneExpiredInDiskCh, p.stateBloom, p.config.CacheConfig.StateScheme) + rets[1] = asyncPruneExpiredStorageInDisk(p.db, pruneExpiredInDiskCh, bloom, p.config.CacheConfig.StateScheme) }() - rets[2] = snapshot.TraverseContractTrie(p.snaptree, root, pruneExpiredTrieCh) + rets[2] = snapshot.TraverseContractTrie(p.snaptree, root, scanExpiredTrieCh) // wait task done tasksWG.Wait() @@ -737,6 +762,84 @@ func (p *Pruner) ExpiredPrune(height *big.Int, root common.Hash) error { return nil } +func (p *Pruner) unExpiredBloomTag(trieDB *trie.Database, epoch types.StateEpoch, root common.Hash) (*bloomfilter.Filter, error) { + var ( + scanUnExpiredTrieCh = make(chan *snapshot.ContractItem, 100000) + tagUnExpiredInBloomCh = make(chan *trie.NodeInfo, 100000) + rets = make([]error, 3) + tasksWG sync.WaitGroup + ) + + bloom, err := bloomfilter.New(p.config.BloomSize*1024*1024*8, 4) + if err != nil { + return nil, err + } + + tasksWG.Add(2) + go func() { + defer tasksWG.Done() + rets[0] = asyncScanUnExpiredInTrie(trieDB, root, epoch, scanUnExpiredTrieCh, tagUnExpiredInBloomCh) + }() + go func() { + defer tasksWG.Done() + rets[1] = asyncTagUnExpiredInBloom(tagUnExpiredInBloomCh, bloom) + }() + rets[2] = snapshot.TraverseContractTrie(p.snaptree, root, scanUnExpiredTrieCh) + tasksWG.Wait() + + return bloom, nil +} + +func asyncTagUnExpiredInBloom(tagUnExpiredInBloomCh chan *trie.NodeInfo, bloom *bloomfilter.Filter) error { + var ( + trieCount = 0 + start = time.Now() + logged = time.Now() + ) + for info := range tagUnExpiredInBloomCh { + trieCount++ + bloom.Add(stateBloomHasher(info.Hash[:])) + if time.Since(logged) > 8*time.Second { + log.Info("Tag unexpired states in bloom", "trieNodes", trieCount) + logged = time.Now() + } + } + log.Info("Tag unexpired states in bloom", "trieNodes", trieCount, "elapsed", common.PrettyDuration(time.Since(start))) + return nil +} + +func asyncScanUnExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch types.StateEpoch, scanUnExpiredTrieCh chan *snapshot.ContractItem, tagUnExpiredInBloomCh chan *trie.NodeInfo) error { + var ( + trieCount atomic.Uint64 + start = time.Now() + logged = time.Now() + ) + for item := range scanUnExpiredTrieCh { + log.Info("start scan trie unexpired state", "addrHash", item.Addr, "root", item.Root) + tr, err := trie.New(&trie.ID{ + StateRoot: stateRoot, + Owner: item.Addr, + Root: item.Root, + }, db) + if err != nil { + log.Error("asyncScanUnExpiredInTrie, trie.New err", "id", item, "err", err) + return err + } + tr.SetEpoch(epoch) + if err = tr.ScanForPrune(tagUnExpiredInBloomCh, &trieCount, false); err != nil { + log.Error("asyncScanExpiredInTrie, ScanForPrune err", "id", item, "err", err) + return err + } + if time.Since(logged) > 8*time.Second { + log.Info("Pruning expired states", "trieNodes", trieCount.Load()) + logged = time.Now() + } + } + log.Info("Scan unexpired states", "trieNodes", trieCount.Load(), "elapsed", common.PrettyDuration(time.Since(start))) + close(tagUnExpiredInBloomCh) + return nil +} + // asyncScanExpiredInTrie prune trie expired state // here are some issues when just delete it from hash-based storage, because it's shared kv in hbss // but it's ok for pbss. @@ -758,8 +861,8 @@ func asyncScanExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch type return err } tr.SetEpoch(epoch) - if err = tr.PruneExpired(pruneExpiredInDisk, &trieCount); err != nil { - log.Error("asyncScanExpiredInTrie, PruneExpired err", "id", item, "err", err) + if err = tr.ScanForPrune(pruneExpiredInDisk, &trieCount, true); err != nil { + log.Error("asyncScanExpiredInTrie, ScanForPrune err", "id", item, "err", err) return err } if time.Since(logged) > 8*time.Second { @@ -772,7 +875,7 @@ func asyncScanExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch type return nil } -func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, pruneExpiredInDisk chan *trie.NodeInfo, bloom *stateBloom, scheme string) error { +func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, pruneExpiredInDisk chan *trie.NodeInfo, bloom *bloomfilter.Filter, scheme string) error { var ( trieCount = 0 epochMetaCount = 0 @@ -798,7 +901,7 @@ func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, pruneExpiredInDisk ch rawdb.DeleteTrieNode(batch, addr, info.Path, info.Hash, rawdb.PathScheme) case rawdb.HashScheme: // hbss has shared kv, so using bloom to filter them out. - if !bloom.Contain(info.Hash.Bytes()) { + if bloom == nil || !bloom.Contains(stateBloomHasher(info.Hash.Bytes())) { val := rawdb.ReadTrieNode(diskdb, addr, info.Path, info.Hash, rawdb.HashScheme) trieSize += common.StorageSize(len(val) + 33) rawdb.DeleteTrieNode(batch, addr, info.Path, info.Hash, rawdb.HashScheme) diff --git a/core/state/snapshot/snapshot_value.go b/core/state/snapshot/snapshot_value.go index 3a89bab148..e487889b70 100644 --- a/core/state/snapshot/snapshot_value.go +++ b/core/state/snapshot/snapshot_value.go @@ -108,16 +108,6 @@ func DecodeValueFromRLPBytes(b []byte) (SnapValue, error) { return decodeTypedVal(b) } -func GetValueTypeFromRLPBytes(b []byte) byte { - if len(b) == 0 { - return RawValueType - } - if b[0] > 0x7f { - return RawValueType - } - return b[0] -} - func decodeTypedVal(b []byte) (SnapValue, error) { switch b[0] { case ValueWithEpochType: diff --git a/trie/database.go b/trie/database.go index 15d0178539..f313b10e78 100644 --- a/trie/database.go +++ b/trie/database.go @@ -46,6 +46,7 @@ type Config struct { // state expiry feature EnableStateExpiry bool + EpochMeta *epochmeta.Config } // HashDefaults represents a config for using hash-based scheme with @@ -167,7 +168,7 @@ func NewDatabase(diskdb ethdb.Database, config *Config) *Database { db.backend = hashdb.New(diskdb, config.HashDB, mptResolver{}) } if config != nil && config.EnableStateExpiry { - snapTree, err := epochmeta.NewEpochMetaSnapTree(diskdb) + snapTree, err := epochmeta.NewEpochMetaSnapTree(diskdb, config.EpochMeta) if err != nil { panic(fmt.Sprintf("init SnapshotTree err: %v", err)) } diff --git a/trie/epochmeta/database.go b/trie/epochmeta/database.go index 161eac61a0..4921451dea 100644 --- a/trie/epochmeta/database.go +++ b/trie/epochmeta/database.go @@ -1,13 +1,10 @@ package epochmeta import ( - "bytes" - "errors" "fmt" - "math/big" - "sync" - "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/metrics" + "math/big" "github.com/ethereum/go-ethereum/rlp" @@ -19,6 +16,13 @@ const ( AccountMetadataPath = "m" ) +var ( + metaAccessMeter = metrics.NewRegisteredMeter("epochmeta/access", nil) + metaHitDiffMeter = metrics.NewRegisteredMeter("epochmeta/access/hit/diff", nil) + metaHitDiskCacheMeter = metrics.NewRegisteredMeter("epochmeta/access/hit/diskcache", nil) + metaHitDiskMeter = metrics.NewRegisteredMeter("epochmeta/access/hit/disk", nil) +) + type BranchNodeEpochMeta struct { EpochMap [16]types.StateEpoch } @@ -45,106 +49,28 @@ func DecodeFullNodeEpochMeta(enc []byte) (*BranchNodeEpochMeta, error) { return &n, nil } -// TODO(0xbundler): modify it as reader -type Storage interface { - Get(addr common.Hash, path string) ([]byte, error) - Delete(addr common.Hash, path string) error - Put(addr common.Hash, path string, val []byte) error - Commit(number *big.Int, blockRoot common.Hash) error -} - -type StorageRW struct { - snap snapshot - tree *SnapshotTree - dirties map[common.Hash]map[string][]byte - - stale bool - lock sync.RWMutex +type Reader struct { + snap snapshot + tree *SnapshotTree } -// NewEpochMetaDatabase first find snap by blockRoot, if got nil, try using number to instance a read only storage -func NewEpochMetaDatabase(tree *SnapshotTree, number *big.Int, blockRoot common.Hash) (Storage, error) { +// NewReader first find snap by blockRoot, if got nil, try using number to instance a read only storage +func NewReader(tree *SnapshotTree, number *big.Int, blockRoot common.Hash) (*Reader, error) { snap := tree.Snapshot(blockRoot) if snap == nil { // try using default snap if snap = tree.Snapshot(types.EmptyRootHash); snap == nil { return nil, fmt.Errorf("cannot find target epoch layer %#x", blockRoot) } - log.Debug("NewEpochMetaDatabase use default database", "number", number, "root", blockRoot) + log.Debug("NewReader use default database", "number", number, "root", blockRoot) } - return &StorageRW{ - snap: snap, - tree: tree, - dirties: make(map[common.Hash]map[string][]byte), + return &Reader{ + snap: snap, + tree: tree, }, nil } -func (s *StorageRW) Get(addr common.Hash, path string) ([]byte, error) { - s.lock.RLock() - defer s.lock.RUnlock() - // TODO(0xbundler): remove cache - sub, exist := s.dirties[addr] - if exist { - if val, ok := sub[path]; ok { - return val, nil - } - } - - // TODO(0xbundler): metrics hit count +func (s *Reader) Get(addr common.Hash, path string) ([]byte, error) { + metaAccessMeter.Mark(1) return s.snap.EpochMeta(addr, path) } - -func (s *StorageRW) Delete(addr common.Hash, path string) error { - s.lock.RLock() - defer s.lock.RUnlock() - if s.stale { - return errors.New("storage has staled") - } - _, ok := s.dirties[addr] - if !ok { - s.dirties[addr] = make(map[string][]byte) - } - - s.dirties[addr][path] = nil - return nil -} - -func (s *StorageRW) Put(addr common.Hash, path string, val []byte) error { - prev, err := s.Get(addr, path) - if err != nil { - return err - } - if bytes.Equal(prev, val) { - return nil - } - - s.lock.RLock() - defer s.lock.RUnlock() - if s.stale { - return errors.New("storage has staled") - } - - _, ok := s.dirties[addr] - if !ok { - s.dirties[addr] = make(map[string][]byte) - } - s.dirties[addr][path] = val - return nil -} - -// Commit if you commit to an unknown parent, like deeper than 128 layers, will get error -func (s *StorageRW) Commit(number *big.Int, blockRoot common.Hash) error { - s.lock.Lock() - defer s.lock.Unlock() - if s.stale { - return errors.New("storage has staled") - } - - s.stale = true - err := s.tree.Update(s.snap.Root(), number, blockRoot, s.dirties) - if err != nil { - return err - } - - return s.tree.Cap(blockRoot) -} diff --git a/trie/epochmeta/database_test.go b/trie/epochmeta/database_test.go index e7ba2d5867..2bb728c8ae 100644 --- a/trie/epochmeta/database_test.go +++ b/trie/epochmeta/database_test.go @@ -15,51 +15,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestEpochMetaRW_CRUD(t *testing.T) { - diskdb := memorydb.New() - tree, err := NewEpochMetaSnapTree(diskdb) - assert.NoError(t, err) - storageDB, err := NewEpochMetaDatabase(tree, common.Big1, blockRoot1) - assert.NoError(t, err) - - err = storageDB.Put(contract1, "hello", []byte("world")) - assert.NoError(t, err) - err = storageDB.Put(contract1, "hello", []byte("world")) - assert.NoError(t, err) - val, err := storageDB.Get(contract1, "hello") - assert.NoError(t, err) - assert.Equal(t, []byte("world"), val) - err = storageDB.Delete(contract1, "hello") - assert.NoError(t, err) - val, err = storageDB.Get(contract1, "hello") - assert.NoError(t, err) - assert.Equal(t, []byte(nil), val) -} - -func TestEpochMetaRO_Get(t *testing.T) { - diskdb := memorydb.New() - makeDiskLayer(diskdb, common.Big1, blockRoot1, contract1, []string{"k1", "v1"}) - - tree, err := NewEpochMetaSnapTree(diskdb) - assert.NoError(t, err) - storageRO, err := NewEpochMetaDatabase(tree, common.Big1, blockRoot1) - assert.NoError(t, err) - - err = storageRO.Put(contract1, "hello", []byte("world")) - assert.NoError(t, err) - err = storageRO.Delete(contract1, "hello") - assert.NoError(t, err) - err = storageRO.Commit(common.Big2, blockRoot2) - assert.NoError(t, err) - - val, err := storageRO.Get(contract1, "hello") - assert.NoError(t, err) - assert.Equal(t, []byte(nil), val) - val, err = storageRO.Get(contract1, "k1") - assert.NoError(t, err) - assert.Equal(t, []byte("v1"), val) -} - func makeDiskLayer(diskdb *memorydb.Database, number *big.Int, root common.Hash, addr common.Hash, kv []string) { if len(kv)%2 != 0 { panic("wrong kv") @@ -76,20 +31,12 @@ func makeDiskLayer(diskdb *memorydb.Database, number *big.Int, root common.Hash, } } -func TestEpochMetaRW_Commit(t *testing.T) { +func TestEpochMetaReader(t *testing.T) { diskdb := memorydb.New() - tree, err := NewEpochMetaSnapTree(diskdb) - assert.NoError(t, err) - storageDB, err := NewEpochMetaDatabase(tree, common.Big1, blockRoot1) - assert.NoError(t, err) - - err = storageDB.Put(contract1, "hello", []byte("world")) - assert.NoError(t, err) - - err = storageDB.Commit(common.Big1, blockRoot1) + makeDiskLayer(diskdb, common.Big1, blockRoot1, contract1, []string{"hello", "world"}) + tree, err := NewEpochMetaSnapTree(diskdb, nil) assert.NoError(t, err) - - storageDB, err = NewEpochMetaDatabase(tree, common.Big1, blockRoot1) + storageDB, err := NewReader(tree, common.Big1, blockRoot1) assert.NoError(t, err) val, err := storageDB.Get(contract1, "hello") assert.NoError(t, err) diff --git a/trie/epochmeta/difflayer.go b/trie/epochmeta/difflayer.go index 2ee34080de..1dc8f9dbe0 100644 --- a/trie/epochmeta/difflayer.go +++ b/trie/epochmeta/difflayer.go @@ -2,326 +2,97 @@ package epochmeta import ( "bytes" + "encoding/binary" "errors" - "fmt" - "github.com/ethereum/go-ethereum/core/types" - "io" - "math/big" - "sync" - - "github.com/ethereum/go-ethereum/log" - - lru "github.com/hashicorp/golang-lru" - "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" + bloomfilter "github.com/holiman/bloomfilter/v2" + "math" + "math/big" + "math/rand" + "sync" ) const ( // MaxEpochMetaDiffDepth default is 128 layers - MaxEpochMetaDiffDepth = 128 - journalVersion uint64 = 1 - defaultDiskLayerCacheSize = 100000 + MaxEpochMetaDiffDepth = 128 + journalVersion uint64 = 1 ) -// snapshot record diff layer and disk layer of shadow nodes, support mini reorg -type snapshot interface { - // Root block state root - Root() common.Hash - - // EpochMeta query shadow node from db, got RLP format - EpochMeta(addrHash common.Hash, path string) ([]byte, error) - - // Parent parent snap - Parent() snapshot - - // Update create a new diff layer from here - Update(blockNumber *big.Int, blockRoot common.Hash, nodeSet map[common.Hash]map[string][]byte) (snapshot, error) - - // Journal commit self as a journal to buffer - Journal(buffer *bytes.Buffer) (common.Hash, error) -} - -// SnapshotTree maintain all diff layers support reorg, will flush to db when MaxEpochMetaDiffDepth reach -// every layer response to a block state change set, there no flatten layers operation. -type SnapshotTree struct { - diskdb ethdb.KeyValueStore - - // diffLayers + diskLayer, disk layer, always not nil - layers map[common.Hash]snapshot - children map[common.Hash][]common.Hash - - lock sync.RWMutex -} - -func NewEpochMetaSnapTree(diskdb ethdb.KeyValueStore) (*SnapshotTree, error) { - diskLayer, err := loadDiskLayer(diskdb) - if err != nil { - return nil, err - } - layers, children, err := loadDiffLayers(diskdb, diskLayer) - if err != nil { - return nil, err - } - - layers[diskLayer.blockRoot] = diskLayer - // check if continuously after disk layer - if len(layers) > 1 && len(children[diskLayer.blockRoot]) == 0 { - return nil, errors.New("cannot found any diff layers link to disk layer") - } - return &SnapshotTree{ - diskdb: diskdb, - layers: layers, - children: children, - }, nil -} - -// Cap keep tree depth not greater MaxEpochMetaDiffDepth, all forks parent to disk layer will delete -func (s *SnapshotTree) Cap(blockRoot common.Hash) error { - snap := s.Snapshot(blockRoot) - if snap == nil { - return fmt.Errorf("epoch meta snapshot missing: [%#x]", blockRoot) - } - nextDiff, ok := snap.(*diffLayer) - if !ok { - return nil - } - for i := 0; i < MaxEpochMetaDiffDepth-1; i++ { - nextDiff, ok = nextDiff.Parent().(*diffLayer) - // if depth less MaxEpochMetaDiffDepth, just return - if !ok { - return nil - } - } - - flatten := make([]snapshot, 0) - parent := nextDiff.Parent() - for parent != nil { - flatten = append(flatten, parent) - parent = parent.Parent() - } - if len(flatten) <= 1 { - return nil - } - - last, ok := flatten[len(flatten)-1].(*diskLayer) - if !ok { - return errors.New("the diff layers not link to disk layer") - } - - s.lock.Lock() - defer s.lock.Unlock() - newDiskLayer, err := s.flattenDiffs2Disk(flatten[:len(flatten)-1], last) - if err != nil { - return err - } - - // clear forks, but keep latest disk forks - for i := len(flatten) - 1; i > 0; i-- { - var childRoot common.Hash - if i > 0 { - childRoot = flatten[i-1].Root() - } else { - childRoot = nextDiff.Root() - } - root := flatten[i].Root() - s.removeSubLayers(s.children[root], &childRoot) - delete(s.layers, root) - delete(s.children, root) - } - - // reset newDiskLayer and children's parent - s.layers[newDiskLayer.Root()] = newDiskLayer - for _, child := range s.children[newDiskLayer.Root()] { - if diff, exist := s.layers[child].(*diffLayer); exist { - diff.setParent(newDiskLayer) - } - } - return nil -} - -func (s *SnapshotTree) Update(parentRoot common.Hash, blockNumber *big.Int, blockRoot common.Hash, nodeSet map[common.Hash]map[string][]byte) error { - // if there are no changes, just skip - if blockRoot == parentRoot { - return nil - } - - // Generate a new snapshot on top of the parent - parent := s.Snapshot(parentRoot) - if parent == nil { - // just point to fake disk layers - parent = s.Snapshot(types.EmptyRootHash) - if parent == nil { - return errors.New("cannot find any suitable parent") - } - parentRoot = parent.Root() - } - snap, err := parent.Update(blockNumber, blockRoot, nodeSet) - if err != nil { - return err - } - - s.lock.Lock() - defer s.lock.Unlock() - - s.layers[blockRoot] = snap - s.children[parentRoot] = append(s.children[parentRoot], blockRoot) - return nil -} - -func (s *SnapshotTree) Snapshot(blockRoot common.Hash) snapshot { - s.lock.RLock() - defer s.lock.RUnlock() - return s.layers[blockRoot] -} - -func (s *SnapshotTree) DB() ethdb.KeyValueStore { - s.lock.RLock() - defer s.lock.RUnlock() - return s.diskdb -} - -func (s *SnapshotTree) Journal() error { - s.lock.Lock() - defer s.lock.Unlock() - - // Firstly write out the metadata of journal - journal := new(bytes.Buffer) - if err := rlp.Encode(journal, journalVersion); err != nil { - return err - } - for _, snap := range s.layers { - if _, err := snap.Journal(journal); err != nil { - return err - } - } - rawdb.WriteEpochMetaSnapshotJournal(s.diskdb, journal.Bytes()) - return nil -} +var ( + // aggregatorMemoryLimit is the maximum size of the bottom-most diff layer + // that aggregates the writes from above until it's flushed into the disk + // layer. + // + // Note, bumping this up might drastically increase the size of the bloom + // filters that's stored in every diff layer. Don't do that without fully + // understanding all the implications. + aggregatorMemoryLimit = uint64(4 * 1024 * 1024) + + // aggregatorItemLimit is an approximate number of items that will end up + // in the agregator layer before it's flushed out to disk. A plain account + // weighs around 14B (+hash), a storage slot 32B (+hash), a deleted slot + // 0B (+hash). Slots are mostly set/unset in lockstep, so that average at + // 16B (+hash). All in all, the average entry seems to be 15+32=47B. Use a + // smaller number to be on the safe side. + aggregatorItemLimit = aggregatorMemoryLimit / 42 + + // bloomTargetError is the target false positive rate when the aggregator + // layer is at its fullest. The actual value will probably move around up + // and down from this number, it's mostly a ballpark figure. + // + // Note, dropping this down might drastically increase the size of the bloom + // filters that's stored in every diff layer. Don't do that without fully + // understanding all the implications. + bloomTargetError = 0.02 + + // bloomSize is the ideal bloom filter size given the maximum number of items + // it's expected to hold and the target false positive error rate. + bloomSize = math.Ceil(float64(aggregatorItemLimit) * math.Log(bloomTargetError) / math.Log(1/math.Pow(2, math.Log(2)))) + + // bloomFuncs is the ideal number of bits a single entry should set in the + // bloom filter to keep its size to a minimum (given it's size and maximum + // entry count). + bloomFuncs = math.Round((bloomSize / float64(aggregatorItemLimit)) * math.Log(2)) + // the bloom offsets are runtime constants which determines which part of the + // account/storage hash the hasher functions looks at, to determine the + // bloom key for an account/slot. This is randomized at init(), so that the + // global population of nodes do not all display the exact same behaviour with + // regards to bloom content + bloomStorageHasherOffset = 0 +) -func (s *SnapshotTree) removeSubLayers(layers []common.Hash, skip *common.Hash) { - for _, layer := range layers { - if skip != nil && layer == *skip { - continue - } - s.removeSubLayers(s.children[layer], nil) - delete(s.layers, layer) - delete(s.children, layer) - } +func init() { + // Init the bloom offsets in the range [0:24] (requires 8 bytes) + bloomStorageHasherOffset = rand.Intn(25) } -// flattenDiffs2Disk delete all flatten and push them to db -func (s *SnapshotTree) flattenDiffs2Disk(flatten []snapshot, diskLayer *diskLayer) (*diskLayer, error) { - var err error - for i := len(flatten) - 1; i >= 0; i-- { - diskLayer, err = diskLayer.PushDiff(flatten[i].(*diffLayer)) - if err != nil { - return nil, err - } - } - - return diskLayer, nil +// storageBloomHasher is a wrapper around a [2]common.Hash to satisfy the interface +// API requirements of the bloom library used. It's used to convert an account +// hash into a 64 bit mini hash. +type storageBloomHasher struct { + accountHash common.Hash + path string } -// loadDiskLayer load from db, could be nil when none in db -func loadDiskLayer(db ethdb.KeyValueStore) (*diskLayer, error) { - val := rawdb.ReadEpochMetaPlainStateMeta(db) - // if there is no disk layer, will construct a fake disk layer - if len(val) == 0 { - diskLayer, err := newEpochMetaDiskLayer(db, common.Big0, types.EmptyRootHash) - if err != nil { - return nil, err - } - return diskLayer, nil +func (h storageBloomHasher) Write(p []byte) (n int, err error) { panic("not implemented") } +func (h storageBloomHasher) Sum(b []byte) []byte { panic("not implemented") } +func (h storageBloomHasher) Reset() { panic("not implemented") } +func (h storageBloomHasher) BlockSize() int { panic("not implemented") } +func (h storageBloomHasher) Size() int { return 8 } +func (h storageBloomHasher) Sum64() uint64 { + if len(h.path) < 8 { + path := [8]byte{} + copy(path[:], h.path) + return binary.BigEndian.Uint64(h.accountHash[bloomStorageHasherOffset:bloomStorageHasherOffset+8]) ^ + binary.BigEndian.Uint64(path[:]) } - var meta epochMetaPlainMeta - if err := rlp.DecodeBytes(val, &meta); err != nil { - return nil, err + if len(h.path) < bloomStorageHasherOffset+8 { + return binary.BigEndian.Uint64(h.accountHash[bloomStorageHasherOffset:bloomStorageHasherOffset+8]) ^ + binary.BigEndian.Uint64([]byte(h.path[len(h.path)-8:])) } - - layer, err := newEpochMetaDiskLayer(db, meta.BlockNumber, meta.BlockRoot) - if err != nil { - return nil, err - } - return layer, nil -} - -func loadDiffLayers(db ethdb.KeyValueStore, diskLayer *diskLayer) (map[common.Hash]snapshot, map[common.Hash][]common.Hash, error) { - layers := make(map[common.Hash]snapshot) - children := make(map[common.Hash][]common.Hash) - - journal := rawdb.ReadEpochMetaSnapshotJournal(db) - if len(journal) == 0 { - return layers, children, nil - } - r := rlp.NewStream(bytes.NewReader(journal), 0) - // Firstly, resolve the first element as the journal version - version, err := r.Uint64() - if err != nil { - return nil, nil, errors.New("failed to resolve journal version") - } - if version != journalVersion { - return nil, nil, errors.New("wrong journal version") - } - - parents := make(map[common.Hash]common.Hash) - for { - var ( - parent common.Hash - number big.Int - root common.Hash - js []journalEpochMeta - ) - // Read the next diff journal entry - if err := r.Decode(&number); err != nil { - // The first read may fail with EOF, marking the end of the journal - if errors.Is(err, io.EOF) { - break - } - return nil, nil, fmt.Errorf("load diff number: %v", err) - } - if err := r.Decode(&parent); err != nil { - return nil, nil, fmt.Errorf("load diff parent: %v", err) - } - // Read the next diff journal entry - if err := r.Decode(&root); err != nil { - return nil, nil, fmt.Errorf("load diff root: %v", err) - } - if err := r.Decode(&js); err != nil { - return nil, nil, fmt.Errorf("load diff storage: %v", err) - } - - nodeSet := make(map[common.Hash]map[string][]byte) - for _, entry := range js { - nodes := make(map[string][]byte) - for i, key := range entry.Keys { - if len(entry.Vals[i]) > 0 { // RLP loses nil-ness, but `[]byte{}` is not a valid item, so reinterpret that - nodes[key] = entry.Vals[i] - } else { - nodes[key] = nil - } - } - nodeSet[entry.Hash] = nodes - } - - parents[root] = parent - layers[root] = newEpochMetaDiffLayer(&number, root, nil, nodeSet) - } - - for t, s := range layers { - parent := parents[t] - children[parent] = append(children[parent], t) - if p, ok := layers[parent]; ok { - s.(*diffLayer).parent = p - } else if diskLayer != nil && parent == diskLayer.Root() { - s.(*diffLayer).parent = diskLayer - } else { - return nil, nil, errors.New("cannot find it's parent") - } - } - return layers, children, nil + return binary.BigEndian.Uint64(h.accountHash[bloomStorageHasherOffset:bloomStorageHasherOffset+8]) ^ + binary.BigEndian.Uint64([]byte(h.path[bloomStorageHasherOffset:bloomStorageHasherOffset+8])) } // TODO(0xbundler): add bloom filter? @@ -329,37 +100,61 @@ type diffLayer struct { blockNumber *big.Int blockRoot common.Hash parent snapshot + origin *diskLayer nodeSet map[common.Hash]map[string][]byte - lock sync.RWMutex + diffed *bloomfilter.Filter // Bloom filter tracking all the diffed items up to the disk layer + lock sync.RWMutex // lock only protect parent filed change now. } func newEpochMetaDiffLayer(blockNumber *big.Int, blockRoot common.Hash, parent snapshot, nodeSet map[common.Hash]map[string][]byte) *diffLayer { - return &diffLayer{ + dl := &diffLayer{ blockNumber: blockNumber, blockRoot: blockRoot, parent: parent, nodeSet: nodeSet, } + + switch p := parent.(type) { + case *diffLayer: + dl.origin = p.origin + dl.diffed, _ = p.diffed.Copy() + case *diskLayer: + dl.origin = p + dl.diffed, _ = bloomfilter.New(uint64(bloomSize), uint64(bloomFuncs)) + default: + panic("newEpochMetaDiffLayer got wrong snapshot type") + } + + // Iterate over all the accounts and storage metas and index them + for accountHash, metas := range dl.nodeSet { + for path := range metas { + dl.diffed.Add(storageBloomHasher{accountHash, path}) + } + } + return dl } func (s *diffLayer) Root() common.Hash { - s.lock.RLock() - defer s.lock.RUnlock() return s.blockRoot } +// EpochMeta find target epoch meta from diff layer or disk layer func (s *diffLayer) EpochMeta(addrHash common.Hash, path string) ([]byte, error) { - // TODO(0xbundler): remove lock? - s.lock.RLock() - defer s.lock.RUnlock() - // TODO(0xbundler): difflayer cache hit rate. + // if the diff chain not contain the meta or staled, try get from disk layer + if !s.diffed.Contains(storageBloomHasher{addrHash, path}) { + return s.origin.EpochMeta(addrHash, path) + } + cm, exist := s.nodeSet[addrHash] if exist { if ret, ok := cm[path]; ok { + metaHitDiffMeter.Mark(1) return ret, nil } } + s.lock.RLock() + defer s.lock.RUnlock() return s.parent.EpochMeta(addrHash, path) } @@ -371,18 +166,15 @@ func (s *diffLayer) Parent() snapshot { // Update append new diff layer onto current, nodeChgRecord when val is []byte{}, it delete the kv func (s *diffLayer) Update(blockNumber *big.Int, blockRoot common.Hash, nodeSet map[common.Hash]map[string][]byte) (snapshot, error) { - s.lock.RLock() if s.blockNumber.Int64() != 0 && s.blockNumber.Cmp(blockNumber) >= 0 { return nil, errors.New("update a unordered diff layer in diff layer") } - s.lock.RUnlock() return newEpochMetaDiffLayer(blockNumber, blockRoot, s, nodeSet), nil } func (s *diffLayer) Journal(buffer *bytes.Buffer) (common.Hash, error) { s.lock.RLock() defer s.lock.RUnlock() - if err := rlp.Encode(buffer, s.blockNumber); err != nil { return common.Hash{}, err } @@ -414,16 +206,14 @@ func (s *diffLayer) Journal(buffer *bytes.Buffer) (common.Hash, error) { return s.blockRoot, nil } -func (s *diffLayer) setParent(parent snapshot) { - s.lock.Lock() - defer s.lock.Unlock() - s.parent = parent +func (s *diffLayer) getNodeSet() map[common.Hash]map[string][]byte { + return s.nodeSet } -func (s *diffLayer) getNodeSet() map[common.Hash]map[string][]byte { +func (s *diffLayer) resetParent(parent snapshot) { s.lock.Lock() defer s.lock.Unlock() - return s.nodeSet + s.parent = parent } type journalEpochMeta struct { @@ -436,135 +226,3 @@ type epochMetaPlainMeta struct { BlockNumber *big.Int BlockRoot common.Hash } - -type diskLayer struct { - diskdb ethdb.KeyValueStore - blockNumber *big.Int - blockRoot common.Hash - cache *lru.Cache - lock sync.RWMutex -} - -func newEpochMetaDiskLayer(diskdb ethdb.KeyValueStore, blockNumber *big.Int, blockRoot common.Hash) (*diskLayer, error) { - cache, err := lru.New(defaultDiskLayerCacheSize) - if err != nil { - return nil, err - } - return &diskLayer{ - diskdb: diskdb, - blockNumber: blockNumber, - blockRoot: blockRoot, - cache: cache, - }, nil -} - -func (s *diskLayer) Root() common.Hash { - s.lock.RLock() - defer s.lock.RUnlock() - return s.blockRoot -} - -func (s *diskLayer) EpochMeta(addr common.Hash, path string) ([]byte, error) { - s.lock.RLock() - defer s.lock.RUnlock() - - // TODO(0xbundler): disklayer cache hit rate. - cacheKey := cacheKey(addr, path) - cached, exist := s.cache.Get(cacheKey) - if exist { - return cached.([]byte), nil - } - - val := rawdb.ReadEpochMetaPlainState(s.diskdb, addr, path) - s.cache.Add(cacheKey, val) - return val, nil -} - -func (s *diskLayer) Parent() snapshot { - return nil -} - -func (s *diskLayer) Update(blockNumber *big.Int, blockRoot common.Hash, nodeSet map[common.Hash]map[string][]byte) (snapshot, error) { - s.lock.RLock() - if s.blockNumber.Int64() != 0 && s.blockNumber.Cmp(blockNumber) >= 0 { - return nil, errors.New("update a unordered diff layer in disk layer") - } - s.lock.RUnlock() - return newEpochMetaDiffLayer(blockNumber, blockRoot, s, nodeSet), nil -} - -func (s *diskLayer) Journal(buffer *bytes.Buffer) (common.Hash, error) { - return common.Hash{}, nil -} - -func (s *diskLayer) PushDiff(diff *diffLayer) (*diskLayer, error) { - s.lock.Lock() - defer s.lock.Unlock() - - number := diff.blockNumber - if s.blockNumber.Cmp(number) >= 0 { - return nil, errors.New("push a lower block to disk") - } - batch := s.diskdb.NewBatch() - nodeSet := diff.getNodeSet() - if err := s.writeHistory(number, batch, diff.getNodeSet()); err != nil { - return nil, err - } - - // update meta - meta := epochMetaPlainMeta{ - BlockNumber: number, - BlockRoot: diff.blockRoot, - } - enc, err := rlp.EncodeToBytes(meta) - if err != nil { - return nil, err - } - if err = rawdb.WriteEpochMetaPlainStateMeta(batch, enc); err != nil { - return nil, err - } - - if err = batch.Write(); err != nil { - return nil, err - } - diskLayer := &diskLayer{ - diskdb: s.diskdb, - blockNumber: number, - blockRoot: diff.blockRoot, - cache: s.cache, - } - - // reuse cache - for addr, nodes := range nodeSet { - for path, val := range nodes { - diskLayer.cache.Add(cacheKey(addr, path), val) - } - } - return diskLayer, nil -} - -func (s *diskLayer) writeHistory(number *big.Int, batch ethdb.Batch, nodeSet map[common.Hash]map[string][]byte) error { - for addr, subSet := range nodeSet { - for path, val := range subSet { - // refresh plain state - if len(val) == 0 { - if err := rawdb.DeleteEpochMetaPlainState(batch, addr, path); err != nil { - return err - } - } else { - if err := rawdb.WriteEpochMetaPlainState(batch, addr, path, val); err != nil { - return err - } - } - } - } - log.Debug("shadow node history pruned, only keep plainState", "number", number, "count", len(nodeSet)) - return nil -} - -func cacheKey(addr common.Hash, path string) string { - key := make([]byte, len(addr)+len(path)) - copy(key[:], addr.Bytes()) - copy(key[len(addr):], path) - return string(key) -} diff --git a/trie/epochmeta/difflayer_test.go b/trie/epochmeta/difflayer_test.go index 6a0f61fc9f..54cea5f3c3 100644 --- a/trie/epochmeta/difflayer_test.go +++ b/trie/epochmeta/difflayer_test.go @@ -2,8 +2,6 @@ package epochmeta import ( "github.com/ethereum/go-ethereum/core/types" - "math/big" - "strconv" "testing" "github.com/ethereum/go-ethereum/common" @@ -30,7 +28,7 @@ var ( func TestEpochMetaDiffLayer_whenGenesis(t *testing.T) { diskdb := memorydb.New() // create empty tree - tree, err := NewEpochMetaSnapTree(diskdb) + tree, err := NewEpochMetaSnapTree(diskdb, nil) assert.NoError(t, err) snap := tree.Snapshot(blockRoot0) assert.Nil(t, snap) @@ -46,7 +44,7 @@ func TestEpochMetaDiffLayer_whenGenesis(t *testing.T) { assert.NoError(t, err) // reload - tree, err = NewEpochMetaSnapTree(diskdb) + tree, err = NewEpochMetaSnapTree(diskdb, nil) assert.NoError(t, err) diskLayer := tree.Snapshot(types.EmptyRootHash) assert.NotNil(t, diskLayer) @@ -74,7 +72,7 @@ func TestEpochMetaDiffLayer_whenGenesis(t *testing.T) { func TestEpochMetaDiffLayer_crud(t *testing.T) { diskdb := memorydb.New() // create empty tree - tree, err := NewEpochMetaSnapTree(diskdb) + tree, err := NewEpochMetaSnapTree(diskdb, nil) assert.NoError(t, err) set1 := makeNodeSet(contract1, []string{"hello", "world", "h1", "w1"}) appendNodeSet(set1, contract3, []string{"h3", "w3"}) @@ -115,117 +113,6 @@ func TestEpochMetaDiffLayer_crud(t *testing.T) { assert.Equal(t, []byte("w3"), val) } -func TestEpochMetaDiffLayer_capDiffLayers(t *testing.T) { - diskdb := memorydb.New() - // create empty tree - tree, err := NewEpochMetaSnapTree(diskdb) - assert.NoError(t, err) - - // push 200 diff layers - count := 1 - for i := 0; i < 200; i++ { - ns := strconv.Itoa(count) - root := makeHash("b" + ns) - parent := makeHash("b" + strconv.Itoa(count-1)) - number := new(big.Int).SetUint64(uint64(count)) - err = tree.Update(parent, number, - root, makeNodeSet(contract1, []string{"hello" + ns, "world" + ns})) - assert.NoError(t, err) - - // add 10 forks - for j := 0; j < 10; j++ { - fs := strconv.Itoa(j) - err = tree.Update(parent, number, - makeHash("b"+ns+"f"+fs), makeNodeSet(contract1, []string{"hello" + ns + "f" + fs, "world" + ns + "f" + fs})) - assert.NoError(t, err) - } - - err = tree.Cap(root) - assert.NoError(t, err) - count++ - } - assert.Equal(t, 1409, len(tree.layers)) - - // push 100 diff layers, and cap - for i := 0; i < 100; i++ { - ns := strconv.Itoa(count) - parent := makeHash("b" + strconv.Itoa(count-1)) - root := makeHash("b" + ns) - number := new(big.Int).SetUint64(uint64(count)) - err = tree.Update(parent, number, root, - makeNodeSet(contract1, []string{"hello" + ns, "world" + ns})) - assert.NoError(t, err) - - // add 20 forks - for j := 0; j < 10; j++ { - fs := strconv.Itoa(j) - err = tree.Update(parent, number, - makeHash("b"+ns+"f"+fs), makeNodeSet(contract1, []string{"hello" + ns + "f" + fs, "world" + ns + "f" + fs})) - assert.NoError(t, err) - } - for j := 0; j < 10; j++ { - fs := strconv.Itoa(j) - err = tree.Update(makeHash("b"+strconv.Itoa(count-1)+"f"+fs), number, - makeHash("b"+ns+"f"+fs), makeNodeSet(contract1, []string{"hello" + ns + "f" + fs, "world" + ns + "f" + fs})) - assert.NoError(t, err) - } - count++ - } - lastRoot := makeHash("b" + strconv.Itoa(count-1)) - err = tree.Cap(lastRoot) - assert.NoError(t, err) - assert.Equal(t, 1409, len(tree.layers)) - - // push 100 diff layers, and cap - for i := 0; i < 129; i++ { - ns := strconv.Itoa(count) - parent := makeHash("b" + strconv.Itoa(count-1)) - root := makeHash("b" + ns) - number := new(big.Int).SetUint64(uint64(count)) - err = tree.Update(parent, number, root, - makeNodeSet(contract1, []string{"hello" + ns, "world" + ns})) - assert.NoError(t, err) - - count++ - } - lastRoot = makeHash("b" + strconv.Itoa(count-1)) - err = tree.Cap(lastRoot) - assert.NoError(t, err) - - assert.Equal(t, 129, len(tree.layers)) - assert.Equal(t, 128, len(tree.children)) - for parent, children := range tree.children { - if tree.layers[parent] == nil { - t.Log(tree.layers[parent]) - } - assert.NotNil(t, tree.layers[parent]) - for _, child := range children { - if tree.layers[child] == nil { - t.Log(tree.layers[child]) - } - assert.NotNil(t, tree.layers[child]) - } - } - - snap := tree.Snapshot(lastRoot) - assert.NotNil(t, snap) - for i := 1; i < count; i++ { - ns := strconv.Itoa(i) - n, err := snap.EpochMeta(contract1, "hello"+ns) - assert.NoError(t, err) - assert.Equal(t, []byte("world"+ns), n) - } - - // store - err = tree.Journal() - assert.NoError(t, err) - - tree, err = NewEpochMetaSnapTree(diskdb) - assert.NoError(t, err) - assert.Equal(t, 129, len(tree.layers)) - assert.Equal(t, 128, len(tree.children)) -} - func makeHash(s string) common.Hash { var ret common.Hash if len(s) >= 32 { diff --git a/trie/epochmeta/disklayer.go b/trie/epochmeta/disklayer.go new file mode 100644 index 0000000000..ac82dc441f --- /dev/null +++ b/trie/epochmeta/disklayer.go @@ -0,0 +1,151 @@ +package epochmeta + +import ( + "bytes" + "errors" + "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" + lru "github.com/hashicorp/golang-lru" + "math/big" + "sync" +) + +const ( + defaultDiskLayerCacheSize = 100000 +) + +type diskLayer struct { + diskdb ethdb.KeyValueStore + blockNumber *big.Int + blockRoot common.Hash + cache *lru.Cache + lock sync.RWMutex +} + +func newEpochMetaDiskLayer(diskdb ethdb.KeyValueStore, blockNumber *big.Int, blockRoot common.Hash) (*diskLayer, error) { + cache, err := lru.New(defaultDiskLayerCacheSize) + if err != nil { + return nil, err + } + return &diskLayer{ + diskdb: diskdb, + blockNumber: blockNumber, + blockRoot: blockRoot, + cache: cache, + }, nil +} + +func (s *diskLayer) Root() common.Hash { + s.lock.RLock() + defer s.lock.RUnlock() + return s.blockRoot +} + +func (s *diskLayer) EpochMeta(addr common.Hash, path string) ([]byte, error) { + s.lock.RLock() + defer s.lock.RUnlock() + + key := cacheKey(addr, path) + cached, exist := s.cache.Get(key) + if exist { + metaHitDiskCacheMeter.Mark(1) + return cached.([]byte), nil + } + + metaHitDiskMeter.Mark(1) + val := rawdb.ReadEpochMetaPlainState(s.diskdb, addr, path) + s.cache.Add(key, val) + return val, nil +} + +func (s *diskLayer) Parent() snapshot { + return nil +} + +func (s *diskLayer) Update(blockNumber *big.Int, blockRoot common.Hash, nodeSet map[common.Hash]map[string][]byte) (snapshot, error) { + s.lock.RLock() + if s.blockNumber.Int64() != 0 && s.blockNumber.Cmp(blockNumber) >= 0 { + return nil, errors.New("update a unordered diff layer in disk layer") + } + s.lock.RUnlock() + return newEpochMetaDiffLayer(blockNumber, blockRoot, s, nodeSet), nil +} + +func (s *diskLayer) Journal(buffer *bytes.Buffer) (common.Hash, error) { + return common.Hash{}, nil +} + +func (s *diskLayer) PushDiff(diff *diffLayer) (*diskLayer, error) { + s.lock.Lock() + defer s.lock.Unlock() + + number := diff.blockNumber + if s.blockNumber.Cmp(number) >= 0 { + return nil, errors.New("push a lower block to disk") + } + batch := s.diskdb.NewBatch() + nodeSet := diff.getNodeSet() + if err := s.writeHistory(number, batch, nodeSet); err != nil { + return nil, err + } + + // update meta + meta := epochMetaPlainMeta{ + BlockNumber: number, + BlockRoot: diff.blockRoot, + } + enc, err := rlp.EncodeToBytes(meta) + if err != nil { + return nil, err + } + if err = rawdb.WriteEpochMetaPlainStateMeta(batch, enc); err != nil { + return nil, err + } + + if err = batch.Write(); err != nil { + return nil, err + } + diskLayer := &diskLayer{ + diskdb: s.diskdb, + blockNumber: number, + blockRoot: diff.blockRoot, + cache: s.cache, + } + + // reuse cache + for addr, nodes := range nodeSet { + for path, val := range nodes { + diskLayer.cache.Add(cacheKey(addr, path), val) + } + } + return diskLayer, nil +} + +func (s *diskLayer) writeHistory(number *big.Int, batch ethdb.Batch, nodeSet map[common.Hash]map[string][]byte) error { + for addr, subSet := range nodeSet { + for path, val := range subSet { + // refresh plain state + if len(val) == 0 { + if err := rawdb.DeleteEpochMetaPlainState(batch, addr, path); err != nil { + return err + } + } else { + if err := rawdb.WriteEpochMetaPlainState(batch, addr, path, val); err != nil { + return err + } + } + } + } + log.Debug("shadow node history pruned, only keep plainState", "number", number, "count", len(nodeSet)) + return nil +} + +func cacheKey(addr common.Hash, path string) string { + key := make([]byte, len(addr)+len(path)) + copy(key[:], addr.Bytes()) + copy(key[len(addr):], path) + return string(key) +} diff --git a/trie/epochmeta/snapshot.go b/trie/epochmeta/snapshot.go new file mode 100644 index 0000000000..89675b760a --- /dev/null +++ b/trie/epochmeta/snapshot.go @@ -0,0 +1,345 @@ +package epochmeta + +import ( + "bytes" + "errors" + "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rlp" + "io" + "math/big" + "sync" +) + +// snapshot record diff layer and disk layer of shadow nodes, support mini reorg +type snapshot interface { + // Root block state root + Root() common.Hash + + // EpochMeta query shadow node from db, got RLP format + EpochMeta(addrHash common.Hash, path string) ([]byte, error) + + // Parent parent snap + Parent() snapshot + + // Update create a new diff layer from here + Update(blockNumber *big.Int, blockRoot common.Hash, nodeSet map[common.Hash]map[string][]byte) (snapshot, error) + + // Journal commit self as a journal to buffer + Journal(buffer *bytes.Buffer) (common.Hash, error) +} + +type Config struct { + capLimit int // it indicates how depth diff layer to keep +} + +var Defaults = &Config{ + capLimit: MaxEpochMetaDiffDepth, +} + +// SnapshotTree maintain all diff layers support reorg, will flush to db when MaxEpochMetaDiffDepth reach +// every layer response to a block state change set, there no flatten layers operation. +type SnapshotTree struct { + diskdb ethdb.KeyValueStore + + // diffLayers + diskLayer, disk layer, always not nil + layers map[common.Hash]snapshot + children map[common.Hash][]common.Hash + cfg *Config + + lock sync.RWMutex +} + +func NewEpochMetaSnapTree(diskdb ethdb.KeyValueStore, cfg *Config) (*SnapshotTree, error) { + diskLayer, err := loadDiskLayer(diskdb) + if err != nil { + return nil, err + } + layers, children, err := loadDiffLayers(diskdb, diskLayer) + if err != nil { + return nil, err + } + + layers[diskLayer.blockRoot] = diskLayer + // check if continuously after disk layer + if len(layers) > 1 && len(children[diskLayer.blockRoot]) == 0 { + return nil, errors.New("cannot found any diff layers link to disk layer") + } + + if cfg == nil { + cfg = Defaults + } + return &SnapshotTree{ + diskdb: diskdb, + layers: layers, + children: children, + cfg: cfg, + }, nil +} + +// Cap keep tree depth not greater MaxEpochMetaDiffDepth, all forks parent to disk layer will delete +func (s *SnapshotTree) Cap(blockRoot common.Hash) error { + snap := s.Snapshot(blockRoot) + if snap == nil { + return fmt.Errorf("epoch meta snapshot missing: [%#x]", blockRoot) + } + nextDiff, ok := snap.(*diffLayer) + if !ok { + return nil + } + for i := 0; i < s.cfg.capLimit-1; i++ { + nextDiff, ok = nextDiff.Parent().(*diffLayer) + // if depth less MaxEpochMetaDiffDepth, just return + if !ok { + return nil + } + } + + flatten := make([]snapshot, 0) + parent := nextDiff.Parent() + for parent != nil { + flatten = append(flatten, parent) + parent = parent.Parent() + } + if len(flatten) <= 1 { + return nil + } + + last, ok := flatten[len(flatten)-1].(*diskLayer) + if !ok { + return errors.New("the diff layers not link to disk layer") + } + + s.lock.Lock() + defer s.lock.Unlock() + newDiskLayer, err := s.flattenDiffs2Disk(flatten[:len(flatten)-1], last) + if err != nil { + return err + } + + // clear forks, but keep latest disk forks + for i := len(flatten) - 1; i > 0; i-- { + var childRoot common.Hash + if i > 0 { + childRoot = flatten[i-1].Root() + } else { + childRoot = nextDiff.Root() + } + root := flatten[i].Root() + s.removeSubLayers(s.children[root], &childRoot) + delete(s.layers, root) + delete(s.children, root) + } + + // reset newDiskLayer and children's parent + s.layers[newDiskLayer.Root()] = newDiskLayer + for _, child := range s.children[newDiskLayer.Root()] { + if diff, exist := s.layers[child].(*diffLayer); exist { + diff.resetParent(newDiskLayer) + } + } + log.Info("SnapshotTree cap", "layers", len(s.layers), "children", len(s.children), "flatten", len(flatten)) + return nil +} + +func (s *SnapshotTree) Update(parentRoot common.Hash, blockNumber *big.Int, blockRoot common.Hash, nodeSet map[common.Hash]map[string][]byte) error { + // if there are no changes, just skip + if blockRoot == parentRoot { + return nil + } + + // Generate a new snapshot on top of the parent + parent := s.Snapshot(parentRoot) + if parent == nil { + // just point to fake disk layers + parent = s.Snapshot(types.EmptyRootHash) + if parent == nil { + return errors.New("cannot find any suitable parent") + } + parentRoot = parent.Root() + } + snap, err := parent.Update(blockNumber, blockRoot, nodeSet) + if err != nil { + return err + } + + s.lock.Lock() + defer s.lock.Unlock() + + s.layers[blockRoot] = snap + s.children[parentRoot] = append(s.children[parentRoot], blockRoot) + return nil +} + +func (s *SnapshotTree) Snapshot(blockRoot common.Hash) snapshot { + s.lock.RLock() + defer s.lock.RUnlock() + return s.layers[blockRoot] +} + +func (s *SnapshotTree) DB() ethdb.KeyValueStore { + s.lock.RLock() + defer s.lock.RUnlock() + return s.diskdb +} + +func (s *SnapshotTree) Journal() error { + s.lock.Lock() + defer s.lock.Unlock() + + // Firstly write out the metadata of journal + journal := new(bytes.Buffer) + if err := rlp.Encode(journal, journalVersion); err != nil { + return err + } + for _, snap := range s.layers { + if _, err := snap.Journal(journal); err != nil { + return err + } + } + rawdb.WriteEpochMetaSnapshotJournal(s.diskdb, journal.Bytes()) + return nil +} + +func (s *SnapshotTree) removeSubLayers(layers []common.Hash, skip *common.Hash) { + for _, layer := range layers { + if skip != nil && layer == *skip { + continue + } + s.removeSubLayers(s.children[layer], nil) + delete(s.layers, layer) + delete(s.children, layer) + } +} + +// flattenDiffs2Disk delete all flatten and push them to db +func (s *SnapshotTree) flattenDiffs2Disk(flatten []snapshot, diskLayer *diskLayer) (*diskLayer, error) { + var err error + for i := len(flatten) - 1; i >= 0; i-- { + diskLayer, err = diskLayer.PushDiff(flatten[i].(*diffLayer)) + if err != nil { + return nil, err + } + } + + return diskLayer, nil +} + +// loadDiskLayer load from db, could be nil when none in db +func loadDiskLayer(db ethdb.KeyValueStore) (*diskLayer, error) { + val := rawdb.ReadEpochMetaPlainStateMeta(db) + // if there is no disk layer, will construct a fake disk layer + if len(val) == 0 { + diskLayer, err := newEpochMetaDiskLayer(db, common.Big0, types.EmptyRootHash) + if err != nil { + return nil, err + } + return diskLayer, nil + } + var meta epochMetaPlainMeta + if err := rlp.DecodeBytes(val, &meta); err != nil { + return nil, err + } + + layer, err := newEpochMetaDiskLayer(db, meta.BlockNumber, meta.BlockRoot) + if err != nil { + return nil, err + } + return layer, nil +} + +type diffTmp struct { + parent common.Hash + number big.Int + root common.Hash + nodeSet map[common.Hash]map[string][]byte +} + +func loadDiffLayers(db ethdb.KeyValueStore, dl *diskLayer) (map[common.Hash]snapshot, map[common.Hash][]common.Hash, error) { + layers := make(map[common.Hash]snapshot) + children := make(map[common.Hash][]common.Hash) + + journal := rawdb.ReadEpochMetaSnapshotJournal(db) + if len(journal) == 0 { + return layers, children, nil + } + r := rlp.NewStream(bytes.NewReader(journal), 0) + // Firstly, resolve the first element as the journal version + version, err := r.Uint64() + if err != nil { + return nil, nil, errors.New("failed to resolve journal version") + } + if version != journalVersion { + return nil, nil, errors.New("wrong journal version") + } + + diffTmps := make(map[common.Hash]diffTmp) + parents := make(map[common.Hash]common.Hash) + for { + var ( + parent common.Hash + number big.Int + root common.Hash + js []journalEpochMeta + ) + // Read the next diff journal entry + if err := r.Decode(&number); err != nil { + // The first read may fail with EOF, marking the end of the journal + if errors.Is(err, io.EOF) { + break + } + return nil, nil, fmt.Errorf("load diff number: %v", err) + } + if err := r.Decode(&parent); err != nil { + return nil, nil, fmt.Errorf("load diff parent: %v", err) + } + // Read the next diff journal entry + if err := r.Decode(&root); err != nil { + return nil, nil, fmt.Errorf("load diff root: %v", err) + } + if err := r.Decode(&js); err != nil { + return nil, nil, fmt.Errorf("load diff storage: %v", err) + } + + nodeSet := make(map[common.Hash]map[string][]byte) + for _, entry := range js { + nodes := make(map[string][]byte) + for i, key := range entry.Keys { + if len(entry.Vals[i]) > 0 { // RLP loses nil-ness, but `[]byte{}` is not a valid item, so reinterpret that + nodes[key] = entry.Vals[i] + } else { + nodes[key] = nil + } + } + nodeSet[entry.Hash] = nodes + } + + diffTmps[root] = diffTmp{ + parent: parent, + number: number, + root: root, + nodeSet: nodeSet, + } + children[parent] = append(children[parent], root) + + parents[root] = parent + layers[root] = newEpochMetaDiffLayer(&number, root, dl, nodeSet) + } + + // rebuild diff layers from disk layer + rebuildFromParent(dl, children, layers, diffTmps) + return layers, children, nil +} + +func rebuildFromParent(p snapshot, children map[common.Hash][]common.Hash, layers map[common.Hash]snapshot, diffTmps map[common.Hash]diffTmp) { + subs := children[p.Root()] + for _, cur := range subs { + df := diffTmps[cur] + layers[cur] = newEpochMetaDiffLayer(&df.number, df.root, p, df.nodeSet) + rebuildFromParent(layers[cur], children, layers, diffTmps) + } +} diff --git a/trie/epochmeta/snapshot_test.go b/trie/epochmeta/snapshot_test.go new file mode 100644 index 0000000000..35099d5ba2 --- /dev/null +++ b/trie/epochmeta/snapshot_test.go @@ -0,0 +1,120 @@ +package epochmeta + +import ( + "github.com/ethereum/go-ethereum/ethdb/memorydb" + "github.com/stretchr/testify/assert" + "math/big" + "strconv" + "testing" +) + +func TestEpochMetaDiffLayer_capDiffLayers(t *testing.T) { + diskdb := memorydb.New() + // create empty tree + tree, err := NewEpochMetaSnapTree(diskdb, nil) + assert.NoError(t, err) + + // push 200 diff layers + count := 1 + for i := 0; i < 200; i++ { + ns := strconv.Itoa(count) + root := makeHash("b" + ns) + parent := makeHash("b" + strconv.Itoa(count-1)) + number := new(big.Int).SetUint64(uint64(count)) + err = tree.Update(parent, number, + root, makeNodeSet(contract1, []string{"hello" + ns, "world" + ns})) + assert.NoError(t, err) + + // add 10 forks + for j := 0; j < 10; j++ { + fs := strconv.Itoa(j) + err = tree.Update(parent, number, + makeHash("b"+ns+"f"+fs), makeNodeSet(contract1, []string{"hello" + ns + "f" + fs, "world" + ns + "f" + fs})) + assert.NoError(t, err) + } + + err = tree.Cap(root) + assert.NoError(t, err) + count++ + } + assert.Equal(t, 1409, len(tree.layers)) + + // push 100 diff layers, and cap + for i := 0; i < 100; i++ { + ns := strconv.Itoa(count) + parent := makeHash("b" + strconv.Itoa(count-1)) + root := makeHash("b" + ns) + number := new(big.Int).SetUint64(uint64(count)) + err = tree.Update(parent, number, root, + makeNodeSet(contract1, []string{"hello" + ns, "world" + ns})) + assert.NoError(t, err) + + // add 20 forks + for j := 0; j < 10; j++ { + fs := strconv.Itoa(j) + err = tree.Update(parent, number, + makeHash("b"+ns+"f"+fs), makeNodeSet(contract1, []string{"hello" + ns + "f" + fs, "world" + ns + "f" + fs})) + assert.NoError(t, err) + } + for j := 0; j < 10; j++ { + fs := strconv.Itoa(j) + err = tree.Update(makeHash("b"+strconv.Itoa(count-1)+"f"+fs), number, + makeHash("b"+ns+"f"+fs), makeNodeSet(contract1, []string{"hello" + ns + "f" + fs, "world" + ns + "f" + fs})) + assert.NoError(t, err) + } + count++ + } + lastRoot := makeHash("b" + strconv.Itoa(count-1)) + err = tree.Cap(lastRoot) + assert.NoError(t, err) + assert.Equal(t, 1409, len(tree.layers)) + + // push 100 diff layers, and cap + for i := 0; i < 129; i++ { + ns := strconv.Itoa(count) + parent := makeHash("b" + strconv.Itoa(count-1)) + root := makeHash("b" + ns) + number := new(big.Int).SetUint64(uint64(count)) + err = tree.Update(parent, number, root, + makeNodeSet(contract1, []string{"hello" + ns, "world" + ns})) + assert.NoError(t, err) + + count++ + } + lastRoot = makeHash("b" + strconv.Itoa(count-1)) + err = tree.Cap(lastRoot) + assert.NoError(t, err) + + assert.Equal(t, 129, len(tree.layers)) + assert.Equal(t, 128, len(tree.children)) + for parent, children := range tree.children { + if tree.layers[parent] == nil { + t.Log(tree.layers[parent]) + } + assert.NotNil(t, tree.layers[parent]) + for _, child := range children { + if tree.layers[child] == nil { + t.Log(tree.layers[child]) + } + assert.NotNil(t, tree.layers[child]) + } + } + + snap := tree.Snapshot(lastRoot) + assert.NotNil(t, snap) + for i := 1; i < count; i++ { + ns := strconv.Itoa(i) + n, err := snap.EpochMeta(contract1, "hello"+ns) + assert.NoError(t, err) + assert.Equal(t, []byte("world"+ns), n) + } + + // store + err = tree.Journal() + assert.NoError(t, err) + + tree, err = NewEpochMetaSnapTree(diskdb, nil) + assert.NoError(t, err) + assert.Equal(t, 129, len(tree.layers)) + assert.Equal(t, 128, len(tree.children)) +} diff --git a/trie/trie.go b/trie/trie.go index 0398d569d4..8ab262cc97 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -1055,11 +1055,17 @@ func (t *Trie) resolveEpochMeta(n node, epoch types.StateEpoch, prefix []byte) e return nil case *fullNode: n.setEpoch(epoch) + // TODO if parent's epoch <= 1, just set Epoch0, opt in startup hit more time epochmeta problem + //if epoch <= types.StateEpoch1 { + // return nil + //} meta, err := t.reader.epochMeta(prefix) if err != nil { return err } - n.EpochMap = meta.EpochMap + if meta != nil { + n.EpochMap = meta.EpochMap + } return nil case valueNode, hashNode, nil: // just skip @@ -1430,8 +1436,8 @@ type NodeInfo struct { IsBranch bool } -// PruneExpired traverses the storage trie and prunes all expired nodes. -func (t *Trie) PruneExpired(pruneItemCh chan *NodeInfo, stats *atomic.Uint64) error { +// ScanForPrune traverses the storage trie and prunes all expired or unexpired nodes. +func (t *Trie) ScanForPrune(itemCh chan *NodeInfo, stats *atomic.Uint64, findExpired bool) error { if !t.enableExpiry { return nil @@ -1442,10 +1448,10 @@ func (t *Trie) PruneExpired(pruneItemCh chan *NodeInfo, stats *atomic.Uint64) er } err := t.findExpiredSubTree(t.root, nil, t.getRootEpoch(), func(n node, path []byte, epoch types.StateEpoch) { - if pruneErr := t.recursePruneExpiredNode(n, path, epoch, pruneItemCh); pruneErr != nil { + if pruneErr := t.recursePruneExpiredNode(n, path, epoch, itemCh); pruneErr != nil { log.Error("recursePruneExpiredNode err", "Path", path, "err", pruneErr) } - }, stats) + }, stats, itemCh, findExpired) if err != nil { return err } @@ -1453,12 +1459,14 @@ func (t *Trie) PruneExpired(pruneItemCh chan *NodeInfo, stats *atomic.Uint64) er return nil } -func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, pruner func(n node, path []byte, epoch types.StateEpoch), stats *atomic.Uint64) error { +func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, pruner func(n node, path []byte, epoch types.StateEpoch), stats *atomic.Uint64, itemCh chan *NodeInfo, findExpired bool) error { // Upon reaching expired node, it will recursively traverse downwards to all the child nodes // and collect their hashes. Then, the corresponding key-value pairs will be deleted from the // database by batches. if t.epochExpired(n, epoch) { - pruner(n, path, epoch) + if findExpired { + pruner(n, path, epoch) + } return nil } @@ -1467,7 +1475,12 @@ func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, p if stats != nil { stats.Add(1) } - err := t.findExpiredSubTree(n.Val, append(path, n.Key...), epoch, pruner, stats) + if !findExpired { + itemCh <- &NodeInfo{ + Hash: common.BytesToHash(n.flags.hash), + } + } + err := t.findExpiredSubTree(n.Val, append(path, n.Key...), epoch, pruner, stats, nil, false) if err != nil { return err } @@ -1479,7 +1492,7 @@ func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, p var err error // Go through every child and recursively delete expired nodes for i, child := range n.Children { - err = t.findExpiredSubTree(child, append(path, byte(i)), n.GetChildEpoch(i), pruner, stats) + err = t.findExpiredSubTree(child, append(path, byte(i)), n.GetChildEpoch(i), pruner, stats, nil, false) if err != nil { return err } @@ -1498,7 +1511,7 @@ func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, p return err } - return t.findExpiredSubTree(resolve, path, epoch, pruner, stats) + return t.findExpiredSubTree(resolve, path, epoch, pruner, stats, nil, false) case valueNode: return nil case nil: diff --git a/trie/trie_reader.go b/trie/trie_reader.go index 926221f507..e0d7b2bf3e 100644 --- a/trie/trie_reader.go +++ b/trie/trie_reader.go @@ -43,10 +43,10 @@ type Reader interface { // trieReader is a wrapper of the underlying node reader. It's not safe // for concurrent usage. type trieReader struct { - owner common.Hash - reader Reader - emdb epochmeta.Storage - banned map[string]struct{} // Marker to prevent node from being accessed, for tests + owner common.Hash + reader Reader + emReader *epochmeta.Reader + banned map[string]struct{} // Marker to prevent node from being accessed, for tests } // newTrieReader initializes the trie reader with the given node reader. @@ -72,7 +72,7 @@ func newTrieReader(stateRoot, owner common.Hash, db *Database) (*trieReader, err } tr := trieReader{owner: owner, reader: reader} if db.snapTree != nil { - tr.emdb, err = epochmeta.NewEpochMetaDatabase(db.snapTree, new(big.Int), stateRoot) + tr.emReader, err = epochmeta.NewReader(db.snapTree, new(big.Int), stateRoot) if err != nil { return nil, err } @@ -123,19 +123,17 @@ func (l *trieLoader) OpenStorageTrie(stateRoot common.Hash, addrHash, root commo // epochMeta resolve from epoch meta storage func (r *trieReader) epochMeta(path []byte) (*epochmeta.BranchNodeEpochMeta, error) { - if r.emdb == nil { + if r.emReader == nil { return nil, fmt.Errorf("cannot resolve epochmeta without db, path: %#x", path) } // epoch meta cloud be empty, because epoch0 or delete? - blob, err := r.emdb.Get(r.owner, string(path)) + blob, err := r.emReader.Get(r.owner, string(path)) if err != nil { return nil, fmt.Errorf("resolve epoch meta err, path: %#x, err: %v", path, err) } if len(blob) == 0 { - // set default epoch map - // TODO(0xbundler): remove mem alloc? - return epochmeta.NewBranchNodeEpochMeta([16]types.StateEpoch{}), nil + return nil, nil } meta, err := epochmeta.DecodeFullNodeEpochMeta(blob) if err != nil { @@ -146,11 +144,11 @@ func (r *trieReader) epochMeta(path []byte) (*epochmeta.BranchNodeEpochMeta, err // accountMeta resolve account metadata func (r *trieReader) accountMeta() (types.MetaNoConsensus, error) { - if r.emdb == nil { + if r.emReader == nil { return types.EmptyMetaNoConsensus, errors.New("cannot resolve epoch meta without db for account") } - blob, err := r.emdb.Get(r.owner, epochmeta.AccountMetadataPath) + blob, err := r.emReader.Get(r.owner, epochmeta.AccountMetadataPath) if err != nil { return types.EmptyMetaNoConsensus, fmt.Errorf("resolve epoch meta err for account, err: %v", err) } From 5f776c2cd63bc52395fb5d52035d3eb378b42d23 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Thu, 12 Oct 2023 21:42:25 +0800 Subject: [PATCH 47/65] metrics: add some trace metrics for epoch meta; trie/trie: fix some update epoch bugs; state/state_object: fix epoch update issue; state/statedb: fix oom issue; --- core/blockchain.go | 14 +++++--- core/state/state_expiry.go | 3 ++ core/state/state_object.go | 19 ++++++---- core/state/statedb.go | 12 +++++++ trie/epochmeta/difflayer.go | 36 ++++++++++--------- trie/epochmeta/snapshot.go | 3 +- trie/trie.go | 64 ++++++++++++++++----------------- trie/trie_expiry.go | 8 ++--- trie/trie_reader.go | 17 +++++++++ trie/triedb/pathdb/layertree.go | 3 ++ 10 files changed, 115 insertions(+), 64 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index c69a23b97b..48932115a6 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1645,6 +1645,10 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. // If node is running in path mode, skip explicit gc operation // which is unnecessary in this mode. if bc.triedb.Scheme() == rawdb.PathScheme { + err := bc.triedb.CommitEpochMeta(block.Root()) + if err != nil { + return err + } return nil } @@ -1659,10 +1663,12 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. // Full but not archive node, do proper garbage collection triedb.Reference(block.Root(), common.Hash{}) // metadata reference to keep trie alive bc.triegc.Push(block.Root(), -int64(block.NumberU64())) - err := triedb.CommitEpochMeta(block.Root()) - if err != nil { - return err - } + // TODO(0xbundler): when opt commit later, remove it. + go triedb.CommitEpochMeta(block.Root()) + //err := triedb.CommitEpochMeta(block.Root()) + //if err != nil { + // return err + //} if current := block.NumberU64(); current > bc.triesInMemory { // If we exceeded our memory allowance, flush matured singleton nodes to disk diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index fbbd45f9a4..1f90760a43 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -112,6 +112,9 @@ func batchFetchExpiredFromRemote(expiryMeta *stateExpiryMeta, addr common.Addres keysStr[i] = common.Bytes2Hex(key[:]) } } + if len(prefixKeysStr) == 0 { + return ret, nil + } // cannot revive locally, fetch remote proof proofs, err := expiryMeta.fullStateDB.GetStorageReviveProof(expiryMeta.originalRoot, addr, root, prefixKeysStr, keysStr) diff --git a/core/state/state_object.go b/core/state/state_object.go index 9575bb5208..e4c39289a6 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -169,7 +169,6 @@ func (s *stateObject) getTrie() (Trie, error) { s.trie = s.db.prefetcher.trie(s.addrHash, s.data.Root) } if s.trie == nil { - // TODO(0xbundler): if any change to open a storage trie in state expiry feature? tr, err := s.db.db.OpenStorageTrie(s.db.originalRoot, s.address, s.data.Root) if err != nil { return nil, err @@ -320,6 +319,7 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { // handle state expiry situation if s.db.EnableExpire() { if enErr, ok := err.(*trie.ExpiredNodeError); ok { + log.Debug("GetCommittedState expired in trie", "addr", s.address, "key", key, "err", err) val, err = s.fetchExpiredFromRemote(enErr.Path, key, false) } // TODO(0xbundler): add epoch record cache for prevent frequency access epoch update, may implement later @@ -486,6 +486,7 @@ func (s *stateObject) updateTrie() (Trie, error) { if _, err = fetchExpiredStorageFromRemote(s.db.expiryMeta, s.address, s.data.Root, tr, enErr.Path, key); err != nil { s.db.setError(fmt.Errorf("state object pendingFutureReviveState fetchExpiredStorageFromRemote err, contract: %v, key: %v, path: %v, err: %v", s.address, key, enErr.Path, err)) } + log.Debug("updateTrie pendingFutureReviveState", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "tr.epoch", tr.Epoch(), "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) } } for key, value := range dirtyStorage { @@ -493,11 +494,13 @@ func (s *stateObject) updateTrie() (Trie, error) { if err := tr.DeleteStorage(s.address, key[:]); err != nil { s.db.setError(fmt.Errorf("state object update trie DeleteStorage err, contract: %v, key: %v, err: %v", s.address, key, err)) } + log.Debug("updateTrie DeleteStorage", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "value", value, "tr.epoch", tr.Epoch(), "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) s.db.StorageDeleted += 1 } else { if err := tr.UpdateStorage(s.address, key[:], value); err != nil { s.db.setError(fmt.Errorf("state object update trie UpdateStorage err, contract: %v, key: %v, err: %v", s.address, key, err)) } + log.Debug("updateTrie UpdateStorage", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "value", value, "tr.epoch", tr.Epoch(), "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) s.db.StorageUpdated += 1 } // Cache the items for preloading @@ -527,13 +530,16 @@ func (s *stateObject) updateTrie() (Trie, error) { // rlp-encoded value to be used by the snapshot var snapshotVal []byte - // Encoding []byte cannot fail, ok to ignore the error. - if s.db.EnableExpire() { - snapshotVal, _ = snapshot.EncodeValueToRLPBytes(snapshot.NewValueWithEpoch(s.db.Epoch(), value)) - } else { - snapshotVal, _ = rlp.EncodeToBytes(value) + if len(value) != 0 { + // Encoding []byte cannot fail, ok to ignore the error. + if s.db.EnableExpire() { + snapshotVal, _ = snapshot.EncodeValueToRLPBytes(snapshot.NewValueWithEpoch(s.db.Epoch(), value)) + } else { + snapshotVal, _ = rlp.EncodeToBytes(value) + } } storage[khash] = snapshotVal // snapshotVal will be nil if it's deleted + log.Debug("updateTrie UpdateSnapShot", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "value", snapshotVal, "tr.epoch", tr.Epoch(), "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) // Track the original value of slot only if it's mutated first time prev := s.originStorage[key] @@ -905,6 +911,7 @@ func (s *stateObject) getExpirySnapStorage(key common.Hash) ([]byte, error, erro return val.GetVal(), nil, nil } + log.Debug("GetCommittedState expired in snapshot", "addr", s.address, "key", key, "val", val, "enc", enc, "err", err) // handle from remoteDB, if got err just setError, or return to revert in consensus version. valRaw, err := s.fetchExpiredFromRemote(nil, key, true) if err != nil { diff --git a/core/state/statedb.go b/core/state/statedb.go index 142e434e59..a7eb0aa300 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -18,6 +18,7 @@ package state import ( + "bytes" "errors" "fmt" "github.com/ethereum/go-ethereum/ethdb" @@ -1800,6 +1801,7 @@ func (s *StateDB) Commit(block uint64, failPostCommitFunc func(), postCommitFunc if root == (common.Hash{}) { root = types.EmptyRootHash } + //log.Info("state commit", "nodes", stringfyEpochMeta(nodes.FlattenEpochMeta())) //origin := s.originalRoot //if origin == (common.Hash{}) { // origin = types.EmptyRootHash @@ -1824,6 +1826,16 @@ func (s *StateDB) Commit(block uint64, failPostCommitFunc func(), postCommitFunc return root, diffLayer, nil } +func stringfyEpochMeta(meta map[common.Hash]map[string][]byte) string { + buf := bytes.NewBuffer(nil) + for hash, m := range meta { + for k, v := range m { + buf.WriteString(fmt.Sprintf("%v: %v|%v, ", hash, []byte(k), common.Bytes2Hex(v))) + } + } + return buf.String() +} + func (s *StateDB) SnapToDiffLayer() ([]common.Address, []types.DiffAccount, []types.DiffStorage) { destructs := make([]common.Address, 0, len(s.stateObjectsDestruct)) for account := range s.stateObjectsDestruct { diff --git a/trie/epochmeta/difflayer.go b/trie/epochmeta/difflayer.go index 1dc8f9dbe0..2838e8a9d0 100644 --- a/trie/epochmeta/difflayer.go +++ b/trie/epochmeta/difflayer.go @@ -17,6 +17,7 @@ const ( // MaxEpochMetaDiffDepth default is 128 layers MaxEpochMetaDiffDepth = 128 journalVersion uint64 = 1 + enableBloomFilter = false ) var ( @@ -95,7 +96,6 @@ func (h storageBloomHasher) Sum64() uint64 { binary.BigEndian.Uint64([]byte(h.path[bloomStorageHasherOffset:bloomStorageHasherOffset+8])) } -// TODO(0xbundler): add bloom filter? type diffLayer struct { blockNumber *big.Int blockRoot common.Hash @@ -114,23 +114,25 @@ func newEpochMetaDiffLayer(blockNumber *big.Int, blockRoot common.Hash, parent s nodeSet: nodeSet, } - switch p := parent.(type) { - case *diffLayer: - dl.origin = p.origin - dl.diffed, _ = p.diffed.Copy() - case *diskLayer: - dl.origin = p - dl.diffed, _ = bloomfilter.New(uint64(bloomSize), uint64(bloomFuncs)) - default: - panic("newEpochMetaDiffLayer got wrong snapshot type") - } - - // Iterate over all the accounts and storage metas and index them - for accountHash, metas := range dl.nodeSet { - for path := range metas { - dl.diffed.Add(storageBloomHasher{accountHash, path}) + if enableBloomFilter { + switch p := parent.(type) { + case *diffLayer: + dl.origin = p.origin + dl.diffed, _ = p.diffed.Copy() + case *diskLayer: + dl.origin = p + dl.diffed, _ = bloomfilter.New(uint64(bloomSize), uint64(bloomFuncs)) + default: + panic("newEpochMetaDiffLayer got wrong snapshot type") + } + // Iterate over all the accounts and storage metas and index them + for accountHash, metas := range dl.nodeSet { + for path := range metas { + dl.diffed.Add(storageBloomHasher{accountHash, path}) + } } } + return dl } @@ -141,7 +143,7 @@ func (s *diffLayer) Root() common.Hash { // EpochMeta find target epoch meta from diff layer or disk layer func (s *diffLayer) EpochMeta(addrHash common.Hash, path string) ([]byte, error) { // if the diff chain not contain the meta or staled, try get from disk layer - if !s.diffed.Contains(storageBloomHasher{addrHash, path}) { + if s.diffed != nil && !s.diffed.Contains(storageBloomHasher{addrHash, path}) { return s.origin.EpochMeta(addrHash, path) } diff --git a/trie/epochmeta/snapshot.go b/trie/epochmeta/snapshot.go index 89675b760a..71201f961d 100644 --- a/trie/epochmeta/snapshot.go +++ b/trie/epochmeta/snapshot.go @@ -142,7 +142,7 @@ func (s *SnapshotTree) Cap(blockRoot common.Hash) error { diff.resetParent(newDiskLayer) } } - log.Info("SnapshotTree cap", "layers", len(s.layers), "children", len(s.children), "flatten", len(flatten)) + log.Debug("epochmeta snap tree cap", "root", blockRoot, "layers", len(s.layers), "flatten", len(flatten)) return nil } @@ -172,6 +172,7 @@ func (s *SnapshotTree) Update(parentRoot common.Hash, blockNumber *big.Int, bloc s.layers[blockRoot] = snap s.children[parentRoot] = append(s.children[parentRoot], blockRoot) + log.Debug("epochmeta snap tree update", "root", blockRoot, "number", blockNumber, "layers", len(s.layers)) return nil } diff --git a/trie/trie.go b/trie/trie.go index 8ab262cc97..fe364aac0b 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -274,6 +274,7 @@ func (t *Trie) getWithEpoch(origNode node, key []byte, pos int, epoch types.Stat if updateEpoch { n.setEpoch(t.currentEpoch) } + n.flags = t.newFlag() didResolve = true } return value, n, didResolve, err @@ -288,6 +289,7 @@ func (t *Trie) getWithEpoch(origNode node, key []byte, pos int, epoch types.Stat if updateEpoch && newnode != nil { n.UpdateChildEpoch(int(key[pos]), t.currentEpoch) } + n.flags = t.newFlag() didResolve = true } return value, n, didResolve, err @@ -297,10 +299,8 @@ func (t *Trie) getWithEpoch(origNode node, key []byte, pos int, epoch types.Stat return nil, n, true, err } - if child, ok := child.(*fullNode); ok { - if err = t.resolveEpochMeta(child, epoch, key[:pos]); err != nil { - return nil, n, true, err - } + if err = t.resolveEpochMeta(child, epoch, key[:pos]); err != nil { + return nil, n, true, err } value, newnode, _, err := t.getWithEpoch(child, key, pos, epoch, updateEpoch) return value, newnode, true, err @@ -626,7 +626,6 @@ func (t *Trie) insertWithEpoch(n node, prefix, key []byte, value node, epoch typ return false, nil, err } branch.UpdateChildEpoch(int(key[matchlen]), t.currentEpoch) - branch.setEpoch(t.currentEpoch) // Replace this shortNode with the branch if it occurs at index 0. if matchlen == 0 { @@ -672,10 +671,8 @@ func (t *Trie) insertWithEpoch(n node, prefix, key []byte, value node, epoch typ return false, nil, err } - if child, ok := rn.(*fullNode); ok { - if err = t.resolveEpochMeta(child, epoch, prefix); err != nil { - return false, nil, err - } + if err = t.resolveEpochMeta(rn, epoch, prefix); err != nil { + return false, nil, err } dirty, nn, err := t.insertWithEpoch(rn, prefix, key, value, epoch) @@ -813,7 +810,7 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) { // shortNode{..., shortNode{...}}. Since the entry // might not be loaded yet, resolve it just for this // check. - cnode, err := t.resolve(n.Children[pos], append(prefix, byte(pos))) + cnode, err := t.resolve(n.Children[pos], append(prefix, byte(pos)), n.GetChildEpoch(pos)) if err != nil { return false, nil, err } @@ -885,7 +882,7 @@ func (t *Trie) deleteWithEpoch(n node, prefix, key []byte, epoch types.StateEpoc // subtrie must contain at least two other values with keys // longer than n.Key. dirty, child, err := t.deleteWithEpoch(n.Val, append(prefix, key[:len(n.Key)]...), key[len(n.Key):], epoch) - if !t.renewNode(epoch, dirty, true) || err != nil { + if !dirty || err != nil { return false, n, err } switch child := child.(type) { @@ -907,7 +904,7 @@ func (t *Trie) deleteWithEpoch(n node, prefix, key []byte, epoch types.StateEpoc case *fullNode: dirty, nn, err := t.deleteWithEpoch(n.Children[key[0]], append(prefix, key[0]), key[1:], n.GetChildEpoch(int(key[0]))) - if !t.renewNode(epoch, dirty, true) || err != nil { + if !dirty || err != nil { return false, n, err } n = n.copy() @@ -952,7 +949,7 @@ func (t *Trie) deleteWithEpoch(n node, prefix, key []byte, epoch types.StateEpoc // shortNode{..., shortNode{...}}. Since the entry // might not be loaded yet, resolve it just for this // check. - cnode, err := t.resolve(n.Children[pos], append(prefix, byte(pos))) + cnode, err := t.resolve(n.Children[pos], append(prefix, byte(pos)), n.GetChildEpoch(pos)) if err != nil { return false, nil, err } @@ -990,14 +987,12 @@ func (t *Trie) deleteWithEpoch(n node, prefix, key []byte, epoch types.StateEpoc return false, nil, err } - if child, ok := rn.(*fullNode); ok { - if err = t.resolveEpochMeta(child, epoch, prefix); err != nil { - return false, nil, err - } + if err = t.resolveEpochMeta(rn, epoch, prefix); err != nil { + return false, nil, err } dirty, nn, err := t.deleteWithEpoch(rn, prefix, key, epoch) - if !t.renewNode(epoch, dirty, true) || err != nil { + if !dirty || err != nil { return false, rn, err } return true, nn, nil @@ -1015,9 +1010,16 @@ func concat(s1 []byte, s2 ...byte) []byte { return r } -func (t *Trie) resolve(n node, prefix []byte) (node, error) { +func (t *Trie) resolve(n node, prefix []byte, epoch types.StateEpoch) (node, error) { if n, ok := n.(hashNode); ok { - return t.resolveAndTrack(n, prefix) + n, err := t.resolveAndTrack(n, prefix) + if err != nil { + return nil, err + } + if err = t.resolveEpochMeta(n, epoch, prefix); err != nil { + return nil, err + } + return n, nil } return n, nil } @@ -1055,10 +1057,6 @@ func (t *Trie) resolveEpochMeta(n node, epoch types.StateEpoch, prefix []byte) e return nil case *fullNode: n.setEpoch(epoch) - // TODO if parent's epoch <= 1, just set Epoch0, opt in startup hit more time epochmeta problem - //if epoch <= types.StateEpoch1 { - // return nil - //} meta, err := t.reader.epochMeta(prefix) if err != nil { return err @@ -1298,11 +1296,8 @@ func (t *Trie) tryRevive(n node, key []byte, targetPrefixKey []byte, nub MPTProo if err != nil { return nil, false, err } - - if child, ok := child.(*fullNode); ok { - if err = t.resolveEpochMeta(child, epoch, key[:pos]); err != nil { - return nil, false, err - } + if err = t.resolveEpochMeta(child, epoch, key[:pos]); err != nil { + return nil, false, err } newNode, _, err := t.tryRevive(child, key, targetPrefixKey, nub, pos, epoch, isExpired) @@ -1480,7 +1475,7 @@ func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, p Hash: common.BytesToHash(n.flags.hash), } } - err := t.findExpiredSubTree(n.Val, append(path, n.Key...), epoch, pruner, stats, nil, false) + err := t.findExpiredSubTree(n.Val, append(path, n.Key...), epoch, pruner, stats, itemCh, findExpired) if err != nil { return err } @@ -1489,10 +1484,15 @@ func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, p if stats != nil { stats.Add(1) } + if !findExpired { + itemCh <- &NodeInfo{ + Hash: common.BytesToHash(n.flags.hash), + } + } var err error // Go through every child and recursively delete expired nodes for i, child := range n.Children { - err = t.findExpiredSubTree(child, append(path, byte(i)), n.GetChildEpoch(i), pruner, stats, nil, false) + err = t.findExpiredSubTree(child, append(path, byte(i)), n.GetChildEpoch(i), pruner, stats, itemCh, findExpired) if err != nil { return err } @@ -1511,7 +1511,7 @@ func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, p return err } - return t.findExpiredSubTree(resolve, path, epoch, pruner, stats, nil, false) + return t.findExpiredSubTree(resolve, path, epoch, pruner, stats, itemCh, findExpired) case valueNode: return nil case nil: diff --git a/trie/trie_expiry.go b/trie/trie_expiry.go index 1dd6f098a1..c284e4b6ae 100644 --- a/trie/trie_expiry.go +++ b/trie/trie_expiry.go @@ -38,6 +38,7 @@ func (t *Trie) tryLocalRevive(origNode node, key []byte, pos int, epoch types.St n = n.copy() n.Val = newnode n.setEpoch(t.currentEpoch) + n.flags = t.newFlag() didResolve = true } return value, n, didResolve, err @@ -50,6 +51,7 @@ func (t *Trie) tryLocalRevive(origNode node, key []byte, pos int, epoch types.St if newnode != nil { n.UpdateChildEpoch(int(key[pos]), t.currentEpoch) } + n.flags = t.newFlag() didResolve = true } return value, n, didResolve, err @@ -59,10 +61,8 @@ func (t *Trie) tryLocalRevive(origNode node, key []byte, pos int, epoch types.St return nil, n, true, err } - if child, ok := child.(*fullNode); ok { - if err = t.resolveEpochMeta(child, epoch, key[:pos]); err != nil { - return nil, n, true, err - } + if err = t.resolveEpochMeta(child, epoch, key[:pos]); err != nil { + return nil, n, true, err } value, newnode, _, err := t.tryLocalRevive(child, key, pos, epoch) return value, newnode, true, err diff --git a/trie/trie_reader.go b/trie/trie_reader.go index e0d7b2bf3e..c1b9424765 100644 --- a/trie/trie_reader.go +++ b/trie/trie_reader.go @@ -22,9 +22,17 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/trie/epochmeta" "github.com/ethereum/go-ethereum/trie/triestate" "math/big" + "time" +) + +var ( + accountMetaTimer = metrics.NewRegisteredTimer("trie/reader/accountmeta", nil) + epochMetaTimer = metrics.NewRegisteredTimer("trie/reader/epochmeta", nil) + nodeTimer = metrics.NewRegisteredTimer("trie/reader/node", nil) ) // Reader wraps the Node method of a backing trie store. @@ -90,6 +98,9 @@ func newEmptyReader() *trieReader { // information. An MissingNodeError will be returned in case the node is // not found or any error is encountered. func (r *trieReader) node(path []byte, hash common.Hash) ([]byte, error) { + defer func(start time.Time) { + nodeTimer.Update(time.Since(start)) + }(time.Now()) // Perform the logics in tests for preventing trie node access. if r.banned != nil { if _, ok := r.banned[string(path)]; ok { @@ -123,6 +134,9 @@ func (l *trieLoader) OpenStorageTrie(stateRoot common.Hash, addrHash, root commo // epochMeta resolve from epoch meta storage func (r *trieReader) epochMeta(path []byte) (*epochmeta.BranchNodeEpochMeta, error) { + defer func(start time.Time) { + epochMetaTimer.Update(time.Since(start)) + }(time.Now()) if r.emReader == nil { return nil, fmt.Errorf("cannot resolve epochmeta without db, path: %#x", path) } @@ -144,6 +158,9 @@ func (r *trieReader) epochMeta(path []byte) (*epochmeta.BranchNodeEpochMeta, err // accountMeta resolve account metadata func (r *trieReader) accountMeta() (types.MetaNoConsensus, error) { + defer func(start time.Time) { + accountMetaTimer.Update(time.Since(start)) + }(time.Now()) if r.emReader == nil { return types.EmptyMetaNoConsensus, errors.New("cannot resolve epoch meta without db for account") } diff --git a/trie/triedb/pathdb/layertree.go b/trie/triedb/pathdb/layertree.go index d314779910..f7fc4930bc 100644 --- a/trie/triedb/pathdb/layertree.go +++ b/trie/triedb/pathdb/layertree.go @@ -19,6 +19,7 @@ package pathdb import ( "errors" "fmt" + "github.com/ethereum/go-ethereum/log" "sync" "github.com/ethereum/go-ethereum/common" @@ -105,6 +106,7 @@ func (tree *layerTree) add(root common.Hash, parentRoot common.Hash, block uint6 tree.lock.Lock() tree.layers[l.rootHash()] = l + log.Debug("pathdb snap tree update", "root", root, "number", block, "layers", len(tree.layers)) tree.lock.Unlock() return nil } @@ -190,6 +192,7 @@ func (tree *layerTree) cap(root common.Hash, layers int) error { remove(root) } } + log.Debug("pathdb snap tree cap", "root", root, "layers", len(tree.layers)) return nil } From 4b05dc49f4bfeb217425b1d7110d32d5bf417fd0 Mon Sep 17 00:00:00 2001 From: asyukii Date: Thu, 12 Oct 2023 18:23:43 +0800 Subject: [PATCH 48/65] core/state: fix snapshot recovery a --- core/state/snapshot/generate.go | 26 ++++++++++++-- core/state/snapshot/generate_test.go | 45 ++++++++++++++++--------- ethclient/gethclient/gethclient.go | 4 +-- ethclient/gethclient/gethclient_test.go | 8 ++--- trie/trie_test.go | 2 +- 5 files changed, 59 insertions(+), 26 deletions(-) diff --git a/core/state/snapshot/generate.go b/core/state/snapshot/generate.go index 28f2abd289..7b5be0f2e7 100644 --- a/core/state/snapshot/generate.go +++ b/core/state/snapshot/generate.go @@ -194,7 +194,11 @@ func (dl *diskLayer) proveRange(ctx *generatorContext, trieId *trie.ID, prefix [ keys = append(keys, common.CopyBytes(key[len(prefix):])) if valueConvertFn == nil { - vals = append(vals, common.CopyBytes(iter.Value())) + rlpVal, err := convertSnapValToRLPVal(iter.Value()) + if err != nil { + return nil, err + } + vals = append(vals, rlpVal) } else { val, err := valueConvertFn(iter.Value()) if err != nil { @@ -204,10 +208,18 @@ func (dl *diskLayer) proveRange(ctx *generatorContext, trieId *trie.ID, prefix [ // // Here append the original value to ensure that the number of key and // value are aligned. - vals = append(vals, common.CopyBytes(iter.Value())) + rlpVal, err := convertSnapValToRLPVal(val) + if err != nil { + return nil, err + } + vals = append(vals, rlpVal) log.Error("Failed to convert account state data", "err", err) } else { - vals = append(vals, val) + rlpVal, err := convertSnapValToRLPVal(val) + if err != nil { + return nil, err + } + vals = append(vals, rlpVal) } } } @@ -735,6 +747,14 @@ func increaseKey(key []byte) []byte { return nil } +func convertSnapValToRLPVal(val []byte) ([]byte, error) { + snapVal, err := DecodeValueFromRLPBytes(val) + if err != nil { + return nil, err + } + return rlp.EncodeToBytes(snapVal.GetVal()) +} + // abortErr wraps an interruption signal received to represent the // generation is aborted by external processes. type abortErr struct { diff --git a/core/state/snapshot/generate_test.go b/core/state/snapshot/generate_test.go index 07016b675c..dc86636488 100644 --- a/core/state/snapshot/generate_test.go +++ b/core/state/snapshot/generate_test.go @@ -66,7 +66,7 @@ func testGeneration(t *testing.T, scheme string) { helper.makeStorageTrie(hashData([]byte("acc-3")), []string{"key-1", "key-2", "key-3"}, []string{"val-1", "val-2", "val-3"}, true) root, snap := helper.CommitAndGenerate() - if have, want := root, common.HexToHash("0xe3712f1a226f3782caca78ca770ccc19ee000552813a9f59d479f8611db9b1fd"); have != want { + if have, want := root, common.HexToHash("0x1b4b0ae3b50e6ce40184d08fc5857c5b6909e2b1d8017d9e3f69170e323b1f6c"); have != want { t.Fatalf("have %#x want %#x", have, want) } select { @@ -196,7 +196,8 @@ func (t *testHelper) addAccount(acckey string, acc *types.StateAccount) { func (t *testHelper) addSnapStorage(accKey string, keys []string, vals []string) { accHash := hashData([]byte(accKey)) for i, key := range keys { - rawdb.WriteStorageSnapshot(t.diskdb, accHash, hashData([]byte(key)), []byte(vals[i])) + val, _ := rlp.EncodeToBytes(vals[i]) + rawdb.WriteStorageSnapshot(t.diskdb, accHash, hashData([]byte(key)), val) } } @@ -204,7 +205,8 @@ func (t *testHelper) makeStorageTrie(owner common.Hash, keys []string, vals []st id := trie.StorageTrieID(types.EmptyRootHash, owner, types.EmptyRootHash) stTrie, _ := trie.NewStateTrie(id, t.triedb) for i, k := range keys { - stTrie.MustUpdate([]byte(k), []byte(vals[i])) + rlpVal, _ := rlp.EncodeToBytes(vals[i]) + stTrie.MustUpdate([]byte(k), rlpVal) // [133,118,97,108,45,49] } if !commit { return stTrie.Hash() @@ -512,7 +514,7 @@ func testGenerateCorruptStorageTrie(t *testing.T, scheme string) { // Delete a node in the storage trie. targetPath := []byte{0x4} - targetHash := common.HexToHash("0x18a0f4d79cff4459642dd7604f303886ad9d77c30cf3d7d7cedb3a693ab6d371") + targetHash := common.HexToHash("0x1b4b0ae3b50e6ce40184d08fc5857c5b6909e2b1d8017d9e3f69170e323b1f6c") rawdb.DeleteTrieNode(helper.diskdb, hashData([]byte("acc-1")), targetPath, targetHash, scheme) rawdb.DeleteTrieNode(helper.diskdb, hashData([]byte("acc-3")), targetPath, targetHash, scheme) @@ -552,12 +554,16 @@ func testGenerateWithExtraAccounts(t *testing.T, scheme string) { // Identical in the snap key := hashData([]byte("acc-1")) - rawdb.WriteAccountSnapshot(helper.diskdb, key, val) - rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-1")), []byte("val-1")) - rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-2")), []byte("val-2")) - rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-3")), []byte("val-3")) - rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-4")), []byte("val-4")) - rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-5")), []byte("val-5")) + val, _ = rlp.EncodeToBytes([]byte("val-1")) + rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-1")), val) + val, _ = rlp.EncodeToBytes([]byte("val-2")) + rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-2")), val) + val, _ = rlp.EncodeToBytes([]byte("val-3")) + rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-3")), val) + val, _ = rlp.EncodeToBytes([]byte("val-4")) + rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-4")), val) + val, _ = rlp.EncodeToBytes([]byte("val-5")) + rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-5")), val) } { // Account two exists only in the snapshot @@ -570,9 +576,13 @@ func testGenerateWithExtraAccounts(t *testing.T, scheme string) { val, _ := rlp.EncodeToBytes(acc) key := hashData([]byte("acc-2")) rawdb.WriteAccountSnapshot(helper.diskdb, key, val) - rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("b-key-1")), []byte("b-val-1")) - rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("b-key-2")), []byte("b-val-2")) - rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("b-key-3")), []byte("b-val-3")) + val, _ = rlp.EncodeToBytes([]byte("b-val-1")) + rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("b-key-1")), val) + val, _ = rlp.EncodeToBytes([]byte("b-val-2")) + rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("b-key-2")), val) + val, _ = rlp.EncodeToBytes([]byte("b-val-3")) + rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("b-key-3")), val) + } root := helper.Commit() @@ -629,9 +639,12 @@ func testGenerateWithManyExtraAccounts(t *testing.T, scheme string) { // Identical in the snap key := hashData([]byte("acc-1")) rawdb.WriteAccountSnapshot(helper.diskdb, key, val) - rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-1")), []byte("val-1")) - rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-2")), []byte("val-2")) - rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-3")), []byte("val-3")) + val, _ = rlp.EncodeToBytes([]byte("val-1")) + rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-1")), val) + val, _ = rlp.EncodeToBytes([]byte("val-2")) + rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-2")), val) + val, _ = rlp.EncodeToBytes([]byte("val-3")) + rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("key-3")), val) } { // 100 accounts exist only in snapshot diff --git a/ethclient/gethclient/gethclient.go b/ethclient/gethclient/gethclient.go index fd29676de0..7b9c8587fd 100644 --- a/ethclient/gethclient/gethclient.go +++ b/ethclient/gethclient/gethclient.go @@ -127,7 +127,7 @@ func (ec *Client) GetProof(ctx context.Context, account common.Address, keys []s // GetStorageReviveProof returns the proof for the given keys. Prefix keys can be specified to obtain partial proof for a given key. // Both keys and prefix keys should have the same length. If user wish to obtain full proof for a given key, the corresponding prefix key should be empty string. -func (ec *Client) GetStorageReviveProof(ctx context.Context, account common.Address, keys []string, prefixKeys []string, hash common.Hash) (*types.ReviveResult, error) { +func (ec *Client) GetStorageReviveProof(ctx context.Context, stateRoot common.Hash, account common.Address, root common.Hash, keys []string, prefixKeys []string) (*types.ReviveResult, error) { type reviveResult struct { StorageProof []types.ReviveStorageProof `json:"storageProof"` BlockNum hexutil.Uint64 `json:"blockNum"` @@ -136,7 +136,7 @@ func (ec *Client) GetStorageReviveProof(ctx context.Context, account common.Addr var err error var res reviveResult - err = ec.c.CallContext(ctx, &res, "eth_getStorageReviveProof", account, keys, prefixKeys, hash) + err = ec.c.CallContext(ctx, &res, "eth_getStorageReviveProof", stateRoot, account, root, keys, prefixKeys) return &types.ReviveResult{ StorageProof: res.StorageProof, diff --git a/ethclient/gethclient/gethclient_test.go b/ethclient/gethclient/gethclient_test.go index 3e24fcfa37..29b4b95f5f 100644 --- a/ethclient/gethclient/gethclient_test.go +++ b/ethclient/gethclient/gethclient_test.go @@ -93,7 +93,7 @@ func generateTestChain() (*core.Genesis, []*types.Block) { } func TestGethClient(t *testing.T) { - backend, _ := newTestBackend(t) + backend, blocks := newTestBackend(t) client := backend.Attach() defer backend.Close() defer client.Close() @@ -107,7 +107,7 @@ func TestGethClient(t *testing.T) { func(t *testing.T) { testGetProof(t, client) }, }, { "TestGetStorageReviveProof", - func(t *testing.T) { testGetStorageReviveProof(t, client) }, + func(t *testing.T) { testGetStorageReviveProof(t, client, blocks[0]) }, }, { "TestGetProofCanonicalizeKeys", func(t *testing.T) { testGetProofCanonicalizeKeys(t, client) }, @@ -239,9 +239,9 @@ func testGetProof(t *testing.T, client *rpc.Client) { } } -func testGetStorageReviveProof(t *testing.T, client *rpc.Client) { +func testGetStorageReviveProof(t *testing.T, client *rpc.Client, block *types.Block) { ec := New(client) - result, err := ec.GetStorageReviveProof(context.Background(), testAddr, []string{testSlot.String()}, []string{""}, common.Hash{}) + result, err := ec.GetStorageReviveProof(context.Background(), block.Header().Root, testAddr, block.Header().Root, []string{testSlot.String()}, []string{""}) proofs := result.StorageProof if err != nil { diff --git a/trie/trie_test.go b/trie/trie_test.go index dfff72b857..aa704f8166 100644 --- a/trie/trie_test.go +++ b/trie/trie_test.go @@ -1118,7 +1118,7 @@ func TestReviveBadProof(t *testing.T) { // Verify value does exists after revive val, err := trieA.Get([]byte("abcd")) - assert.NoError(t, err, "Get failed, key %x, val %x", []byte("abcd"), val) + assert.Error(t, err, "Get failed, key %x, val %x", []byte("abcd"), val) assert.NotEqual(t, []byte("A"), val) } From 4e7a438f01e9ea5a8e69a85903e8632dac9bb367 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Mon, 16 Oct 2023 15:36:44 +0800 Subject: [PATCH 49/65] trie: fix rebase error; --- trie/trie_reader.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trie/trie_reader.go b/trie/trie_reader.go index c1b9424765..24b63c3593 100644 --- a/trie/trie_reader.go +++ b/trie/trie_reader.go @@ -67,7 +67,7 @@ func newTrieReader(stateRoot, owner common.Hash, db *Database) (*trieReader, err } tr := &trieReader{owner: owner} if db.snapTree != nil { - tr.emdb, err = epochmeta.NewEpochMetaDatabase(db.snapTree, new(big.Int), stateRoot) + tr.emReader, err = epochmeta.NewReader(db.snapTree, new(big.Int), stateRoot) if err != nil { return nil, err } From 811f37a6b0644b12c9c587a8a0b6fabb42f27ddd Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Mon, 16 Oct 2023 15:31:18 +0800 Subject: [PATCH 50/65] trie: opt trie update; trie: fix rebase error; --- core/state/pruner/pruner.go | 11 +++- core/state/snapshot/snapshot_expire.go | 10 ++- trie/committer.go | 13 +++- trie/epochmeta/database.go | 16 +++++ trie/tracer.go | 82 +++++++++++++++++------- trie/trie.go | 88 +++++++++++++++++++++----- trie/trie_expiry.go | 2 +- trie/trie_reader.go | 19 ++---- trie/trienode/node.go | 25 ++------ 9 files changed, 189 insertions(+), 77 deletions(-) diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go index 9c9637ee02..3758793207 100644 --- a/core/state/pruner/pruner.go +++ b/core/state/pruner/pruner.go @@ -850,7 +850,7 @@ func asyncScanExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch type logged = time.Now() ) for item := range expireContractCh { - log.Info("start scan trie expired state", "addrHash", item.Addr, "root", item.Root) + log.Debug("start scan trie expired state", "addrHash", item.Addr, "root", item.Root) tr, err := trie.New(&trie.ID{ StateRoot: stateRoot, Owner: item.Addr, @@ -897,12 +897,18 @@ func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, pruneExpiredInDisk ch switch scheme { case rawdb.PathScheme: val := rawdb.ReadTrieNode(diskdb, addr, info.Path, info.Hash, rawdb.PathScheme) + if len(val) == 0 { + log.Warn("cannot find source trie?", "addr", addr, "path", info.Path, "hash", info.Hash) + } trieSize += common.StorageSize(len(val) + 33 + len(info.Path)) rawdb.DeleteTrieNode(batch, addr, info.Path, info.Hash, rawdb.PathScheme) case rawdb.HashScheme: // hbss has shared kv, so using bloom to filter them out. if bloom == nil || !bloom.Contains(stateBloomHasher(info.Hash.Bytes())) { val := rawdb.ReadTrieNode(diskdb, addr, info.Path, info.Hash, rawdb.HashScheme) + if len(val) == 0 { + log.Warn("cannot find source trie?", "addr", addr, "path", info.Path, "hash", info.Hash) + } trieSize += common.StorageSize(len(val) + 33) rawdb.DeleteTrieNode(batch, addr, info.Path, info.Hash, rawdb.HashScheme) } @@ -911,6 +917,9 @@ func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, pruneExpiredInDisk ch if info.IsBranch { epochMetaCount++ val := rawdb.ReadEpochMetaPlainState(diskdb, addr, string(info.Path)) + if len(val) == 0 { + log.Warn("cannot find source epochmeta?", "addr", addr, "path", info.Path, "hash", info.Hash) + } epochMetaSize += common.StorageSize(33 + len(info.Path) + len(val)) rawdb.DeleteEpochMetaPlainState(batch, addr, string(info.Path)) } diff --git a/core/state/snapshot/snapshot_expire.go b/core/state/snapshot/snapshot_expire.go index 29520160b0..7659131a77 100644 --- a/core/state/snapshot/snapshot_expire.go +++ b/core/state/snapshot/snapshot_expire.go @@ -5,6 +5,7 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/log" ) // ShrinkExpiredLeaf tool function for snapshot kv prune @@ -14,13 +15,20 @@ func ShrinkExpiredLeaf(writer ethdb.KeyValueWriter, reader ethdb.KeyValueReader, //cannot prune snapshot in hbss, because it will used for trie prune, but it's ok in pbss. case rawdb.PathScheme: val := rawdb.ReadStorageSnapshot(reader, accountHash, storageHash) + if len(val) == 0 { + log.Warn("cannot find source snapshot?", "addr", accountHash, "path", storageHash) + } valWithEpoch := NewValueWithEpoch(epoch, nil) enc, err := EncodeValueToRLPBytes(valWithEpoch) if err != nil { return 0, err } rawdb.WriteStorageSnapshot(writer, accountHash, storageHash, enc) - return int64(65 + len(val)), nil + shrinkSize := len(val) - len(enc) + if shrinkSize < 0 { + shrinkSize = 0 + } + return int64(shrinkSize), nil } return 0, nil } diff --git a/trie/committer.go b/trie/committer.go index c028cfc151..3669bcda21 100644 --- a/trie/committer.go +++ b/trie/committer.go @@ -142,18 +142,25 @@ func (c *committer) store(path []byte, n node) node { } // Collect the dirty node to nodeset for return. nhash := common.BytesToHash(hash) - c.nodes.AddNode(path, trienode.New(nhash, nodeToBytes(n))) + blob := nodeToBytes(n) + changed := c.tracer.checkNodeChanged(path, blob) + if changed { + c.nodes.AddNode(path, trienode.New(nhash, blob)) + } if c.enableStateExpiry { switch n := n.(type) { case *fullNode: - c.nodes.AddBranchNodeEpochMeta(path, epochmeta.NewBranchNodeEpochMeta(n.EpochMap)) + metaBlob := epochmeta.BranchMeta2Bytes(epochmeta.NewBranchNodeEpochMeta(n.EpochMap)) + if c.tracer.checkEpochMetaChanged(path, metaBlob) { + c.nodes.AddBranchNodeEpochMeta(path, metaBlob) + } } } // Collect the corresponding leaf node if it's required. We don't check // full node since it's impossible to store value in fullNode. The key // length of leaves should be exactly same. - if c.collectLeaf { + if changed && c.collectLeaf { if sn, ok := n.(*shortNode); ok { if val, ok := sn.Val.(valueNode); ok { c.nodes.AddLeaf(nhash, val) diff --git a/trie/epochmeta/database.go b/trie/epochmeta/database.go index 4921451dea..bfd2072303 100644 --- a/trie/epochmeta/database.go +++ b/trie/epochmeta/database.go @@ -74,3 +74,19 @@ func (s *Reader) Get(addr common.Hash, path string) ([]byte, error) { metaAccessMeter.Mark(1) return s.snap.EpochMeta(addr, path) } + +func BranchMeta2Bytes(meta *BranchNodeEpochMeta) []byte { + if meta == nil || *meta == (BranchNodeEpochMeta{}) { + return []byte{} + } + buf := rlp.NewEncoderBuffer(nil) + meta.Encode(buf) + return buf.ToBytes() +} + +func AccountMeta2Bytes(meta types.StateMeta) ([]byte, error) { + if meta == nil { + return []byte{}, nil + } + return meta.EncodeToRLPBytes() +} diff --git a/trie/tracer.go b/trie/tracer.go index 993869db52..e8d5afb3ba 100644 --- a/trie/tracer.go +++ b/trie/tracer.go @@ -17,6 +17,7 @@ package trie import ( + "bytes" "github.com/ethereum/go-ethereum/common" ) @@ -40,19 +41,21 @@ import ( // Note tracer is not thread-safe, callers should be responsible for handling // the concurrency issues by themselves. type tracer struct { - inserts map[string]struct{} - deletes map[string]struct{} - deleteBranchNodes map[string]struct{} // record for epoch meta - accessList map[string][]byte + inserts map[string]struct{} + deletes map[string]struct{} + deleteEpochMetas map[string]struct{} // record for epoch meta + accessList map[string][]byte + accessEpochMetaList map[string][]byte } // newTracer initializes the tracer for capturing trie changes. func newTracer() *tracer { return &tracer{ - inserts: make(map[string]struct{}), - deletes: make(map[string]struct{}), - deleteBranchNodes: make(map[string]struct{}), - accessList: make(map[string][]byte), + inserts: make(map[string]struct{}), + deletes: make(map[string]struct{}), + deleteEpochMetas: make(map[string]struct{}), + accessList: make(map[string][]byte), + accessEpochMetaList: make(map[string][]byte), } } @@ -63,6 +66,11 @@ func (t *tracer) onRead(path []byte, val []byte) { t.accessList[string(path)] = val } +// onReadEpochMeta tracks the newly loaded trie epoch meta +func (t *tracer) onReadEpochMeta(path string, val []byte) { + t.accessEpochMetaList[path] = val +} + // onInsert tracks the newly inserted trie node. If it's already // in the deletion set (resurrected node), then just wipe it from // the deletion set as it's "untouched". @@ -76,8 +84,8 @@ func (t *tracer) onInsert(path []byte) { // onExpandToBranchNode tracks the newly inserted trie branch node. func (t *tracer) onExpandToBranchNode(path []byte) { - if _, present := t.deleteBranchNodes[string(path)]; present { - delete(t.deleteBranchNodes, string(path)) + if _, present := t.deleteEpochMetas[string(path)]; present { + delete(t.deleteEpochMetas, string(path)) } } @@ -94,24 +102,26 @@ func (t *tracer) onDelete(path []byte) { // onDeleteBranchNode tracks the newly deleted trie branch node. func (t *tracer) onDeleteBranchNode(path []byte) { - t.deleteBranchNodes[string(path)] = struct{}{} + t.deleteEpochMetas[string(path)] = struct{}{} } // reset clears the content tracked by tracer. func (t *tracer) reset() { t.inserts = make(map[string]struct{}) t.deletes = make(map[string]struct{}) - t.deleteBranchNodes = make(map[string]struct{}) + t.deleteEpochMetas = make(map[string]struct{}) t.accessList = make(map[string][]byte) + t.accessEpochMetaList = make(map[string][]byte) } // copy returns a deep copied tracer instance. func (t *tracer) copy() *tracer { var ( - inserts = make(map[string]struct{}) - deletes = make(map[string]struct{}) - deleteBranchNodes = make(map[string]struct{}) - accessList = make(map[string][]byte) + inserts = make(map[string]struct{}) + deletes = make(map[string]struct{}) + deleteBranchNodes = make(map[string]struct{}) + accessList = make(map[string][]byte) + accessEpochMetaList = make(map[string][]byte) ) for path := range t.inserts { inserts[path] = struct{}{} @@ -119,17 +129,21 @@ func (t *tracer) copy() *tracer { for path := range t.deletes { deletes[path] = struct{}{} } - for path := range t.deleteBranchNodes { + for path := range t.deleteEpochMetas { deleteBranchNodes[path] = struct{}{} } for path, blob := range t.accessList { accessList[path] = common.CopyBytes(blob) } + for path, blob := range t.accessEpochMetaList { + accessEpochMetaList[path] = common.CopyBytes(blob) + } return &tracer{ - inserts: inserts, - deletes: deletes, - deleteBranchNodes: deleteBranchNodes, - accessList: accessList, + inserts: inserts, + deletes: deletes, + deleteEpochMetas: deleteBranchNodes, + accessList: accessList, + accessEpochMetaList: accessEpochMetaList, } } @@ -152,8 +166,32 @@ func (t *tracer) deletedNodes() []string { // deletedBranchNodes returns a list of branch node paths which are deleted from the trie. func (t *tracer) deletedBranchNodes() []string { var paths []string - for path := range t.deleteBranchNodes { + for path := range t.deleteEpochMetas { + _, ok := t.accessEpochMetaList[path] + if !ok { + continue + } paths = append(paths, path) } return paths } + +// checkNodeChanged check if change for node. +func (t *tracer) checkNodeChanged(path []byte, blob []byte) bool { + val, ok := t.accessList[string(path)] + if !ok { + return len(blob) > 0 + } + + return !bytes.Equal(val, blob) +} + +// checkEpochMetaChanged check if change for epochMeta. +func (t *tracer) checkEpochMetaChanged(path []byte, blob []byte) bool { + val, ok := t.accessEpochMetaList[string(path)] + if !ok { + return len(blob) > 0 + } + + return !bytes.Equal(val, blob) +} diff --git a/trie/trie.go b/trie/trie.go index fe364aac0b..a3bd7ced8a 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -25,6 +25,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" + "github.com/ethereum/go-ethereum/trie/epochmeta" "github.com/ethereum/go-ethereum/trie/trienode" "sync/atomic" ) @@ -107,13 +108,13 @@ func New(id *ID, db *Database) (*Trie, error) { } // resolve root epoch if trie.enableExpiry { - meta, err := reader.accountMeta() - if err != nil { - return nil, err - } - trie.rootEpoch = meta.Epoch() if id.Root != (common.Hash{}) && id.Root != types.EmptyRootHash { trie.root = hashNode(id.Root[:]) + meta, err := trie.resolveAccountMetaAndTrack() + if err != nil { + return nil, err + } + trie.rootEpoch = meta.Epoch() } return trie, nil } @@ -299,7 +300,7 @@ func (t *Trie) getWithEpoch(origNode node, key []byte, pos int, epoch types.Stat return nil, n, true, err } - if err = t.resolveEpochMeta(child, epoch, key[:pos]); err != nil { + if err = t.resolveEpochMetaAndTrack(child, epoch, key[:pos]); err != nil { return nil, n, true, err } value, newnode, _, err := t.getWithEpoch(child, key, pos, epoch, updateEpoch) @@ -671,7 +672,7 @@ func (t *Trie) insertWithEpoch(n node, prefix, key []byte, value node, epoch typ return false, nil, err } - if err = t.resolveEpochMeta(rn, epoch, prefix); err != nil { + if err = t.resolveEpochMetaAndTrack(rn, epoch, prefix); err != nil { return false, nil, err } @@ -987,7 +988,7 @@ func (t *Trie) deleteWithEpoch(n node, prefix, key []byte, epoch types.StateEpoc return false, nil, err } - if err = t.resolveEpochMeta(rn, epoch, prefix); err != nil { + if err = t.resolveEpochMetaAndTrack(rn, epoch, prefix); err != nil { return false, nil, err } @@ -1016,7 +1017,7 @@ func (t *Trie) resolve(n node, prefix []byte, epoch types.StateEpoch) (node, err if err != nil { return nil, err } - if err = t.resolveEpochMeta(n, epoch, prefix); err != nil { + if err = t.resolveEpochMetaAndTrack(n, epoch, prefix); err != nil { return nil, err } return n, nil @@ -1057,11 +1058,48 @@ func (t *Trie) resolveEpochMeta(n node, epoch types.StateEpoch, prefix []byte) e return nil case *fullNode: n.setEpoch(epoch) - meta, err := t.reader.epochMeta(prefix) + enc, err := t.reader.epochMeta(prefix) + if err != nil { + return err + } + if len(enc) > 0 { + meta, err := epochmeta.DecodeFullNodeEpochMeta(enc) + if err != nil { + return err + } + n.EpochMap = meta.EpochMap + } + return nil + case valueNode, hashNode, nil: + // just skip + return nil + default: + return errors.New("resolveShadowNode unsupported node type") + } +} + +// resolveEpochMetaAndTrack resolve full node's epoch map. +func (t *Trie) resolveEpochMetaAndTrack(n node, epoch types.StateEpoch, prefix []byte) error { + if !t.enableExpiry { + return nil + } + // 1. Check if the node is a full node + switch n := n.(type) { + case *shortNode: + n.setEpoch(epoch) + return nil + case *fullNode: + n.setEpoch(epoch) + enc, err := t.reader.epochMeta(prefix) if err != nil { return err } - if meta != nil { + t.tracer.onReadEpochMeta(string(prefix), enc) + if len(enc) > 0 { + meta, err := epochmeta.DecodeFullNodeEpochMeta(enc) + if err != nil { + return err + } n.EpochMap = meta.EpochMap } return nil @@ -1073,6 +1111,22 @@ func (t *Trie) resolveEpochMeta(n node, epoch types.StateEpoch, prefix []byte) e } } +// resolveAccountMetaAndTrack resolve account's epoch map. +func (t *Trie) resolveAccountMetaAndTrack() (types.MetaNoConsensus, error) { + if !t.enableExpiry { + return types.EmptyMetaNoConsensus, nil + } + enc, err := t.reader.accountMeta() + if err != nil { + return types.EmptyMetaNoConsensus, err + } + t.tracer.onReadEpochMeta(epochmeta.AccountMetadataPath, enc) + if len(enc) > 0 { + return types.DecodeMetaNoConsensusFromRLPBytes(enc) + } + return types.EmptyMetaNoConsensus, nil +} + // Hash returns the root hash of the trie. It does not write to the // database and can be used even if the trie doesn't have one. func (t *Trie) Hash() common.Hash { @@ -1135,9 +1189,13 @@ func (t *Trie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet, error) } // store state expiry account meta if t.enableExpiry { - if err := nodes.AddAccountMeta(types.NewMetaNoConsensus(t.rootEpoch)); err != nil { + blob, err := epochmeta.AccountMeta2Bytes(types.NewMetaNoConsensus(t.rootEpoch)) + if err != nil { return common.Hash{}, nil, err } + if t.tracer.checkEpochMetaChanged([]byte(epochmeta.AccountMetadataPath), blob) { + nodes.AddAccountMeta(blob) + } } t.root = newCommitter(nodes, t.tracer, collectLeaf, t.enableExpiry).Commit(t.root) return rootHash, nodes, nil @@ -1296,7 +1354,7 @@ func (t *Trie) tryRevive(n node, key []byte, targetPrefixKey []byte, nub MPTProo if err != nil { return nil, false, err } - if err = t.resolveEpochMeta(child, epoch, key[:pos]); err != nil { + if err = t.resolveEpochMetaAndTrack(child, epoch, key[:pos]); err != nil { return nil, false, err } @@ -1507,7 +1565,7 @@ func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, p if err != nil { return err } - if err = t.resolveEpochMeta(resolve, epoch, path); err != nil { + if err = t.resolveEpochMetaAndTrack(resolve, epoch, path); err != nil { return err } @@ -1566,7 +1624,7 @@ func (t *Trie) recursePruneExpiredNode(n node, path []byte, epoch types.StateEpo if err != nil { return err } - if err = t.resolveEpochMeta(resolve, epoch, path); err != nil { + if err = t.resolveEpochMetaAndTrack(resolve, epoch, path); err != nil { return err } return t.recursePruneExpiredNode(resolve, path, epoch, pruneItemCh) diff --git a/trie/trie_expiry.go b/trie/trie_expiry.go index c284e4b6ae..94ee7927ee 100644 --- a/trie/trie_expiry.go +++ b/trie/trie_expiry.go @@ -61,7 +61,7 @@ func (t *Trie) tryLocalRevive(origNode node, key []byte, pos int, epoch types.St return nil, n, true, err } - if err = t.resolveEpochMeta(child, epoch, key[:pos]); err != nil { + if err = t.resolveEpochMetaAndTrack(child, epoch, key[:pos]); err != nil { return nil, n, true, err } value, newnode, _, err := t.tryLocalRevive(child, key, pos, epoch) diff --git a/trie/trie_reader.go b/trie/trie_reader.go index 24b63c3593..c4c61fc4d2 100644 --- a/trie/trie_reader.go +++ b/trie/trie_reader.go @@ -133,7 +133,7 @@ func (l *trieLoader) OpenStorageTrie(stateRoot common.Hash, addrHash, root commo } // epochMeta resolve from epoch meta storage -func (r *trieReader) epochMeta(path []byte) (*epochmeta.BranchNodeEpochMeta, error) { +func (r *trieReader) epochMeta(path []byte) ([]byte, error) { defer func(start time.Time) { epochMetaTimer.Update(time.Since(start)) }(time.Now()) @@ -146,28 +146,21 @@ func (r *trieReader) epochMeta(path []byte) (*epochmeta.BranchNodeEpochMeta, err if err != nil { return nil, fmt.Errorf("resolve epoch meta err, path: %#x, err: %v", path, err) } - if len(blob) == 0 { - return nil, nil - } - meta, err := epochmeta.DecodeFullNodeEpochMeta(blob) - if err != nil { - return nil, err - } - return meta, nil + return blob, nil } // accountMeta resolve account metadata -func (r *trieReader) accountMeta() (types.MetaNoConsensus, error) { +func (r *trieReader) accountMeta() ([]byte, error) { defer func(start time.Time) { accountMetaTimer.Update(time.Since(start)) }(time.Now()) if r.emReader == nil { - return types.EmptyMetaNoConsensus, errors.New("cannot resolve epoch meta without db for account") + return nil, errors.New("cannot resolve epoch meta without db for account") } blob, err := r.emReader.Get(r.owner, epochmeta.AccountMetadataPath) if err != nil { - return types.EmptyMetaNoConsensus, fmt.Errorf("resolve epoch meta err for account, err: %v", err) + return nil, fmt.Errorf("resolve epoch meta err for account, err: %v", err) } - return types.DecodeMetaNoConsensusFromRLPBytes(blob) + return blob, nil } diff --git a/trie/trienode/node.go b/trie/trienode/node.go index 3b0b71e9e5..ebd13aeae1 100644 --- a/trie/trienode/node.go +++ b/trie/trienode/node.go @@ -18,8 +18,6 @@ package trienode import ( "fmt" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie/epochmeta" "sort" "strings" @@ -105,28 +103,13 @@ func (set *NodeSet) AddNode(path []byte, n *Node) { } // AddBranchNodeEpochMeta adds the provided epoch meta into set. -func (set *NodeSet) AddBranchNodeEpochMeta(path []byte, meta *epochmeta.BranchNodeEpochMeta) { - if meta == nil || *meta == (epochmeta.BranchNodeEpochMeta{}) { - set.EpochMetas[string(path)] = []byte{} - return - } - buf := rlp.NewEncoderBuffer(nil) - meta.Encode(buf) - set.EpochMetas[string(path)] = buf.ToBytes() +func (set *NodeSet) AddBranchNodeEpochMeta(path []byte, blob []byte) { + set.EpochMetas[string(path)] = blob } // AddAccountMeta adds the provided account into set. -func (set *NodeSet) AddAccountMeta(meta types.StateMeta) error { - if meta == nil { - set.EpochMetas[epochmeta.AccountMetadataPath] = []byte{} - return nil - } - enc, err := meta.EncodeToRLPBytes() - if err != nil { - return err - } - set.EpochMetas[epochmeta.AccountMetadataPath] = enc - return nil +func (set *NodeSet) AddAccountMeta(blob []byte) { + set.EpochMetas[epochmeta.AccountMetadataPath] = blob } // Merge adds a set of nodes into the set. From 7a7abb91d162e279ea070eb8cc2b0f10087b8e70 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:59:25 +0800 Subject: [PATCH 51/65] pruner: opt expired prune, add more logs; --- core/state/pruner/pruner.go | 51 ++++++++++++++++---------- core/state/snapshot/snapshot_expire.go | 3 +- trie/epochmeta/disklayer.go | 2 +- trie/inspect_trie.go | 2 +- trie/proof.go | 12 ++++-- trie/trie.go | 46 ++++++++++++----------- trie/triedb/pathdb/difflayer.go | 2 +- trie/triedb/pathdb/disklayer.go | 4 +- trie/triedb/pathdb/nodebuffer.go | 2 +- 9 files changed, 74 insertions(+), 50 deletions(-) diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go index 3758793207..dfd5ab280a 100644 --- a/core/state/pruner/pruner.go +++ b/core/state/pruner/pruner.go @@ -64,6 +64,8 @@ const ( // triggering range compaction. It's a quite arbitrary number but just // to avoid triggering range compaction because of small deletion. rangeCompactionThreshold = 100000 + + FixedPrefixAndAddrSize = 33 ) // Config includes all the configurations for pruning. @@ -136,6 +138,9 @@ func NewPruner(db ethdb.Database, config Config, triesInMemory uint64) (*Pruner, flattenBlockHash := rawdb.ReadCanonicalHash(db, headBlock.NumberU64()-triesInMemory) flattenBlock := rawdb.ReadHeader(db, flattenBlockHash, headBlock.NumberU64()-triesInMemory) + if flattenBlock == nil { + return nil, fmt.Errorf("cannot find %v depth block, it cannot prune", triesInMemory) + } return &Pruner{ config: config, chainHeader: headBlock.Header(), @@ -831,7 +836,7 @@ func asyncScanUnExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch ty return err } if time.Since(logged) > 8*time.Second { - log.Info("Pruning expired states", "trieNodes", trieCount.Load()) + log.Info("Scan unexpired states", "trieNodes", trieCount.Load()) logged = time.Now() } } @@ -866,7 +871,7 @@ func asyncScanExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch type return err } if time.Since(logged) > 8*time.Second { - log.Info("Pruning expired states", "trieNodes", trieCount.Load()) + log.Info("Scan unexpired states", "trieNodes", trieCount.Load()) logged = time.Now() } } @@ -877,6 +882,7 @@ func asyncScanExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch type func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, pruneExpiredInDisk chan *trie.NodeInfo, bloom *bloomfilter.Filter, scheme string) error { var ( + itemCount = 0 trieCount = 0 epochMetaCount = 0 snapCount = 0 @@ -891,46 +897,53 @@ func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, pruneExpiredInDisk ch log.Debug("found expired state", "addr", info.Addr, "path", hex.EncodeToString(info.Path), "epoch", info.Epoch, "isBranch", info.IsBranch, "isLeaf", info.IsLeaf) + itemCount++ addr := info.Addr - // delete trie kv - trieCount++ switch scheme { case rawdb.PathScheme: val := rawdb.ReadTrieNode(diskdb, addr, info.Path, info.Hash, rawdb.PathScheme) if len(val) == 0 { - log.Warn("cannot find source trie?", "addr", addr, "path", info.Path, "hash", info.Hash) + log.Debug("cannot find source trie?", "addr", addr, "path", info.Path, "hash", info.Hash, "epoch", info.Epoch) + } else { + trieCount++ + trieSize += common.StorageSize(len(val) + FixedPrefixAndAddrSize + len(info.Path)) + rawdb.DeleteTrieNode(batch, addr, info.Path, info.Hash, rawdb.PathScheme) } - trieSize += common.StorageSize(len(val) + 33 + len(info.Path)) - rawdb.DeleteTrieNode(batch, addr, info.Path, info.Hash, rawdb.PathScheme) case rawdb.HashScheme: // hbss has shared kv, so using bloom to filter them out. if bloom == nil || !bloom.Contains(stateBloomHasher(info.Hash.Bytes())) { val := rawdb.ReadTrieNode(diskdb, addr, info.Path, info.Hash, rawdb.HashScheme) if len(val) == 0 { - log.Warn("cannot find source trie?", "addr", addr, "path", info.Path, "hash", info.Hash) + log.Debug("cannot find source trie?", "addr", addr, "path", info.Path, "hash", info.Hash, "epoch", info.Epoch) + } else { + trieCount++ + trieSize += common.StorageSize(len(val) + FixedPrefixAndAddrSize) + rawdb.DeleteTrieNode(batch, addr, info.Path, info.Hash, rawdb.HashScheme) } - trieSize += common.StorageSize(len(val) + 33) - rawdb.DeleteTrieNode(batch, addr, info.Path, info.Hash, rawdb.HashScheme) } } // delete epoch meta if info.IsBranch { - epochMetaCount++ val := rawdb.ReadEpochMetaPlainState(diskdb, addr, string(info.Path)) - if len(val) == 0 { - log.Warn("cannot find source epochmeta?", "addr", addr, "path", info.Path, "hash", info.Hash) + if len(val) == 0 && info.Epoch > types.StateEpoch0 { + log.Debug("cannot find source epochmeta?", "addr", addr, "path", info.Path, "hash", info.Hash, "epoch", info.Epoch) + } + if len(val) > 0 { + epochMetaCount++ + epochMetaSize += common.StorageSize(FixedPrefixAndAddrSize + len(info.Path) + len(val)) + rawdb.DeleteEpochMetaPlainState(batch, addr, string(info.Path)) } - epochMetaSize += common.StorageSize(33 + len(info.Path) + len(val)) - rawdb.DeleteEpochMetaPlainState(batch, addr, string(info.Path)) } // replace snapshot kv only epoch if info.IsLeaf { - snapCount++ size, err := snapshot.ShrinkExpiredLeaf(batch, diskdb, addr, info.Key, info.Epoch, scheme) if err != nil { log.Error("ShrinkExpiredLeaf err", "addr", addr, "key", info.Key, "err", err) } - snapSize += common.StorageSize(size) + if size > 0 { + snapCount++ + snapSize += common.StorageSize(size) + } } if batch.ValueSize() >= ethdb.IdealBatchSize { if err := batch.Write(); err != nil { @@ -939,7 +952,7 @@ func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, pruneExpiredInDisk ch batch.Reset() } if time.Since(logged) > 8*time.Second { - log.Info("Pruning expired states", "trieNodes", trieCount, "trieSize", trieSize, + log.Info("Pruning expired states", "items", itemCount, "trieNodes", trieCount, "trieSize", trieSize, "SnapKV", snapCount, "SnapKVSize", snapSize, "EpochMeta", epochMetaCount, "EpochMetaSize", epochMetaSize) logged = time.Now() @@ -951,7 +964,7 @@ func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, pruneExpiredInDisk ch } batch.Reset() } - log.Info("Pruned expired states", "trieNodes", trieCount, "trieSize", trieSize, + log.Info("Pruned expired states", "items", itemCount, "trieNodes", trieCount, "trieSize", trieSize, "SnapKV", snapCount, "SnapKVSize", snapSize, "EpochMeta", epochMetaCount, "EpochMetaSize", epochMetaSize, "elapsed", common.PrettyDuration(time.Since(start))) // Start compactions, will remove the deleted data from the disk immediately. diff --git a/core/state/snapshot/snapshot_expire.go b/core/state/snapshot/snapshot_expire.go index 7659131a77..6659494914 100644 --- a/core/state/snapshot/snapshot_expire.go +++ b/core/state/snapshot/snapshot_expire.go @@ -16,7 +16,8 @@ func ShrinkExpiredLeaf(writer ethdb.KeyValueWriter, reader ethdb.KeyValueReader, case rawdb.PathScheme: val := rawdb.ReadStorageSnapshot(reader, accountHash, storageHash) if len(val) == 0 { - log.Warn("cannot find source snapshot?", "addr", accountHash, "path", storageHash) + log.Warn("cannot find source snapshot?", "addr", accountHash, "key", storageHash, "epoch", epoch) + return 0, nil } valWithEpoch := NewValueWithEpoch(epoch, nil) enc, err := EncodeValueToRLPBytes(valWithEpoch) diff --git a/trie/epochmeta/disklayer.go b/trie/epochmeta/disklayer.go index ac82dc441f..cc7894736c 100644 --- a/trie/epochmeta/disklayer.go +++ b/trie/epochmeta/disklayer.go @@ -14,7 +14,7 @@ import ( ) const ( - defaultDiskLayerCacheSize = 100000 + defaultDiskLayerCacheSize = 1024000 ) type diskLayer struct { diff --git a/trie/inspect_trie.go b/trie/inspect_trie.go index d00c6fde75..64c9bedd6e 100644 --- a/trie/inspect_trie.go +++ b/trie/inspect_trie.go @@ -236,7 +236,7 @@ func (inspect *Inspector) ConcurrentTraversal(theTrie *Trie, theTrieTreeStat *Tr } if len(inspect.concurrentQueue)*2 < cap(inspect.concurrentQueue) { inspect.wg.Add(1) - go inspect.SubConcurrentTraversal(theTrie, theTrieTreeStat, child, height+1, copyNewSlice(path, []byte{byte(idx)})) + go inspect.SubConcurrentTraversal(theTrie, theTrieTreeStat, child, height+1, copy2NewBytes(path, []byte{byte(idx)})) } else { inspect.ConcurrentTraversal(theTrie, theTrieTreeStat, child, height+1, append(path, byte(idx))) } diff --git a/trie/proof.go b/trie/proof.go index f4c784c143..6b8a080a1e 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -919,7 +919,7 @@ func (m *MPTProofCache) VerifyProof() error { prefix := m.RootKeyHex for i := 0; i < len(m.cacheNodes); i++ { if i-1 >= 0 { - prefix = copyNewSlice(prefix, m.cacheHexPath[i-1]) + prefix = copy2NewBytes(prefix, m.cacheHexPath[i-1]) } // prefix = append(prefix, m.cacheHexPath[i]...) n1 := m.cacheNodes[i] @@ -938,7 +938,7 @@ func (m *MPTProofCache) VerifyProof() error { } if merge { i++ - prefix = copyNewSlice(prefix, m.cacheHexPath[i-1]) + prefix = copy2NewBytes(prefix, m.cacheHexPath[i-1]) nub.n2 = m.cacheNodes[i] nub.n2PrefixKey = prefix } @@ -948,13 +948,19 @@ func (m *MPTProofCache) VerifyProof() error { return nil } -func copyNewSlice(s1, s2 []byte) []byte { +func copy2NewBytes(s1, s2 []byte) []byte { ret := make([]byte, len(s1)+len(s2)) copy(ret, s1) copy(ret[len(s1):], s2) return ret } +func renewBytes(s []byte) []byte { + ret := make([]byte, len(s)) + copy(ret, s) + return ret +} + func (m *MPTProofCache) CacheNubs() []*MPTProofNub { return m.cacheNubs } diff --git a/trie/trie.go b/trie/trie.go index a3bd7ced8a..0a401ee95e 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -1582,26 +1582,34 @@ func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, p func (t *Trie) recursePruneExpiredNode(n node, path []byte, epoch types.StateEpoch, pruneItemCh chan *NodeInfo) error { switch n := n.(type) { case *shortNode: + subPath := append(path, n.Key...) key := common.Hash{} - _, ok := n.Val.(valueNode) - if ok { - key = common.BytesToHash(hexToKeybytes(append(path, n.Key...))) + _, isLeaf := n.Val.(valueNode) + if isLeaf { + key = common.BytesToHash(hexToKeybytes(subPath)) } - err := t.recursePruneExpiredNode(n.Val, append(path, n.Key...), epoch, pruneItemCh) - if err != nil { - return err - } - // prune child first pruneItemCh <- &NodeInfo{ Addr: t.owner, Hash: common.BytesToHash(n.flags.hash), - Path: path, + Path: renewBytes(path), Key: key, Epoch: epoch, - IsLeaf: ok, + IsLeaf: isLeaf, + } + + err := t.recursePruneExpiredNode(n.Val, subPath, epoch, pruneItemCh) + if err != nil { + return err } return nil case *fullNode: + pruneItemCh <- &NodeInfo{ + Addr: t.owner, + Hash: common.BytesToHash(n.flags.hash), + Path: renewBytes(path), + Epoch: epoch, + IsBranch: true, + } // recurse child, and except valueNode for i := 0; i < BranchNodeLength-1; i++ { err := t.recursePruneExpiredNode(n.Children[i], append(path, byte(i)), n.EpochMap[i], pruneItemCh) @@ -1609,25 +1617,21 @@ func (t *Trie) recursePruneExpiredNode(n node, path []byte, epoch types.StateEpo return err } } - // prune child first - pruneItemCh <- &NodeInfo{ - Addr: t.owner, - Hash: common.BytesToHash(n.flags.hash), - Path: path, - Epoch: epoch, - IsBranch: true, - } return nil case hashNode: // hashNode is a index of trie node storage, need not prune. - resolve, err := t.resolveAndTrack(n, path) + rn, err := t.resolveAndTrack(n, path) + // if touch miss node, just skip + if _, ok := err.(*MissingNodeError); ok { + return nil + } if err != nil { return err } - if err = t.resolveEpochMetaAndTrack(resolve, epoch, path); err != nil { + if err = t.resolveEpochMetaAndTrack(rn, epoch, path); err != nil { return err } - return t.recursePruneExpiredNode(resolve, path, epoch, pruneItemCh) + return t.recursePruneExpiredNode(rn, path, epoch, pruneItemCh) case valueNode: // value node is not a single storage uint, so pass to prune. return nil diff --git a/trie/triedb/pathdb/difflayer.go b/trie/triedb/pathdb/difflayer.go index bea0208461..4ffe0bee2f 100644 --- a/trie/triedb/pathdb/difflayer.go +++ b/trie/triedb/pathdb/difflayer.go @@ -113,7 +113,7 @@ func (dl *diffLayer) node(owner common.Hash, path []byte, hash common.Hash, dept // bubble up an error here. It shouldn't happen at all. if n.Hash != hash { dirtyFalseMeter.Mark(1) - log.Error("Unexpected trie node in diff layer", "root", dl.root, "owner", owner, "path", path, "expect", hash, "got", n.Hash) + log.Debug("Unexpected trie node in diff layer", "root", dl.root, "owner", owner, "path", path, "expect", hash, "got", n.Hash) return nil, newUnexpectedNodeError("diff", hash, n.Hash, owner, path) } dirtyHitMeter.Mark(1) diff --git a/trie/triedb/pathdb/disklayer.go b/trie/triedb/pathdb/disklayer.go index 87718290f9..80da6db7da 100644 --- a/trie/triedb/pathdb/disklayer.go +++ b/trie/triedb/pathdb/disklayer.go @@ -133,7 +133,7 @@ func (dl *diskLayer) Node(owner common.Hash, path []byte, hash common.Hash) ([]b return blob, nil } cleanFalseMeter.Mark(1) - log.Error("Unexpected trie node in clean cache", "owner", owner, "path", path, "expect", hash, "got", got) + log.Debug("Unexpected trie node in clean cache", "owner", owner, "path", path, "expect", hash, "got", got) } cleanMissMeter.Mark(1) } @@ -149,7 +149,7 @@ func (dl *diskLayer) Node(owner common.Hash, path []byte, hash common.Hash) ([]b } if nHash != hash { diskFalseMeter.Mark(1) - log.Error("Unexpected trie node in disk", "owner", owner, "path", path, "expect", hash, "got", nHash) + log.Debug("Unexpected trie node in disk", "owner", owner, "path", path, "expect", hash, "got", nHash) return nil, newUnexpectedNodeError("disk", hash, nHash, owner, path) } if dl.cleans != nil && len(nBlob) > 0 { diff --git a/trie/triedb/pathdb/nodebuffer.go b/trie/triedb/pathdb/nodebuffer.go index 67de225b04..42d06bd6dd 100644 --- a/trie/triedb/pathdb/nodebuffer.go +++ b/trie/triedb/pathdb/nodebuffer.go @@ -70,7 +70,7 @@ func (b *nodebuffer) node(owner common.Hash, path []byte, hash common.Hash) (*tr } if n.Hash != hash { dirtyFalseMeter.Mark(1) - log.Error("Unexpected trie node in node buffer", "owner", owner, "path", path, "expect", hash, "got", n.Hash) + log.Debug("Unexpected trie node in node buffer", "owner", owner, "path", path, "expect", hash, "got", n.Hash) return nil, newUnexpectedNodeError("dirty", hash, n.Hash, owner, path) } return n, nil From 5a5f371a0efc09eda9a740b8a8f80d8274b2d9fc Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Tue, 17 Oct 2023 14:30:51 +0800 Subject: [PATCH 52/65] state: add some revive metrics; --- core/state/state_expiry.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index 1f90760a43..8d1e3b9bbf 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -13,7 +13,10 @@ import ( ) var ( - reviveStorageTrieTimer = metrics.NewRegisteredTimer("state/revivetrie/rt", nil) + reviveTrieTimer = metrics.NewRegisteredTimer("state/revivetrie/rt", nil) + reviveTrieMeter = metrics.NewRegisteredMeter("state/revivetrie", nil) + reviveFromLocalMeter = metrics.NewRegisteredMeter("state/revivetrie/local", nil) + reviveFromRemoteMeter = metrics.NewRegisteredMeter("state/revivetrie/remote", nil) ) // stateExpiryMeta it contains all state expiry meta for target block @@ -33,6 +36,7 @@ func defaultStateExpiryMeta() *stateExpiryMeta { // fetchExpiredStorageFromRemote request expired state from remote full state node; func fetchExpiredStorageFromRemote(meta *stateExpiryMeta, addr common.Address, root common.Hash, tr Trie, prefixKey []byte, key common.Hash) (map[string][]byte, error) { log.Debug("fetching expired storage from remoteDB", "addr", addr, "prefix", prefixKey, "key", key) + reviveTrieMeter.Mark(1) if meta.enableLocalRevive { // if there need revive expired state, try to revive locally, when the node is not being pruned, just renew the epoch val, err := tr.TryLocalRevive(addr, key.Bytes()) @@ -44,6 +48,7 @@ func fetchExpiredStorageFromRemote(meta *stateExpiryMeta, addr common.Address, r case *trie.MissingNodeError: // cannot revive locally, request from remote case nil: + reviveFromLocalMeter.Mark(1) ret := make(map[string][]byte, 1) ret[key.String()] = val return ret, nil @@ -52,6 +57,7 @@ func fetchExpiredStorageFromRemote(meta *stateExpiryMeta, addr common.Address, r } } + reviveFromRemoteMeter.Mark(1) // cannot revive locally, fetch remote proof proofs, err := meta.fullStateDB.GetStorageReviveProof(meta.originalRoot, addr, root, []string{common.Bytes2Hex(prefixKey)}, []string{common.Bytes2Hex(key[:])}) log.Debug("fetchExpiredStorageFromRemote GetStorageReviveProof", "addr", addr, "key", key, "proofs", len(proofs), "err", err) @@ -69,7 +75,7 @@ func fetchExpiredStorageFromRemote(meta *stateExpiryMeta, addr common.Address, r // batchFetchExpiredStorageFromRemote request expired state from remote full state node with a list of keys and prefixes. func batchFetchExpiredFromRemote(expiryMeta *stateExpiryMeta, addr common.Address, root common.Hash, tr Trie, prefixKeys [][]byte, keys []common.Hash) ([]map[string][]byte, error) { - + reviveTrieMeter.Mark(int64(len(keys))) ret := make([]map[string][]byte, len(keys)) prefixKeysStr := make([]string, len(prefixKeys)) keysStr := make([]string, len(keys)) @@ -95,7 +101,7 @@ func batchFetchExpiredFromRemote(expiryMeta *stateExpiryMeta, addr common.Addres return nil, err } } - + reviveFromLocalMeter.Mark(int64(len(keys) - len(expiredKeys))) for i, prefix := range expiredPrefixKeys { prefixKeysStr[i] = common.Bytes2Hex(prefix) } @@ -117,6 +123,7 @@ func batchFetchExpiredFromRemote(expiryMeta *stateExpiryMeta, addr common.Addres } // cannot revive locally, fetch remote proof + reviveFromRemoteMeter.Mark(int64(len(keysStr))) proofs, err := expiryMeta.fullStateDB.GetStorageReviveProof(expiryMeta.originalRoot, addr, root, prefixKeysStr, keysStr) log.Debug("fetchExpiredStorageFromRemote GetStorageReviveProof", "addr", addr, "keys", keysStr, "prefixKeys", prefixKeysStr, "proofs", len(proofs), "err", err) if err != nil { @@ -144,7 +151,7 @@ func batchFetchExpiredFromRemote(expiryMeta *stateExpiryMeta, addr common.Addres // ReviveStorageTrie revive trie's expired state from proof func ReviveStorageTrie(addr common.Address, tr Trie, proof types.ReviveStorageProof, targetKey common.Hash) (map[string][]byte, error) { defer func(start time.Time) { - reviveStorageTrieTimer.Update(time.Since(start)) + reviveTrieTimer.Update(time.Since(start)) }(time.Now()) // Decode keys and proofs From 74169f72d8f0ec36a9ddd478f2400e246fec6847 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Tue, 17 Oct 2023 16:54:13 +0800 Subject: [PATCH 53/65] fix: revert to master change; --- core/blockchain.go | 21 ++++++++++----------- eth/backend.go | 2 +- trie/inspect_trie.go | 4 ---- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 48932115a6..5e2c7fe7a8 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1255,20 +1255,19 @@ func (bc *BlockChain) Stop() { log.Error("Dangling trie nodes after full cleanup") } } - - epochMetaSnap := bc.triedb.EpochMetaSnapTree() - if epochMetaSnap != nil { - if err := epochMetaSnap.Journal(); err != nil { - log.Error("Failed to journal epochMetaSnapTree", "err", err) - } + } + epochMetaSnap := bc.triedb.EpochMetaSnapTree() + if epochMetaSnap != nil { + if err := epochMetaSnap.Journal(); err != nil { + log.Error("Failed to journal epochMetaSnapTree", "err", err) } + } - // Flush the collected preimages to disk - if err := bc.stateCache.TrieDB().Close(); err != nil { - log.Error("Failed to close trie db", "err", err) - } - log.Info("Blockchain stopped") + // Close the trie database, release all the held resources as the last step. + if err := bc.triedb.Close(); err != nil { + log.Error("Failed to close trie database", "err", err) } + log.Info("Blockchain stopped") } // StopInsert interrupts all insertion methods, causing them to return diff --git a/eth/backend.go b/eth/backend.go index d2977fed36..19c6c094ec 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -276,7 +276,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { DirectBroadcast: config.DirectBroadcast, DisablePeerTxBroadcast: config.DisablePeerTxBroadcast, PeerSet: peers, - EnableStateExpiry: config.StateExpiryCfg.Enable, + EnableStateExpiry: config.StateExpiryCfg.EnableExpiry(), }); err != nil { return nil, err } diff --git a/trie/inspect_trie.go b/trie/inspect_trie.go index 64c9bedd6e..a6a9df7b7d 100644 --- a/trie/inspect_trie.go +++ b/trie/inspect_trie.go @@ -20,10 +20,6 @@ import ( "github.com/olekukonko/tablewriter" ) -const ( - DEFAULT_TRIEDBCACHE_SIZE = 1024 * 1024 * 1024 -) - type Account struct { Nonce uint64 Balance *big.Int From e98a61946bfb2b70eb1a13c5ed285491d370123f Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Wed, 18 Oct 2023 15:00:43 +0800 Subject: [PATCH 54/65] trie/pathdb: support account meta/epoch meta query; trie/trie: merge epoch meta & trie store in PBSS; --- core/rawdb/accessors_trie.go | 8 +- core/state/pruner/pruner.go | 8 +- core/state/snapshot/snapshot_expire.go | 2 +- core/types/typed_trie_node.go | 100 ++++++++++++++++++++ trie/committer.go | 9 +- trie/database.go | 13 ++- trie/epochmeta/database.go | 15 ++- trie/node.go | 29 +++++- trie/node_enc.go | 22 +++++ trie/tracer.go | 4 +- trie/trie.go | 99 +++++++++++-------- trie/trie_reader.go | 4 +- trie/triedb/pathdb/difflayer.go | 3 +- trie/triedb/pathdb/disklayer.go | 5 +- trie/triedb/pathdb/journal.go | 12 ++- trie/triedb/pathdb/nodebuffer.go | 3 +- trie/trienode/node.go | 2 +- trie/typed_trie_node_test.go | 126 +++++++++++++++++++++++++ 18 files changed, 399 insertions(+), 65 deletions(-) create mode 100644 core/types/typed_trie_node.go create mode 100644 trie/typed_trie_node_test.go diff --git a/core/rawdb/accessors_trie.go b/core/rawdb/accessors_trie.go index f5c2f8899a..3cbffc64df 100644 --- a/core/rawdb/accessors_trie.go +++ b/core/rawdb/accessors_trie.go @@ -18,6 +18,7 @@ package rawdb import ( "fmt" + "github.com/ethereum/go-ethereum/core/types" "sync" "github.com/ethereum/go-ethereum/common" @@ -110,9 +111,14 @@ func ReadStorageTrieNode(db ethdb.KeyValueReader, accountHash common.Hash, path if err != nil { return nil, common.Hash{} } + + raw, err := types.DecodeTypedTrieNodeRaw(data) + if err != nil { + panic(fmt.Errorf("ReadStorageTrieNode err, %v", err)) + } h := newHasher() defer h.release() - return data, h.hash(data) + return data, h.hash(raw) } // HasStorageTrieNode checks the storage trie node presence with the provided diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go index dfd5ab280a..caeb1a2460 100644 --- a/core/state/pruner/pruner.go +++ b/core/state/pruner/pruner.go @@ -849,6 +849,9 @@ func asyncScanUnExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch ty // here are some issues when just delete it from hash-based storage, because it's shared kv in hbss // but it's ok for pbss. func asyncScanExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch types.StateEpoch, expireContractCh chan *snapshot.ContractItem, pruneExpiredInDisk chan *trie.NodeInfo) error { + defer func() { + close(pruneExpiredInDisk) + }() var ( trieCount atomic.Uint64 start = time.Now() @@ -876,7 +879,6 @@ func asyncScanExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch type } } log.Info("Scan unexpired states", "trieNodes", trieCount.Load(), "elapsed", common.PrettyDuration(time.Since(start))) - close(pruneExpiredInDisk) return nil } @@ -922,8 +924,8 @@ func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, pruneExpiredInDisk ch } } } - // delete epoch meta - if info.IsBranch { + // delete epoch meta in HBSS + if info.IsBranch && rawdb.HashScheme == scheme { val := rawdb.ReadEpochMetaPlainState(diskdb, addr, string(info.Path)) if len(val) == 0 && info.Epoch > types.StateEpoch0 { log.Debug("cannot find source epochmeta?", "addr", addr, "path", info.Path, "hash", info.Hash, "epoch", info.Epoch) diff --git a/core/state/snapshot/snapshot_expire.go b/core/state/snapshot/snapshot_expire.go index 6659494914..769e0e3c60 100644 --- a/core/state/snapshot/snapshot_expire.go +++ b/core/state/snapshot/snapshot_expire.go @@ -16,7 +16,7 @@ func ShrinkExpiredLeaf(writer ethdb.KeyValueWriter, reader ethdb.KeyValueReader, case rawdb.PathScheme: val := rawdb.ReadStorageSnapshot(reader, accountHash, storageHash) if len(val) == 0 { - log.Warn("cannot find source snapshot?", "addr", accountHash, "key", storageHash, "epoch", epoch) + log.Debug("cannot find source snapshot?", "addr", accountHash, "key", storageHash, "epoch", epoch) return 0, nil } valWithEpoch := NewValueWithEpoch(epoch, nil) diff --git a/core/types/typed_trie_node.go b/core/types/typed_trie_node.go new file mode 100644 index 0000000000..f7ac2ea044 --- /dev/null +++ b/core/types/typed_trie_node.go @@ -0,0 +1,100 @@ +package types + +import ( + "bytes" + "errors" + "github.com/ethereum/go-ethereum/rlp" +) + +const ( + TrieNodeRawType = iota + TrieBranchNodeWithEpochType +) + +var ( + ErrTypedNodeNotSupport = errors.New("the typed node not support now") +) + +type TypedTrieNode interface { + Type() uint8 + EncodeToRLPBytes(buf *rlp.EncoderBuffer) +} + +type TrieNodeRaw []byte + +func (n TrieNodeRaw) Type() uint8 { + return TrieNodeRawType +} + +func (n TrieNodeRaw) EncodeToRLPBytes(buf *rlp.EncoderBuffer) { +} + +type TrieBranchNodeWithEpoch struct { + EpochMap [16]StateEpoch + Blob []byte +} + +func (n *TrieBranchNodeWithEpoch) Type() uint8 { + return TrieBranchNodeWithEpochType +} + +func (n *TrieBranchNodeWithEpoch) EncodeToRLPBytes(buf *rlp.EncoderBuffer) { + rlp.Encode(buf, n) +} + +func DecodeTrieBranchNodeWithEpoch(enc []byte) (*TrieBranchNodeWithEpoch, error) { + var n TrieBranchNodeWithEpoch + if err := rlp.DecodeBytes(enc, &n); err != nil { + return nil, err + } + return &n, nil +} + +func EncodeTypedTrieNode(val TypedTrieNode) []byte { + switch raw := val.(type) { + case TrieNodeRaw: + return raw + } + // encode with type prefix + buf := bytes.NewBuffer(make([]byte, 0, 40)) + buf.WriteByte(val.Type()) + encoder := rlp.NewEncoderBuffer(buf) + val.EncodeToRLPBytes(&encoder) + // it cannot be error here. + encoder.Flush() + return buf.Bytes() +} + +func DecodeTypedTrieNode(enc []byte) (TypedTrieNode, error) { + if len(enc) == 0 { + return TrieNodeRaw{}, nil + } + if len(enc) == 1 || enc[0] > 0x7f { + return TrieNodeRaw(enc), nil + } + switch enc[0] { + case TrieBranchNodeWithEpochType: + return DecodeTrieBranchNodeWithEpoch(enc[1:]) + default: + return nil, ErrTypedNodeNotSupport + } +} + +func DecodeTypedTrieNodeRaw(enc []byte) ([]byte, error) { + if len(enc) == 0 { + return enc, nil + } + if len(enc) == 1 || enc[0] > 0x7f { + return enc, nil + } + switch enc[0] { + case TrieBranchNodeWithEpochType: + rn, err := DecodeTrieBranchNodeWithEpoch(enc[1:]) + if err != nil { + return nil, err + } + return rn.Blob, nil + default: + return nil, ErrTypedNodeNotSupport + } +} diff --git a/trie/committer.go b/trie/committer.go index 3669bcda21..1980475408 100644 --- a/trie/committer.go +++ b/trie/committer.go @@ -32,15 +32,17 @@ type committer struct { tracer *tracer collectLeaf bool enableStateExpiry bool + enableMetaDB bool } // newCommitter creates a new committer or picks one from the pool. -func newCommitter(nodeset *trienode.NodeSet, tracer *tracer, collectLeaf bool, enableStateExpiry bool) *committer { +func newCommitter(nodeset *trienode.NodeSet, tracer *tracer, collectLeaf, enableStateExpiry, enableMetaDB bool) *committer { return &committer{ nodes: nodeset, tracer: tracer, collectLeaf: collectLeaf, enableStateExpiry: enableStateExpiry, + enableMetaDB: enableMetaDB, } } @@ -143,11 +145,14 @@ func (c *committer) store(path []byte, n node) node { // Collect the dirty node to nodeset for return. nhash := common.BytesToHash(hash) blob := nodeToBytes(n) + if c.enableStateExpiry && !c.enableMetaDB { + blob = nodeToBytesWithEpoch(n, blob) + } changed := c.tracer.checkNodeChanged(path, blob) if changed { c.nodes.AddNode(path, trienode.New(nhash, blob)) } - if c.enableStateExpiry { + if c.enableStateExpiry && c.enableMetaDB { switch n := n.(type) { case *fullNode: metaBlob := epochmeta.BranchMeta2Bytes(epochmeta.NewBranchNodeEpochMeta(n.EpochMap)) diff --git a/trie/database.go b/trie/database.go index f313b10e78..904b485119 100644 --- a/trie/database.go +++ b/trie/database.go @@ -146,10 +146,12 @@ func NewDatabase(diskdb ethdb.Database, config *Config) *Database { * 2. Second, initialize the db according to the scheme already used by db * 3. Last, use the default scheme, namely hash scheme */ + enableEpochMetaDB := false if config.HashDB != nil { if rawdb.ReadStateScheme(diskdb) == rawdb.PathScheme { log.Warn("incompatible state scheme", "old", rawdb.PathScheme, "new", rawdb.HashScheme) } + enableEpochMetaDB = true db.backend = hashdb.New(diskdb, config.HashDB, mptResolver{}) } else if config.PathDB != nil { if rawdb.ReadStateScheme(diskdb) == rawdb.HashScheme { @@ -165,9 +167,10 @@ func NewDatabase(diskdb ethdb.Database, config *Config) *Database { if config.HashDB == nil { config.HashDB = hashdb.Defaults } + enableEpochMetaDB = true db.backend = hashdb.New(diskdb, config.HashDB, mptResolver{}) } - if config != nil && config.EnableStateExpiry { + if config.EnableStateExpiry && enableEpochMetaDB { snapTree, err := epochmeta.NewEpochMetaSnapTree(diskdb, config.EpochMeta) if err != nil { panic(fmt.Sprintf("init SnapshotTree err: %v", err)) @@ -248,6 +251,14 @@ func (db *Database) CommitEpochMeta(root common.Hash) error { return nil } +func (db *Database) EnableExpiry() bool { + if db.config != nil { + return db.config.EnableStateExpiry + } + + return false +} + // Size returns the storage size of dirty trie nodes in front of the persistent // database and the size of cached preimages. func (db *Database) Size() (common.StorageSize, common.StorageSize) { diff --git a/trie/epochmeta/database.go b/trie/epochmeta/database.go index bfd2072303..8feb858991 100644 --- a/trie/epochmeta/database.go +++ b/trie/epochmeta/database.go @@ -1,6 +1,7 @@ package epochmeta import ( + "bytes" "fmt" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" @@ -12,11 +13,8 @@ import ( "github.com/ethereum/go-ethereum/core/types" ) -const ( - AccountMetadataPath = "m" -) - var ( + AccountMetadataPath = []byte("m") metaAccessMeter = metrics.NewRegisteredMeter("epochmeta/access", nil) metaHitDiffMeter = metrics.NewRegisteredMeter("epochmeta/access/hit/diff", nil) metaHitDiskCacheMeter = metrics.NewRegisteredMeter("epochmeta/access/hit/diskcache", nil) @@ -90,3 +88,12 @@ func AccountMeta2Bytes(meta types.StateMeta) ([]byte, error) { } return meta.EncodeToRLPBytes() } + +// IsEpochMetaPath add some skip hash check rule +func IsEpochMetaPath(path []byte) bool { + if bytes.Equal(AccountMetadataPath, path) { + return true + } + + return false +} diff --git a/trie/node.go b/trie/node.go index 4e36ef0fd7..235344bcca 100644 --- a/trie/node.go +++ b/trie/node.go @@ -213,10 +213,10 @@ func mustDecodeNode(hash, buf []byte) node { return n } -// mustDecodeNodeUnsafe is a wrapper of decodeNodeUnsafe and panic if any error is +// mustDecodeNodeUnsafe is a wrapper of decodeTypedNodeUnsafe and panic if any error is // encountered. func mustDecodeNodeUnsafe(hash, buf []byte) node { - n, err := decodeNodeUnsafe(hash, buf) + n, err := decodeTypedNodeUnsafe(hash, buf) if err != nil { panic(fmt.Sprintf("node %x: %v", hash, err)) } @@ -229,7 +229,30 @@ func mustDecodeNodeUnsafe(hash, buf []byte) node { // scenarios with low performance requirements and hard to determine whether the // byte slice be modified or not. func decodeNode(hash, buf []byte) (node, error) { - return decodeNodeUnsafe(hash, common.CopyBytes(buf)) + return decodeTypedNodeUnsafe(hash, common.CopyBytes(buf)) +} + +func decodeTypedNodeUnsafe(hash, buf []byte) (node, error) { + // try decode typed node first + tn, err := types.DecodeTypedTrieNode(buf) + if err != nil { + return nil, err + } + switch tn := tn.(type) { + case types.TrieNodeRaw: + return decodeNodeUnsafe(hash, tn) + case *types.TrieBranchNodeWithEpoch: + rn, err := decodeNodeUnsafe(hash, tn.Blob) + if err != nil { + return nil, err + } + if rn, ok := rn.(*fullNode); ok { + rn.EpochMap = tn.EpochMap + } + return rn, nil + default: + return nil, types.ErrTypedNodeNotSupport + } } // decodeNodeUnsafe parses the RLP encoding of a trie node. The passed byte slice diff --git a/trie/node_enc.go b/trie/node_enc.go index 1b2eca682f..6485169ac2 100644 --- a/trie/node_enc.go +++ b/trie/node_enc.go @@ -17,6 +17,7 @@ package trie import ( + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" ) @@ -27,6 +28,27 @@ func nodeToBytes(n node) []byte { w.Flush() return result } +func nodeToBytesWithEpoch(n node, raw []byte) []byte { + switch n := n.(type) { + case *fullNode: + withEpoch := false + for i := 0; i < BranchNodeLength-1; i++ { + if n.EpochMap[i] > 0 { + withEpoch = true + break + } + } + if withEpoch { + tn := types.TrieBranchNodeWithEpoch{ + EpochMap: n.EpochMap, + Blob: raw, + } + rn := types.EncodeTypedTrieNode(&tn) + return rn + } + } + return raw +} func (n *fullNode) encode(w rlp.EncoderBuffer) { offset := w.List() diff --git a/trie/tracer.go b/trie/tracer.go index e8d5afb3ba..d278fec39f 100644 --- a/trie/tracer.go +++ b/trie/tracer.go @@ -67,8 +67,8 @@ func (t *tracer) onRead(path []byte, val []byte) { } // onReadEpochMeta tracks the newly loaded trie epoch meta -func (t *tracer) onReadEpochMeta(path string, val []byte) { - t.accessEpochMetaList[path] = val +func (t *tracer) onReadEpochMeta(path []byte, val []byte) { + t.accessEpochMetaList[string(path)] = val } // onInsert tracks the newly inserted trie node. If it's already diff --git a/trie/trie.go b/trie/trie.go index 0a401ee95e..be5462cbe8 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -67,6 +67,7 @@ type Trie struct { currentEpoch types.StateEpoch rootEpoch types.StateEpoch enableExpiry bool + enableMetaDB bool } // newFlag returns the cache flag value for a newly created node. @@ -105,6 +106,7 @@ func New(id *ID, db *Database) (*Trie, error) { reader: reader, tracer: newTracer(), enableExpiry: enableStateExpiry(id, db), + enableMetaDB: reader.emReader != nil, } // resolve root epoch if trie.enableExpiry { @@ -131,21 +133,17 @@ func New(id *ID, db *Database) (*Trie, error) { } func enableStateExpiry(id *ID, db *Database) bool { - if db.snapTree == nil { - return false - } if id.Owner == (common.Hash{}) { return false } - return true + + return db.EnableExpiry() } // NewEmpty is a shortcut to create empty tree. It's mostly used in tests. func NewEmpty(db *Database) *Trie { tr, _ := New(TrieID(types.EmptyRootHash), db) - if db.snapTree != nil { - tr.enableExpiry = true - } + tr.enableExpiry = db.EnableExpiry() return tr } @@ -1051,23 +1049,25 @@ func (t *Trie) resolveEpochMeta(n node, epoch types.StateEpoch, prefix []byte) e if !t.enableExpiry { return nil } - // 1. Check if the node is a full node + switch n := n.(type) { case *shortNode: n.setEpoch(epoch) return nil case *fullNode: n.setEpoch(epoch) - enc, err := t.reader.epochMeta(prefix) - if err != nil { - return err - } - if len(enc) > 0 { - meta, err := epochmeta.DecodeFullNodeEpochMeta(enc) + if t.enableMetaDB { + enc, err := t.reader.epochMeta(prefix) if err != nil { return err } - n.EpochMap = meta.EpochMap + if len(enc) > 0 { + meta, err := epochmeta.DecodeFullNodeEpochMeta(enc) + if err != nil { + return err + } + n.EpochMap = meta.EpochMap + } } return nil case valueNode, hashNode, nil: @@ -1090,17 +1090,19 @@ func (t *Trie) resolveEpochMetaAndTrack(n node, epoch types.StateEpoch, prefix [ return nil case *fullNode: n.setEpoch(epoch) - enc, err := t.reader.epochMeta(prefix) - if err != nil { - return err - } - t.tracer.onReadEpochMeta(string(prefix), enc) - if len(enc) > 0 { - meta, err := epochmeta.DecodeFullNodeEpochMeta(enc) + if t.enableMetaDB { + enc, err := t.reader.epochMeta(prefix) if err != nil { return err } - n.EpochMap = meta.EpochMap + t.tracer.onReadEpochMeta(prefix, enc) + if len(enc) > 0 { + meta, err := epochmeta.DecodeFullNodeEpochMeta(enc) + if err != nil { + return err + } + n.EpochMap = meta.EpochMap + } } return nil case valueNode, hashNode, nil: @@ -1116,11 +1118,25 @@ func (t *Trie) resolveAccountMetaAndTrack() (types.MetaNoConsensus, error) { if !t.enableExpiry { return types.EmptyMetaNoConsensus, nil } - enc, err := t.reader.accountMeta() - if err != nil { - return types.EmptyMetaNoConsensus, err + var ( + enc []byte + err error + ) + + if t.enableMetaDB { + enc, err = t.reader.accountMeta() + if err != nil { + return types.EmptyMetaNoConsensus, err + } + t.tracer.onReadEpochMeta(epochmeta.AccountMetadataPath, enc) + } else { + enc, err = t.reader.node(epochmeta.AccountMetadataPath, types.EmptyRootHash) + if err != nil { + return types.EmptyMetaNoConsensus, err + } + t.tracer.onRead(epochmeta.AccountMetadataPath, enc) } - t.tracer.onReadEpochMeta(epochmeta.AccountMetadataPath, enc) + if len(enc) > 0 { return types.DecodeMetaNoConsensusFromRLPBytes(enc) } @@ -1162,9 +1178,10 @@ func (t *Trie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet, error) for _, path := range paths { nodes.AddNode([]byte(path), trienode.NewDeleted()) } - paths = t.tracer.deletedBranchNodes() - for _, path := range paths { - nodes.AddBranchNodeEpochMeta([]byte(path), nil) + if t.enableExpiry && t.enableMetaDB { + for _, path := range t.tracer.deletedBranchNodes() { + nodes.AddBranchNodeEpochMeta([]byte(path), nil) + } } return types.EmptyRootHash, nodes, nil // case (b) } @@ -1184,20 +1201,27 @@ func (t *Trie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet, error) for _, path := range t.tracer.deletedNodes() { nodes.AddNode([]byte(path), trienode.NewDeleted()) } - for _, path := range t.tracer.deletedBranchNodes() { - nodes.AddBranchNodeEpochMeta([]byte(path), nil) - } // store state expiry account meta if t.enableExpiry { blob, err := epochmeta.AccountMeta2Bytes(types.NewMetaNoConsensus(t.rootEpoch)) if err != nil { return common.Hash{}, nil, err } - if t.tracer.checkEpochMetaChanged([]byte(epochmeta.AccountMetadataPath), blob) { - nodes.AddAccountMeta(blob) + if t.enableMetaDB { + for _, path := range t.tracer.deletedBranchNodes() { + nodes.AddBranchNodeEpochMeta([]byte(path), nil) + } + if t.rootEpoch > types.StateEpoch0 && t.tracer.checkEpochMetaChanged(epochmeta.AccountMetadataPath, blob) { + nodes.AddAccountMeta(blob) + } + } else { + // TODO(0xbundler): the account meta life cycle is same as account data, when delete?. + if t.rootEpoch > types.StateEpoch0 && t.tracer.checkNodeChanged(epochmeta.AccountMetadataPath, blob) { + nodes.AddNode(epochmeta.AccountMetadataPath, trienode.New(types.EmptyRootHash, blob)) + } } } - t.root = newCommitter(nodes, t.tracer, collectLeaf, t.enableExpiry).Commit(t.root) + t.root = newCommitter(nodes, t.tracer, collectLeaf, t.enableExpiry, t.enableMetaDB).Commit(t.root) return rootHash, nodes, nil } @@ -1559,9 +1583,6 @@ func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, p case hashNode: resolve, err := t.resolveAndTrack(n, path) - if _, ok := err.(*MissingNodeError); ok { - return nil - } if err != nil { return err } diff --git a/trie/trie_reader.go b/trie/trie_reader.go index c4c61fc4d2..4ec9ca27d0 100644 --- a/trie/trie_reader.go +++ b/trie/trie_reader.go @@ -111,7 +111,7 @@ func (r *trieReader) node(path []byte, hash common.Hash) ([]byte, error) { return nil, &MissingNodeError{Owner: r.owner, NodeHash: hash, Path: path} } blob, err := r.reader.Node(r.owner, path, hash) - if err != nil || len(blob) == 0 { + if err != nil || (!epochmeta.IsEpochMetaPath(path) && len(blob) == 0) { return nil, &MissingNodeError{Owner: r.owner, NodeHash: hash, Path: path, err: err} } return blob, nil @@ -158,7 +158,7 @@ func (r *trieReader) accountMeta() ([]byte, error) { return nil, errors.New("cannot resolve epoch meta without db for account") } - blob, err := r.emReader.Get(r.owner, epochmeta.AccountMetadataPath) + blob, err := r.emReader.Get(r.owner, string(epochmeta.AccountMetadataPath)) if err != nil { return nil, fmt.Errorf("resolve epoch meta err for account, err: %v", err) } diff --git a/trie/triedb/pathdb/difflayer.go b/trie/triedb/pathdb/difflayer.go index 4ffe0bee2f..0ffa8a6d90 100644 --- a/trie/triedb/pathdb/difflayer.go +++ b/trie/triedb/pathdb/difflayer.go @@ -18,6 +18,7 @@ package pathdb import ( "fmt" + "github.com/ethereum/go-ethereum/trie/epochmeta" "sync" "github.com/ethereum/go-ethereum/common" @@ -111,7 +112,7 @@ func (dl *diffLayer) node(owner common.Hash, path []byte, hash common.Hash, dept if ok { // If the trie node is not hash matched, or marked as removed, // bubble up an error here. It shouldn't happen at all. - if n.Hash != hash { + if !epochmeta.IsEpochMetaPath(path) && n.Hash != hash { dirtyFalseMeter.Mark(1) log.Debug("Unexpected trie node in diff layer", "root", dl.root, "owner", owner, "path", path, "expect", hash, "got", n.Hash) return nil, newUnexpectedNodeError("diff", hash, n.Hash, owner, path) diff --git a/trie/triedb/pathdb/disklayer.go b/trie/triedb/pathdb/disklayer.go index 80da6db7da..af5b235ed9 100644 --- a/trie/triedb/pathdb/disklayer.go +++ b/trie/triedb/pathdb/disklayer.go @@ -19,6 +19,7 @@ package pathdb import ( "errors" "fmt" + "github.com/ethereum/go-ethereum/trie/epochmeta" "sync" "github.com/VictoriaMetrics/fastcache" @@ -127,7 +128,7 @@ func (dl *diskLayer) Node(owner common.Hash, path []byte, hash common.Hash) ([]b defer h.release() got := h.hash(blob) - if got == hash { + if epochmeta.IsEpochMetaPath(path) || got == hash { cleanHitMeter.Mark(1) cleanReadMeter.Mark(int64(len(blob))) return blob, nil @@ -147,7 +148,7 @@ func (dl *diskLayer) Node(owner common.Hash, path []byte, hash common.Hash) ([]b } else { nBlob, nHash = rawdb.ReadStorageTrieNode(dl.db.diskdb, owner, path) } - if nHash != hash { + if !epochmeta.IsEpochMetaPath(path) && nHash != hash { diskFalseMeter.Mark(1) log.Debug("Unexpected trie node in disk", "owner", owner, "path", path, "expect", hash, "got", nHash) return nil, newUnexpectedNodeError("disk", hash, nHash, owner, path) diff --git a/trie/triedb/pathdb/journal.go b/trie/triedb/pathdb/journal.go index d8c7d39fb9..bc5838cecf 100644 --- a/trie/triedb/pathdb/journal.go +++ b/trie/triedb/pathdb/journal.go @@ -161,7 +161,11 @@ func (db *Database) loadDiskLayer(r *rlp.Stream) (layer, error) { subset := make(map[string]*trienode.Node) for _, n := range entry.Nodes { if len(n.Blob) > 0 { - subset[string(n.Path)] = trienode.New(crypto.Keccak256Hash(n.Blob), n.Blob) + raw, err := types.DecodeTypedTrieNodeRaw(n.Blob) + if err != nil { + return nil, err + } + subset[string(n.Path)] = trienode.New(crypto.Keccak256Hash(raw), n.Blob) } else { subset[string(n.Path)] = trienode.NewDeleted() } @@ -199,7 +203,11 @@ func (db *Database) loadDiffLayer(parent layer, r *rlp.Stream) (layer, error) { subset := make(map[string]*trienode.Node) for _, n := range entry.Nodes { if len(n.Blob) > 0 { - subset[string(n.Path)] = trienode.New(crypto.Keccak256Hash(n.Blob), n.Blob) + raw, err := types.DecodeTypedTrieNodeRaw(n.Blob) + if err != nil { + return nil, err + } + subset[string(n.Path)] = trienode.New(crypto.Keccak256Hash(raw), n.Blob) } else { subset[string(n.Path)] = trienode.NewDeleted() } diff --git a/trie/triedb/pathdb/nodebuffer.go b/trie/triedb/pathdb/nodebuffer.go index 42d06bd6dd..851264cd54 100644 --- a/trie/triedb/pathdb/nodebuffer.go +++ b/trie/triedb/pathdb/nodebuffer.go @@ -18,6 +18,7 @@ package pathdb import ( "fmt" + "github.com/ethereum/go-ethereum/trie/epochmeta" "time" "github.com/VictoriaMetrics/fastcache" @@ -68,7 +69,7 @@ func (b *nodebuffer) node(owner common.Hash, path []byte, hash common.Hash) (*tr if !ok { return nil, nil } - if n.Hash != hash { + if !epochmeta.IsEpochMetaPath(path) && n.Hash != hash { dirtyFalseMeter.Mark(1) log.Debug("Unexpected trie node in node buffer", "owner", owner, "path", path, "expect", hash, "got", n.Hash) return nil, newUnexpectedNodeError("dirty", hash, n.Hash, owner, path) diff --git a/trie/trienode/node.go b/trie/trienode/node.go index ebd13aeae1..6c1660ff79 100644 --- a/trie/trienode/node.go +++ b/trie/trienode/node.go @@ -109,7 +109,7 @@ func (set *NodeSet) AddBranchNodeEpochMeta(path []byte, blob []byte) { // AddAccountMeta adds the provided account into set. func (set *NodeSet) AddAccountMeta(blob []byte) { - set.EpochMetas[epochmeta.AccountMetadataPath] = blob + set.EpochMetas[string(epochmeta.AccountMetadataPath)] = blob } // Merge adds a set of nodes into the set. diff --git a/trie/typed_trie_node_test.go b/trie/typed_trie_node_test.go new file mode 100644 index 0000000000..603b18b665 --- /dev/null +++ b/trie/typed_trie_node_test.go @@ -0,0 +1,126 @@ +package trie + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/assert" + "math/rand" + "testing" +) + +var ( + fullNode1 = fullNode{ + Children: [17]node{ + &shortNode{ + Key: common.FromHex("0x2e2"), + Val: valueNode(common.FromHex("0x1")), + }, + &shortNode{ + Key: common.FromHex("0x31f"), + Val: valueNode(common.FromHex("0x2")), + }, + hashNode(common.FromHex("0x1dce34c5cc509511f743349d758b8c38af8ac831432dbbfd989436acd3dbdeb8")), + hashNode(common.FromHex("0x8bf421d69d8aacac46f15f0abd517e61e7ffe6b314a15a4fbce3e2a54323fa81")), + }, + } + fullNode2 = fullNode{ + Children: [17]node{ + hashNode(common.FromHex("0xac51f786e6cee2f4575d19789c1e7ae91da54f2138f415c0f95f127c2893eff9")), + hashNode(common.FromHex("0x83254958a3640af7a740dfcb32a02edfa1224e0ef65c28b1ff60c0b17eacb5d1")), + hashNode(common.FromHex("0xc5f95b4bdbd1a17736a9162cd551d60c60252ea22d5016198ee6e5a5d04ac03a")), + hashNode(common.FromHex("0xfe0654cc989b62dec1758daf6c4a29997f1f618d456981dd1d32f73c74c75151")), + }, + } + shortNode1 = shortNode{ + Key: common.FromHex("0xdf21"), + Val: hashNode(common.FromHex("0x1dce34c5cc509511f743349d758b8c38af8ac831432dbbfd989436acd3dbdeb8")), + } + shortNode2 = shortNode{ + Key: common.FromHex("0xdf21"), + Val: valueNode(common.FromHex("0x1af23")), + } +) + +func TestSimpleTypedNode_Encode_Decode(t *testing.T) { + tests := []struct { + n types.TypedTrieNode + err bool + }{ + { + n: types.TrieNodeRaw{}, + }, + { + n: types.TrieNodeRaw(common.FromHex("0x2465176C461AfB316ebc773C61fAEe85A6515DAA")), + err: true, + }, + { + n: types.TrieNodeRaw(encodeNode(&shortNode1)), + }, + { + n: types.TrieNodeRaw(encodeNode(&shortNode2)), + }, + { + n: types.TrieNodeRaw(encodeNode(&fullNode1)), + }, + { + n: types.TrieNodeRaw(encodeNode(&fullNode2)), + }, + { + n: &types.TrieBranchNodeWithEpoch{ + EpochMap: randomEpochMap(), + Blob: common.FromHex("0x2465176C461AfB316ebc773C61fAEe85A6515DAA"), + }, + }, + { + n: &types.TrieBranchNodeWithEpoch{ + EpochMap: randomEpochMap(), + Blob: encodeNode(&fullNode1), + }, + }, + { + n: &types.TrieBranchNodeWithEpoch{ + EpochMap: randomEpochMap(), + Blob: encodeNode(&fullNode2), + }, + }, + { + n: &types.TrieBranchNodeWithEpoch{ + EpochMap: randomEpochMap(), + Blob: encodeNode(&shortNode1), + }, + }, + { + n: &types.TrieBranchNodeWithEpoch{ + EpochMap: randomEpochMap(), + Blob: encodeNode(&shortNode2), + }, + }, + } + + for i, item := range tests { + enc := types.EncodeTypedTrieNode(item.n) + t.Log(common.Bytes2Hex(enc)) + rn, err := types.DecodeTypedTrieNode(enc) + if item.err { + assert.Error(t, err, i) + continue + } + assert.NoError(t, err, i) + assert.Equal(t, item.n, rn, i) + } +} + +func encodeNode(n node) []byte { + buf := rlp.NewEncoderBuffer(nil) + n.encode(buf) + return buf.ToBytes() +} + +func randomEpochMap() [16]types.StateEpoch { + var ret [16]types.StateEpoch + for i := range ret { + ret[i] = types.StateEpoch(rand.Int() % 10000) + } + return ret +} From a1a0d4d5c440956423fad3a339e133211ea28350 Mon Sep 17 00:00:00 2001 From: asyukii Date: Wed, 18 Oct 2023 09:48:18 +0800 Subject: [PATCH 55/65] fix: missing epochMaps init --- trie/sync.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/trie/sync.go b/trie/sync.go index c6f0da5e86..12f5a73ed3 100644 --- a/trie/sync.go +++ b/trie/sync.go @@ -138,9 +138,10 @@ type syncMemBatch struct { // newSyncMemBatch allocates a new memory-buffer for not-yet persisted trie nodes. func newSyncMemBatch() *syncMemBatch { return &syncMemBatch{ - nodes: make(map[string][]byte), - hashes: make(map[string]common.Hash), - codes: make(map[common.Hash][]byte), + nodes: make(map[string][]byte), + hashes: make(map[string]common.Hash), + codes: make(map[common.Hash][]byte), + epochMaps: make(map[string][16]types.StateEpoch), } } From d2ad0ec045e4af74015cab2b72fa6878c8e0e557 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Fri, 20 Oct 2023 14:43:08 +0800 Subject: [PATCH 56/65] trie/pathdb: fix clean cache low hit rate issue; trie/trie: reuse node cache, prevent resolve again; --- core/blockchain.go | 5 +++++ core/state/trie_prefetcher.go | 17 ++++++----------- trie/tracer.go | 21 +++++++++++++++++++++ trie/trie.go | 10 ++++++++++ trie/triedb/pathdb/disklayer.go | 4 +++- 5 files changed, 45 insertions(+), 12 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 5e2c7fe7a8..b45a17cb1d 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -182,6 +182,11 @@ func (c *CacheConfig) TriedbConfig() *trie.Config { CleanCacheSize: c.TrieCleanLimit * 1024 * 1024, DirtyCacheSize: c.TrieDirtyLimit * 1024 * 1024, } + if config.EnableStateExpiry { + // state expiry need more cache for save epoch meta, but not exceed maxBuffer + config.PathDB.CleanCacheSize = 2 * config.PathDB.CleanCacheSize + config.PathDB.DirtyCacheSize = 2 * config.PathDB.DirtyCacheSize + } } return config } diff --git a/core/state/trie_prefetcher.go b/core/state/trie_prefetcher.go index 62c48bc45b..91307bd917 100644 --- a/core/state/trie_prefetcher.go +++ b/core/state/trie_prefetcher.go @@ -550,8 +550,6 @@ func (sf *subfetcher) loop() { sf.tasks = nil sf.lock.Unlock() - reviveKeys := make([]common.Hash, 0, len(tasks)) - revivePaths := make([][]byte, 0, len(tasks)) // Prefetch any tasks until the loop is interrupted for i, task := range tasks { select { @@ -577,9 +575,13 @@ func (sf *subfetcher) loop() { _, err := sf.trie.GetStorage(sf.addr, task) // handle expired state if sf.expiryMeta.enableStateExpiry { + // TODO(0xbundler): revert to single fetch, because tasks is a channel if exErr, match := err.(*trie2.ExpiredNodeError); match { - reviveKeys = append(reviveKeys, common.BytesToHash(task)) - revivePaths = append(revivePaths, exErr.Path) + key := common.BytesToHash(task) + _, err = fetchExpiredStorageFromRemote(sf.expiryMeta, sf.addr, sf.root, sf.trie, exErr.Path, key) + if err != nil { + log.Error("subfetcher fetchExpiredStorageFromRemote err", "addr", sf.addr, "path", exErr.Path, "err", err) + } } } } @@ -589,13 +591,6 @@ func (sf *subfetcher) loop() { } } - if len(reviveKeys) != 0 { - _, err = batchFetchExpiredFromRemote(sf.expiryMeta, sf.addr, sf.root, sf.trie, revivePaths, reviveKeys) - if err != nil { - log.Error("subfetcher batchFetchExpiredFromRemote err", "addr", sf.addr, "state", sf.state, "revivePaths", revivePaths, "reviveKeys", reviveKeys, "err", err) - } - } - case ch := <-sf.copy: // Somebody wants a copy of the current trie, grant them ch <- sf.db.CopyTrie(sf.trie) diff --git a/trie/tracer.go b/trie/tracer.go index d278fec39f..fc37f70698 100644 --- a/trie/tracer.go +++ b/trie/tracer.go @@ -46,6 +46,7 @@ type tracer struct { deleteEpochMetas map[string]struct{} // record for epoch meta accessList map[string][]byte accessEpochMetaList map[string][]byte + tagEpochMeta bool } // newTracer initializes the tracer for capturing trie changes. @@ -59,6 +60,10 @@ func newTracer() *tracer { } } +func (t *tracer) enableTagEpochMeta() { + t.tagEpochMeta = true +} + // onRead tracks the newly loaded trie node and caches the rlp-encoded // blob internally. Don't change the value outside of function since // it's not deep-copied. @@ -68,6 +73,9 @@ func (t *tracer) onRead(path []byte, val []byte) { // onReadEpochMeta tracks the newly loaded trie epoch meta func (t *tracer) onReadEpochMeta(path []byte, val []byte) { + if !t.tagEpochMeta { + return + } t.accessEpochMetaList[string(path)] = val } @@ -84,6 +92,9 @@ func (t *tracer) onInsert(path []byte) { // onExpandToBranchNode tracks the newly inserted trie branch node. func (t *tracer) onExpandToBranchNode(path []byte) { + if !t.tagEpochMeta { + return + } if _, present := t.deleteEpochMetas[string(path)]; present { delete(t.deleteEpochMetas, string(path)) } @@ -102,6 +113,9 @@ func (t *tracer) onDelete(path []byte) { // onDeleteBranchNode tracks the newly deleted trie branch node. func (t *tracer) onDeleteBranchNode(path []byte) { + if !t.tagEpochMeta { + return + } t.deleteEpochMetas[string(path)] = struct{}{} } @@ -144,6 +158,7 @@ func (t *tracer) copy() *tracer { deleteEpochMetas: deleteBranchNodes, accessList: accessList, accessEpochMetaList: accessEpochMetaList, + tagEpochMeta: t.tagEpochMeta, } } @@ -176,6 +191,12 @@ func (t *tracer) deletedBranchNodes() []string { return paths } +// cached check if cache the node. +func (t *tracer) cached(path []byte) ([]byte, bool) { + val, ok := t.accessList[string(path)] + return val, ok +} + // checkNodeChanged check if change for node. func (t *tracer) checkNodeChanged(path []byte, blob []byte) bool { val, ok := t.accessList[string(path)] diff --git a/trie/trie.go b/trie/trie.go index be5462cbe8..f19eb37d0e 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -110,6 +110,9 @@ func New(id *ID, db *Database) (*Trie, error) { } // resolve root epoch if trie.enableExpiry { + if trie.enableMetaDB { + trie.tracer.enableTagEpochMeta() + } if id.Root != (common.Hash{}) && id.Root != types.EmptyRootHash { trie.root = hashNode(id.Root[:]) meta, err := trie.resolveAccountMetaAndTrack() @@ -1028,6 +1031,13 @@ func (t *Trie) resolve(n node, prefix []byte, epoch types.StateEpoch) (node, err // node's original value. The rlp-encoded blob is preferred to be loaded from // database because it's easy to decode node while complex to encode node to blob. func (t *Trie) resolveAndTrack(n hashNode, prefix []byte) (node, error) { + if t.enableExpiry { + // when meet expired, the trie will skip the resolve path, but cache in tracer + blob, ok := t.tracer.cached(prefix) + if ok { + return mustDecodeNode(n, blob), nil + } + } blob, err := t.reader.node(prefix, common.BytesToHash(n)) if err != nil { return nil, err diff --git a/trie/triedb/pathdb/disklayer.go b/trie/triedb/pathdb/disklayer.go index af5b235ed9..6bb2e05616 100644 --- a/trie/triedb/pathdb/disklayer.go +++ b/trie/triedb/pathdb/disklayer.go @@ -19,6 +19,7 @@ package pathdb import ( "errors" "fmt" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/trie/epochmeta" "sync" @@ -127,7 +128,8 @@ func (dl *diskLayer) Node(owner common.Hash, path []byte, hash common.Hash) ([]b h := newHasher() defer h.release() - got := h.hash(blob) + raw, _ := types.DecodeTypedTrieNodeRaw(blob) + got := h.hash(raw) if epochmeta.IsEpochMetaPath(path) || got == hash { cleanHitMeter.Mark(1) cleanReadMeter.Mark(int64(len(blob))) From 272522b4ebdaea514fb9c6b3cbcded3737e2337e Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Sun, 22 Oct 2023 14:05:48 +0800 Subject: [PATCH 57/65] prune: add more concurrent logics; --- cmd/geth/snapshot.go | 2 + cmd/utils/flags.go | 6 ++ core/state/pruner/pruner.go | 56 +++++------- core/state/state_object.go | 5 +- trie/trie.go | 174 ++++++++++++++++++++++++++++-------- 5 files changed, 168 insertions(+), 75 deletions(-) diff --git a/cmd/geth/snapshot.go b/cmd/geth/snapshot.go index 946173eaf6..91675e467d 100644 --- a/cmd/geth/snapshot.go +++ b/cmd/geth/snapshot.go @@ -62,6 +62,7 @@ var ( utils.BloomFilterSizeFlag, utils.TriesInMemoryFlag, utils.StateExpiryEnableFlag, + utils.StateExpiryMaxThreadFlag, configFileFlag, }, utils.NetworkFlags, utils.DatabasePathFlags), Description: ` @@ -450,6 +451,7 @@ func pruneState(ctx *cli.Context) error { EnableStateExpiry: cfg.Eth.StateExpiryCfg.EnableExpiry(), ChainConfig: chainConfig, CacheConfig: cacheConfig, + MaxExpireThreads: ctx.Uint64(utils.StateExpiryMaxThreadFlag.Name), } pruner, err := pruner.NewPruner(chaindb, prunerconfig, ctx.Uint64(utils.TriesInMemoryFlag.Name)) if err != nil { diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 5f61cd5566..e39252d20f 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1152,6 +1152,12 @@ var ( Usage: "if enable local revive", Category: flags.StateExpiryCategory, } + StateExpiryMaxThreadFlag = &cli.Uint64Flag{ + Name: "state-expiry.maxthread", + Usage: "set state expiry maxthread in prune", + Value: 10000, + Category: flags.StateExpiryCategory, + } ) func init() { diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go index caeb1a2460..b2f7f02faa 100644 --- a/core/state/pruner/pruner.go +++ b/core/state/pruner/pruner.go @@ -30,7 +30,6 @@ import ( "path/filepath" "strings" "sync" - "sync/atomic" "time" "github.com/prometheus/tsdb/fileutil" @@ -66,6 +65,8 @@ const ( rangeCompactionThreshold = 100000 FixedPrefixAndAddrSize = 33 + + defaultReportDuration = 60 * time.Second ) // Config includes all the configurations for pruning. @@ -75,6 +76,7 @@ type Config struct { EnableStateExpiry bool ChainConfig *params.ChainConfig CacheConfig *core.CacheConfig + MaxExpireThreads uint64 } // Pruner is an offline tool to prune the stale state with the @@ -114,7 +116,7 @@ func NewPruner(db ethdb.Database, config Config, triesInMemory uint64) (*Pruner, } // Offline pruning is only supported in legacy hash based scheme. triedb := trie.NewDatabase(db, trie.HashDefaults) - log.Info("ChainConfig", "headBlock", headBlock.NumberU64(), "config", config.ChainConfig) + log.Info("ChainConfig", "headBlock", headBlock.NumberU64(), "config", config) snapconfig := snapshot.Config{ CacheSize: 256, @@ -201,7 +203,7 @@ func pruneAll(maindb ethdb.Database, g *core.Genesis) error { ) eta = time.Duration(left/speed) * time.Millisecond } - if time.Since(logged) > 8*time.Second { + if time.Since(logged) > defaultReportDuration { log.Info("Pruning state data", "nodes", count, "size", size, "elapsed", common.PrettyDuration(time.Since(pstart)), "eta", common.PrettyDuration(eta)) logged = time.Now() @@ -309,7 +311,7 @@ func prune(snaptree *snapshot.Tree, root common.Hash, maindb ethdb.Database, sta ) eta = time.Duration(left/speed) * time.Millisecond } - if time.Since(logged) > 8*time.Second { + if time.Since(logged) > defaultReportDuration { log.Info("Pruning state data", "nodes", count, "size", size, "elapsed", common.PrettyDuration(time.Since(pstart)), "eta", common.PrettyDuration(eta)) logged = time.Now() @@ -733,7 +735,7 @@ func (p *Pruner) ExpiredPrune(height *big.Int, root common.Hash) error { tasksWG.Add(2) go func() { defer tasksWG.Done() - rets[0] = asyncScanExpiredInTrie(trieDB, root, epoch, scanExpiredTrieCh, pruneExpiredInDiskCh) + rets[0] = asyncScanExpiredInTrie(trieDB, root, epoch, scanExpiredTrieCh, pruneExpiredInDiskCh, p.config.MaxExpireThreads) }() go func() { defer tasksWG.Done() @@ -783,7 +785,7 @@ func (p *Pruner) unExpiredBloomTag(trieDB *trie.Database, epoch types.StateEpoch tasksWG.Add(2) go func() { defer tasksWG.Done() - rets[0] = asyncScanUnExpiredInTrie(trieDB, root, epoch, scanUnExpiredTrieCh, tagUnExpiredInBloomCh) + rets[0] = asyncScanUnExpiredInTrie(trieDB, root, epoch, scanUnExpiredTrieCh, tagUnExpiredInBloomCh, p.config.MaxExpireThreads) }() go func() { defer tasksWG.Done() @@ -804,7 +806,7 @@ func asyncTagUnExpiredInBloom(tagUnExpiredInBloomCh chan *trie.NodeInfo, bloom * for info := range tagUnExpiredInBloomCh { trieCount++ bloom.Add(stateBloomHasher(info.Hash[:])) - if time.Since(logged) > 8*time.Second { + if time.Since(logged) > defaultReportDuration { log.Info("Tag unexpired states in bloom", "trieNodes", trieCount) logged = time.Now() } @@ -813,12 +815,12 @@ func asyncTagUnExpiredInBloom(tagUnExpiredInBloomCh chan *trie.NodeInfo, bloom * return nil } -func asyncScanUnExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch types.StateEpoch, scanUnExpiredTrieCh chan *snapshot.ContractItem, tagUnExpiredInBloomCh chan *trie.NodeInfo) error { - var ( - trieCount atomic.Uint64 - start = time.Now() - logged = time.Now() - ) +func asyncScanUnExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch types.StateEpoch, scanUnExpiredTrieCh chan *snapshot.ContractItem, tagUnExpiredInBloomCh chan *trie.NodeInfo, maxThreads uint64) error { + defer func() { + close(tagUnExpiredInBloomCh) + }() + st := trie.NewScanTask(tagUnExpiredInBloomCh, maxThreads, false) + go st.Report(defaultReportDuration) for item := range scanUnExpiredTrieCh { log.Info("start scan trie unexpired state", "addrHash", item.Addr, "root", item.Root) tr, err := trie.New(&trie.ID{ @@ -831,32 +833,24 @@ func asyncScanUnExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch ty return err } tr.SetEpoch(epoch) - if err = tr.ScanForPrune(tagUnExpiredInBloomCh, &trieCount, false); err != nil { + if err = tr.ScanForPrune(st); err != nil { log.Error("asyncScanExpiredInTrie, ScanForPrune err", "id", item, "err", err) return err } - if time.Since(logged) > 8*time.Second { - log.Info("Scan unexpired states", "trieNodes", trieCount.Load()) - logged = time.Now() - } } - log.Info("Scan unexpired states", "trieNodes", trieCount.Load(), "elapsed", common.PrettyDuration(time.Since(start))) - close(tagUnExpiredInBloomCh) + st.WaitThreads() return nil } // asyncScanExpiredInTrie prune trie expired state // here are some issues when just delete it from hash-based storage, because it's shared kv in hbss // but it's ok for pbss. -func asyncScanExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch types.StateEpoch, expireContractCh chan *snapshot.ContractItem, pruneExpiredInDisk chan *trie.NodeInfo) error { +func asyncScanExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch types.StateEpoch, expireContractCh chan *snapshot.ContractItem, pruneExpiredInDisk chan *trie.NodeInfo, maxThreads uint64) error { defer func() { close(pruneExpiredInDisk) }() - var ( - trieCount atomic.Uint64 - start = time.Now() - logged = time.Now() - ) + st := trie.NewScanTask(pruneExpiredInDisk, maxThreads, true) + go st.Report(defaultReportDuration) for item := range expireContractCh { log.Debug("start scan trie expired state", "addrHash", item.Addr, "root", item.Root) tr, err := trie.New(&trie.ID{ @@ -869,16 +863,12 @@ func asyncScanExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch type return err } tr.SetEpoch(epoch) - if err = tr.ScanForPrune(pruneExpiredInDisk, &trieCount, true); err != nil { + if err = tr.ScanForPrune(st); err != nil { log.Error("asyncScanExpiredInTrie, ScanForPrune err", "id", item, "err", err) return err } - if time.Since(logged) > 8*time.Second { - log.Info("Scan unexpired states", "trieNodes", trieCount.Load()) - logged = time.Now() - } } - log.Info("Scan unexpired states", "trieNodes", trieCount.Load(), "elapsed", common.PrettyDuration(time.Since(start))) + st.WaitThreads() return nil } @@ -953,7 +943,7 @@ func asyncPruneExpiredStorageInDisk(diskdb ethdb.Database, pruneExpiredInDisk ch } batch.Reset() } - if time.Since(logged) > 8*time.Second { + if time.Since(logged) > defaultReportDuration { log.Info("Pruning expired states", "items", itemCount, "trieNodes", trieCount, "trieSize", trieSize, "SnapKV", snapCount, "SnapKVSize", snapSize, "EpochMeta", epochMetaCount, "EpochMetaSize", epochMetaSize) diff --git a/core/state/state_object.go b/core/state/state_object.go index e4c39289a6..ef21b81e48 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -582,7 +582,7 @@ func (s *stateObject) updateTrie() (Trie, error) { } // reset trie as pending trie, will commit later if tr != nil { - s.trie = s.db.db.CopyTrie(tr) + s.trie = tr } } return tr, nil @@ -839,8 +839,7 @@ func (s *stateObject) futureReviveState(key common.Hash) { // TODO(0xbundler): add hash key cache later func (s *stateObject) queryFromReviveState(reviveState map[string]common.Hash, key common.Hash) (common.Hash, bool) { - khash := crypto.HashData(s.db.hasher, key[:]) - val, ok := reviveState[string(khash[:])] + val, ok := reviveState[string(crypto.Keccak256(key[:]))] return val, ok } diff --git a/trie/trie.go b/trie/trie.go index f19eb37d0e..b5889b67b6 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -27,7 +27,10 @@ import ( "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/trie/epochmeta" "github.com/ethereum/go-ethereum/trie/trienode" + "runtime" + "sync" "sync/atomic" + "time" ) var ( @@ -1523,8 +1526,81 @@ type NodeInfo struct { IsBranch bool } +type ScanTask struct { + itemCh chan *NodeInfo + unexpiredStat *atomic.Uint64 + expiredStat *atomic.Uint64 + record *atomic.Uint64 + findExpired bool + maxThreads uint64 + wg *sync.WaitGroup + reportDone chan struct{} +} + +func NewScanTask(itemCh chan *NodeInfo, maxThreads uint64, findExpired bool) *ScanTask { + return &ScanTask{ + itemCh: itemCh, + unexpiredStat: &atomic.Uint64{}, + expiredStat: &atomic.Uint64{}, + record: &atomic.Uint64{}, + findExpired: findExpired, + maxThreads: maxThreads, + wg: &sync.WaitGroup{}, + reportDone: make(chan struct{}), + } +} + +func (st *ScanTask) Stat(expired bool) { + if expired { + st.expiredStat.Add(1) + } else { + st.unexpiredStat.Add(1) + } +} + +func (st *ScanTask) ExpiredStat() uint64 { + return st.expiredStat.Load() +} + +func (st *ScanTask) UnexpiredStat() uint64 { + return st.unexpiredStat.Load() +} + +func (st *ScanTask) WaitThreads() { + st.wg.Wait() + close(st.reportDone) +} + +func (st *ScanTask) Report(d time.Duration) { + start := time.Now() + timer := time.NewTimer(d) + defer timer.Stop() + for { + select { + case <-timer.C: + log.Info("Scan trie stats", "unexpired", st.UnexpiredStat(), "expired", st.ExpiredStat(), "go routine", runtime.NumGoroutine(), "elapsed", common.PrettyDuration(time.Since(start))) + timer.Reset(d) + case <-st.reportDone: + log.Info("Scan trie done", "unexpired", st.UnexpiredStat(), "expired", st.ExpiredStat(), "elapsed", common.PrettyDuration(time.Since(start))) + return + } + } +} + +func (st *ScanTask) moreThread() bool { + total := st.expiredStat.Load() + st.unexpiredStat.Load() + record := st.record.Load() + // every increase 10000, will add a new tread to handle other trie path + if total-record > 10000 && uint64(runtime.NumGoroutine()) < st.maxThreads { + st.record.Store(total) + return true + } + + return false +} + // ScanForPrune traverses the storage trie and prunes all expired or unexpired nodes. -func (t *Trie) ScanForPrune(itemCh chan *NodeInfo, stats *atomic.Uint64, findExpired bool) error { +func (t *Trie) ScanForPrune(st *ScanTask) error { if !t.enableExpiry { return nil @@ -1535,10 +1611,10 @@ func (t *Trie) ScanForPrune(itemCh chan *NodeInfo, stats *atomic.Uint64, findExp } err := t.findExpiredSubTree(t.root, nil, t.getRootEpoch(), func(n node, path []byte, epoch types.StateEpoch) { - if pruneErr := t.recursePruneExpiredNode(n, path, epoch, itemCh); pruneErr != nil { + if pruneErr := t.recursePruneExpiredNode(n, path, epoch, st); pruneErr != nil { log.Error("recursePruneExpiredNode err", "Path", path, "err", pruneErr) } - }, stats, itemCh, findExpired) + }, st) if err != nil { return err } @@ -1546,12 +1622,12 @@ func (t *Trie) ScanForPrune(itemCh chan *NodeInfo, stats *atomic.Uint64, findExp return nil } -func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, pruner func(n node, path []byte, epoch types.StateEpoch), stats *atomic.Uint64, itemCh chan *NodeInfo, findExpired bool) error { +func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, pruner func(n node, path []byte, epoch types.StateEpoch), st *ScanTask) error { // Upon reaching expired node, it will recursively traverse downwards to all the child nodes // and collect their hashes. Then, the corresponding key-value pairs will be deleted from the // database by batches. if t.epochExpired(n, epoch) { - if findExpired { + if st.findExpired { pruner(n, path, epoch) } return nil @@ -1559,32 +1635,28 @@ func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, p switch n := n.(type) { case *shortNode: - if stats != nil { - stats.Add(1) - } - if !findExpired { - itemCh <- &NodeInfo{ + st.Stat(false) + if !st.findExpired { + st.itemCh <- &NodeInfo{ Hash: common.BytesToHash(n.flags.hash), } } - err := t.findExpiredSubTree(n.Val, append(path, n.Key...), epoch, pruner, stats, itemCh, findExpired) + err := t.findExpiredSubTree(n.Val, append(path, n.Key...), epoch, pruner, st) if err != nil { return err } return nil case *fullNode: - if stats != nil { - stats.Add(1) - } - if !findExpired { - itemCh <- &NodeInfo{ + st.Stat(false) + if !st.findExpired { + st.itemCh <- &NodeInfo{ Hash: common.BytesToHash(n.flags.hash), } } var err error // Go through every child and recursively delete expired nodes for i, child := range n.Children { - err = t.findExpiredSubTree(child, append(path, byte(i)), n.GetChildEpoch(i), pruner, stats, itemCh, findExpired) + err = t.findExpiredSubTree(child, append(path, byte(i)), n.GetChildEpoch(i), pruner, st) if err != nil { return err } @@ -1592,15 +1664,24 @@ func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, p return nil case hashNode: - resolve, err := t.resolveAndTrack(n, path) + resolve, err := t.resolveHash(n, path) if err != nil { return err } - if err = t.resolveEpochMetaAndTrack(resolve, epoch, path); err != nil { + if err = t.resolveEpochMeta(resolve, epoch, path); err != nil { return err } + if st.moreThread() { + st.wg.Add(1) + path := common.CopyBytes(path) + go func() { + defer st.wg.Done() + t.findExpiredSubTree(resolve, path, epoch, pruner, st) + }() + return nil + } - return t.findExpiredSubTree(resolve, path, epoch, pruner, stats, itemCh, findExpired) + return t.findExpiredSubTree(resolve, path, epoch, pruner, st) case valueNode: return nil case nil: @@ -1610,40 +1691,46 @@ func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, p } } -func (t *Trie) recursePruneExpiredNode(n node, path []byte, epoch types.StateEpoch, pruneItemCh chan *NodeInfo) error { +func (t *Trie) recursePruneExpiredNode(n node, path []byte, epoch types.StateEpoch, st *ScanTask) error { switch n := n.(type) { case *shortNode: + st.Stat(true) subPath := append(path, n.Key...) key := common.Hash{} _, isLeaf := n.Val.(valueNode) if isLeaf { key = common.BytesToHash(hexToKeybytes(subPath)) } - pruneItemCh <- &NodeInfo{ - Addr: t.owner, - Hash: common.BytesToHash(n.flags.hash), - Path: renewBytes(path), - Key: key, - Epoch: epoch, - IsLeaf: isLeaf, + if st.findExpired { + st.itemCh <- &NodeInfo{ + Addr: t.owner, + Hash: common.BytesToHash(n.flags.hash), + Path: renewBytes(path), + Key: key, + Epoch: epoch, + IsLeaf: isLeaf, + } } - err := t.recursePruneExpiredNode(n.Val, subPath, epoch, pruneItemCh) + err := t.recursePruneExpiredNode(n.Val, subPath, epoch, st) if err != nil { return err } return nil case *fullNode: - pruneItemCh <- &NodeInfo{ - Addr: t.owner, - Hash: common.BytesToHash(n.flags.hash), - Path: renewBytes(path), - Epoch: epoch, - IsBranch: true, + st.Stat(true) + if st.findExpired { + st.itemCh <- &NodeInfo{ + Addr: t.owner, + Hash: common.BytesToHash(n.flags.hash), + Path: renewBytes(path), + Epoch: epoch, + IsBranch: true, + } } // recurse child, and except valueNode for i := 0; i < BranchNodeLength-1; i++ { - err := t.recursePruneExpiredNode(n.Children[i], append(path, byte(i)), n.EpochMap[i], pruneItemCh) + err := t.recursePruneExpiredNode(n.Children[i], append(path, byte(i)), n.EpochMap[i], st) if err != nil { return err } @@ -1651,7 +1738,7 @@ func (t *Trie) recursePruneExpiredNode(n node, path []byte, epoch types.StateEpo return nil case hashNode: // hashNode is a index of trie node storage, need not prune. - rn, err := t.resolveAndTrack(n, path) + rn, err := t.resolveHash(n, path) // if touch miss node, just skip if _, ok := err.(*MissingNodeError); ok { return nil @@ -1659,10 +1746,19 @@ func (t *Trie) recursePruneExpiredNode(n node, path []byte, epoch types.StateEpo if err != nil { return err } - if err = t.resolveEpochMetaAndTrack(rn, epoch, path); err != nil { + if err = t.resolveEpochMeta(rn, epoch, path); err != nil { return err } - return t.recursePruneExpiredNode(rn, path, epoch, pruneItemCh) + if st.moreThread() { + st.wg.Add(1) + path := common.CopyBytes(path) + go func() { + defer st.wg.Done() + t.recursePruneExpiredNode(rn, path, epoch, st) + }() + return nil + } + return t.recursePruneExpiredNode(rn, path, epoch, st) case valueNode: // value node is not a single storage uint, so pass to prune. return nil From 7fb891cd1364e0eba82261a230d51ddec0bd14d5 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Mon, 23 Oct 2023 16:00:14 +0800 Subject: [PATCH 58/65] trie/typednode: opt encode/decode performance; pruner: opt concurrent logic; state/state_object: add access state in prefetch; --- core/blockchain.go | 1 - core/state/pruner/pruner.go | 20 +++++++--- core/state/state_expiry.go | 10 ++--- core/state/state_object.go | 31 +++++++++++----- core/state/statedb.go | 2 +- core/types/typed_trie_node.go | 67 ++++++++++++++++++++++++++++------ trie/committer.go | 6 ++- trie/node_enc.go | 26 +++++++++---- trie/trie.go | 69 +++++++++++++++++++++-------------- trie/typed_trie_node_test.go | 61 ++++++++++++++++++++----------- 10 files changed, 202 insertions(+), 91 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index b45a17cb1d..219065930e 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -185,7 +185,6 @@ func (c *CacheConfig) TriedbConfig() *trie.Config { if config.EnableStateExpiry { // state expiry need more cache for save epoch meta, but not exceed maxBuffer config.PathDB.CleanCacheSize = 2 * config.PathDB.CleanCacheSize - config.PathDB.DirtyCacheSize = 2 * config.PathDB.DirtyCacheSize } } return config diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go index b2f7f02faa..bf91553c3a 100644 --- a/core/state/pruner/pruner.go +++ b/core/state/pruner/pruner.go @@ -62,11 +62,13 @@ const ( // rangeCompactionThreshold is the minimal deleted entry number for // triggering range compaction. It's a quite arbitrary number but just // to avoid triggering range compaction because of small deletion. - rangeCompactionThreshold = 100000 + rangeCompactionThreshold = 1000000 FixedPrefixAndAddrSize = 33 defaultReportDuration = 60 * time.Second + + defaultChannelSize = 200000 ) // Config includes all the configurations for pruning. @@ -727,8 +729,8 @@ func (p *Pruner) ExpiredPrune(height *big.Int, root common.Hash) error { } var ( - scanExpiredTrieCh = make(chan *snapshot.ContractItem, 100000) - pruneExpiredInDiskCh = make(chan *trie.NodeInfo, 100000) + scanExpiredTrieCh = make(chan *snapshot.ContractItem, defaultChannelSize) + pruneExpiredInDiskCh = make(chan *trie.NodeInfo, defaultChannelSize) rets = make([]error, 3) tasksWG sync.WaitGroup ) @@ -771,8 +773,8 @@ func (p *Pruner) ExpiredPrune(height *big.Int, root common.Hash) error { func (p *Pruner) unExpiredBloomTag(trieDB *trie.Database, epoch types.StateEpoch, root common.Hash) (*bloomfilter.Filter, error) { var ( - scanUnExpiredTrieCh = make(chan *snapshot.ContractItem, 100000) - tagUnExpiredInBloomCh = make(chan *trie.NodeInfo, 100000) + scanUnExpiredTrieCh = make(chan *snapshot.ContractItem, defaultChannelSize) + tagUnExpiredInBloomCh = make(chan *trie.NodeInfo, defaultChannelSize) rets = make([]error, 3) tasksWG sync.WaitGroup ) @@ -833,6 +835,14 @@ func asyncScanUnExpiredInTrie(db *trie.Database, stateRoot common.Hash, epoch ty return err } tr.SetEpoch(epoch) + if st.MoreThread() { + st.Schedule(func() { + if err = tr.ScanForPrune(st); err != nil { + log.Error("asyncScanExpiredInTrie, ScanForPrune err", "id", item, "err", err) + } + }) + continue + } if err = tr.ScanForPrune(st); err != nil { log.Error("asyncScanExpiredInTrie, ScanForPrune err", "id", item, "err", err) return err diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index 8d1e3b9bbf..d0742ff65f 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -35,12 +35,12 @@ func defaultStateExpiryMeta() *stateExpiryMeta { // fetchExpiredStorageFromRemote request expired state from remote full state node; func fetchExpiredStorageFromRemote(meta *stateExpiryMeta, addr common.Address, root common.Hash, tr Trie, prefixKey []byte, key common.Hash) (map[string][]byte, error) { - log.Debug("fetching expired storage from remoteDB", "addr", addr, "prefix", prefixKey, "key", key) + //log.Debug("fetching expired storage from remoteDB", "addr", addr, "prefix", prefixKey, "key", key) reviveTrieMeter.Mark(1) if meta.enableLocalRevive { // if there need revive expired state, try to revive locally, when the node is not being pruned, just renew the epoch val, err := tr.TryLocalRevive(addr, key.Bytes()) - log.Debug("fetchExpiredStorageFromRemote TryLocalRevive", "addr", addr, "key", key, "val", val, "err", err) + //log.Debug("fetchExpiredStorageFromRemote TryLocalRevive", "addr", addr, "key", key, "val", val, "err", err) if _, ok := err.(*trie.MissingNodeError); !ok { return nil, err } @@ -60,7 +60,7 @@ func fetchExpiredStorageFromRemote(meta *stateExpiryMeta, addr common.Address, r reviveFromRemoteMeter.Mark(1) // cannot revive locally, fetch remote proof proofs, err := meta.fullStateDB.GetStorageReviveProof(meta.originalRoot, addr, root, []string{common.Bytes2Hex(prefixKey)}, []string{common.Bytes2Hex(key[:])}) - log.Debug("fetchExpiredStorageFromRemote GetStorageReviveProof", "addr", addr, "key", key, "proofs", len(proofs), "err", err) + //log.Debug("fetchExpiredStorageFromRemote GetStorageReviveProof", "addr", addr, "key", key, "proofs", len(proofs), "err", err) if err != nil { return nil, err } @@ -85,7 +85,7 @@ func batchFetchExpiredFromRemote(expiryMeta *stateExpiryMeta, addr common.Addres var expiredPrefixKeys [][]byte for i, key := range keys { val, err := tr.TryLocalRevive(addr, key.Bytes()) - log.Debug("fetchExpiredStorageFromRemote TryLocalRevive", "addr", addr, "key", key, "val", val, "err", err) + //log.Debug("fetchExpiredStorageFromRemote TryLocalRevive", "addr", addr, "key", key, "val", val, "err", err) if _, ok := err.(*trie.MissingNodeError); !ok { return nil, err } @@ -125,7 +125,7 @@ func batchFetchExpiredFromRemote(expiryMeta *stateExpiryMeta, addr common.Addres // cannot revive locally, fetch remote proof reviveFromRemoteMeter.Mark(int64(len(keysStr))) proofs, err := expiryMeta.fullStateDB.GetStorageReviveProof(expiryMeta.originalRoot, addr, root, prefixKeysStr, keysStr) - log.Debug("fetchExpiredStorageFromRemote GetStorageReviveProof", "addr", addr, "keys", keysStr, "prefixKeys", prefixKeysStr, "proofs", len(proofs), "err", err) + //log.Debug("fetchExpiredStorageFromRemote GetStorageReviveProof", "addr", addr, "keys", keysStr, "prefixKeys", prefixKeysStr, "proofs", len(proofs), "err", err) if err != nil { return nil, err } diff --git a/core/state/state_object.go b/core/state/state_object.go index ef21b81e48..23f75d52c6 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -18,10 +18,8 @@ package state import ( "bytes" - "encoding/hex" "fmt" "github.com/ethereum/go-ethereum/core/state/snapshot" - "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/trie" "io" "math/big" @@ -319,7 +317,7 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { // handle state expiry situation if s.db.EnableExpire() { if enErr, ok := err.(*trie.ExpiredNodeError); ok { - log.Debug("GetCommittedState expired in trie", "addr", s.address, "key", key, "err", err) + //log.Debug("GetCommittedState expired in trie", "addr", s.address, "key", key, "err", err) val, err = s.fetchExpiredFromRemote(enErr.Path, key, false) } // TODO(0xbundler): add epoch record cache for prevent frequency access epoch update, may implement later @@ -394,6 +392,19 @@ func (s *stateObject) finalise(prefetch bool) { slotsToPrefetch = append(slotsToPrefetch, common.CopyBytes(key[:])) // Copy needed for closure } + // try prefetch future update state + for key := range s.pendingAccessedState { + if val, ok := s.dirtyStorage[key]; ok { + if val != s.originStorage[key] { + continue + } + } + if _, ok := s.pendingFutureReviveState[key]; ok { + continue + } + slotsToPrefetch = append(slotsToPrefetch, common.CopyBytes(key[:])) // Copy needed for closure + } + if s.db.prefetcher != nil && prefetch && len(slotsToPrefetch) > 0 && s.data.Root != types.EmptyRootHash { s.db.prefetcher.prefetch(s.addrHash, s.data.Root, s.address, slotsToPrefetch) } @@ -463,7 +474,7 @@ func (s *stateObject) updateTrie() (Trie, error) { // it must hit in cache value := s.GetState(key) dirtyStorage[key] = common.TrimLeftZeroes(value[:]) - log.Debug("updateTrie access state", "contract", s.address, "key", key, "epoch", s.db.Epoch()) + //log.Debug("updateTrie access state", "contract", s.address, "key", key, "epoch", s.db.Epoch()) } } @@ -486,7 +497,7 @@ func (s *stateObject) updateTrie() (Trie, error) { if _, err = fetchExpiredStorageFromRemote(s.db.expiryMeta, s.address, s.data.Root, tr, enErr.Path, key); err != nil { s.db.setError(fmt.Errorf("state object pendingFutureReviveState fetchExpiredStorageFromRemote err, contract: %v, key: %v, path: %v, err: %v", s.address, key, enErr.Path, err)) } - log.Debug("updateTrie pendingFutureReviveState", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "tr.epoch", tr.Epoch(), "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) + //log.Debug("updateTrie pendingFutureReviveState", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "tr.epoch", tr.Epoch(), "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) } } for key, value := range dirtyStorage { @@ -494,13 +505,13 @@ func (s *stateObject) updateTrie() (Trie, error) { if err := tr.DeleteStorage(s.address, key[:]); err != nil { s.db.setError(fmt.Errorf("state object update trie DeleteStorage err, contract: %v, key: %v, err: %v", s.address, key, err)) } - log.Debug("updateTrie DeleteStorage", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "value", value, "tr.epoch", tr.Epoch(), "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) + //log.Debug("updateTrie DeleteStorage", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "value", value, "tr.epoch", tr.Epoch(), "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) s.db.StorageDeleted += 1 } else { if err := tr.UpdateStorage(s.address, key[:], value); err != nil { s.db.setError(fmt.Errorf("state object update trie UpdateStorage err, contract: %v, key: %v, err: %v", s.address, key, err)) } - log.Debug("updateTrie UpdateStorage", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "value", value, "tr.epoch", tr.Epoch(), "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) + //log.Debug("updateTrie UpdateStorage", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "value", value, "tr.epoch", tr.Epoch(), "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) s.db.StorageUpdated += 1 } // Cache the items for preloading @@ -539,7 +550,7 @@ func (s *stateObject) updateTrie() (Trie, error) { } } storage[khash] = snapshotVal // snapshotVal will be nil if it's deleted - log.Debug("updateTrie UpdateSnapShot", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "value", snapshotVal, "tr.epoch", tr.Epoch(), "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) + //log.Debug("updateTrie UpdateSnapShot", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "value", snapshotVal, "tr.epoch", tr.Epoch(), "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) // Track the original value of slot only if it's mutated first time prev := s.originStorage[key] @@ -906,11 +917,11 @@ func (s *stateObject) getExpirySnapStorage(key common.Hash) ([]byte, error, erro // if found value not been pruned, just return, local revive later if s.db.EnableLocalRevive() && len(val.GetVal()) > 0 { s.futureReviveState(key) - log.Debug("getExpirySnapStorage GetVal", "addr", s.address, "key", key, "val", hex.EncodeToString(val.GetVal())) + //log.Debug("getExpirySnapStorage GetVal", "addr", s.address, "key", key, "val", hex.EncodeToString(val.GetVal())) return val.GetVal(), nil, nil } - log.Debug("GetCommittedState expired in snapshot", "addr", s.address, "key", key, "val", val, "enc", enc, "err", err) + //log.Debug("GetCommittedState expired in snapshot", "addr", s.address, "key", key, "val", val, "enc", enc, "err", err) // handle from remoteDB, if got err just setError, or return to revert in consensus version. valRaw, err := s.fetchExpiredFromRemote(nil, key, true) if err != nil { diff --git a/core/state/statedb.go b/core/state/statedb.go index a7eb0aa300..96c94b45f5 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -259,7 +259,7 @@ func (s *StateDB) InitStateExpiryFeature(config *types.StateExpiryConfig, remote originalRoot: s.originalRoot, originalHash: startAtBlockHash, } - log.Debug("StateDB enable state expiry feature", "expectHeight", expectHeight, "startAtBlockHash", startAtBlockHash, "epoch", epoch) + //log.Debug("StateDB enable state expiry feature", "expectHeight", expectHeight, "startAtBlockHash", startAtBlockHash, "epoch", epoch) return s } diff --git a/core/types/typed_trie_node.go b/core/types/typed_trie_node.go index f7ac2ea044..01eb120edf 100644 --- a/core/types/typed_trie_node.go +++ b/core/types/typed_trie_node.go @@ -1,9 +1,10 @@ package types import ( - "bytes" "errors" + "fmt" "github.com/ethereum/go-ethereum/rlp" + "io" ) const ( @@ -39,13 +40,54 @@ func (n *TrieBranchNodeWithEpoch) Type() uint8 { } func (n *TrieBranchNodeWithEpoch) EncodeToRLPBytes(buf *rlp.EncoderBuffer) { - rlp.Encode(buf, n) + offset := buf.List() + mapOffset := buf.List() + for _, item := range n.EpochMap { + if item == 0 { + buf.Write(rlp.EmptyString) + } else { + buf.WriteUint64(uint64(item)) + } + } + buf.ListEnd(mapOffset) + buf.Write(n.Blob) + buf.ListEnd(offset) } func DecodeTrieBranchNodeWithEpoch(enc []byte) (*TrieBranchNodeWithEpoch, error) { var n TrieBranchNodeWithEpoch - if err := rlp.DecodeBytes(enc, &n); err != nil { - return nil, err + if len(enc) == 0 { + return nil, io.ErrUnexpectedEOF + } + elems, _, err := rlp.SplitList(enc) + if err != nil { + return nil, fmt.Errorf("decode error: %v", err) + } + + maps, rest, err := rlp.SplitList(elems) + if err != nil { + return nil, fmt.Errorf("decode epochmap error: %v", err) + } + for i := 0; i < len(n.EpochMap); i++ { + var c uint64 + c, maps, err = rlp.SplitUint64(maps) + if err != nil { + return nil, fmt.Errorf("decode epochmap val error: %v", err) + } + n.EpochMap[i] = StateEpoch(c) + } + + k, content, _, err := rlp.Split(rest) + if err != nil { + return nil, fmt.Errorf("decode raw error: %v", err) + } + switch k { + case rlp.String: + n.Blob = content + case rlp.List: + n.Blob = rest + default: + return nil, fmt.Errorf("decode wrong raw type error: %v", err) } return &n, nil } @@ -54,15 +96,16 @@ func EncodeTypedTrieNode(val TypedTrieNode) []byte { switch raw := val.(type) { case TrieNodeRaw: return raw + case *TrieBranchNodeWithEpoch: + // encode with type prefix + w := rlp.NewEncoderBuffer(nil) + w.Write([]byte{val.Type()}) + val.EncodeToRLPBytes(&w) + result := w.ToBytes() + w.Flush() + return result } - // encode with type prefix - buf := bytes.NewBuffer(make([]byte, 0, 40)) - buf.WriteByte(val.Type()) - encoder := rlp.NewEncoderBuffer(buf) - val.EncodeToRLPBytes(&encoder) - // it cannot be error here. - encoder.Flush() - return buf.Bytes() + return nil } func DecodeTypedTrieNode(enc []byte) (TypedTrieNode, error) { diff --git a/trie/committer.go b/trie/committer.go index 1980475408..2ad8d9f4f5 100644 --- a/trie/committer.go +++ b/trie/committer.go @@ -144,9 +144,11 @@ func (c *committer) store(path []byte, n node) node { } // Collect the dirty node to nodeset for return. nhash := common.BytesToHash(hash) - blob := nodeToBytes(n) + var blob []byte if c.enableStateExpiry && !c.enableMetaDB { - blob = nodeToBytesWithEpoch(n, blob) + blob = nodeToBytesWithEpoch(n) + } else { + blob = nodeToBytes(n) } changed := c.tracer.checkNodeChanged(path, blob) if changed { diff --git a/trie/node_enc.go b/trie/node_enc.go index 6485169ac2..98b17bca73 100644 --- a/trie/node_enc.go +++ b/trie/node_enc.go @@ -28,7 +28,8 @@ func nodeToBytes(n node) []byte { w.Flush() return result } -func nodeToBytesWithEpoch(n node, raw []byte) []byte { + +func nodeToBytesWithEpoch(n node) []byte { switch n := n.(type) { case *fullNode: withEpoch := false @@ -39,15 +40,26 @@ func nodeToBytesWithEpoch(n node, raw []byte) []byte { } } if withEpoch { - tn := types.TrieBranchNodeWithEpoch{ - EpochMap: n.EpochMap, - Blob: raw, + w := rlp.NewEncoderBuffer(nil) + w.Write([]byte{types.TrieBranchNodeWithEpochType}) + offset := w.List() + mapOffset := w.List() + for _, item := range n.EpochMap { + if item == 0 { + w.Write(rlp.EmptyString) + } else { + w.WriteUint64(uint64(item)) + } } - rn := types.EncodeTypedTrieNode(&tn) - return rn + w.ListEnd(mapOffset) + n.encode(w) + w.ListEnd(offset) + result := w.ToBytes() + w.Flush() + return result } } - return raw + return nodeToBytes(n) } func (n *fullNode) encode(w rlp.EncoderBuffer) { diff --git a/trie/trie.go b/trie/trie.go index b5889b67b6..75f0ef5e4d 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -1530,9 +1530,8 @@ type ScanTask struct { itemCh chan *NodeInfo unexpiredStat *atomic.Uint64 expiredStat *atomic.Uint64 - record *atomic.Uint64 findExpired bool - maxThreads uint64 + routineCh chan struct{} wg *sync.WaitGroup reportDone chan struct{} } @@ -1542,9 +1541,8 @@ func NewScanTask(itemCh chan *NodeInfo, maxThreads uint64, findExpired bool) *Sc itemCh: itemCh, unexpiredStat: &atomic.Uint64{}, expiredStat: &atomic.Uint64{}, - record: &atomic.Uint64{}, findExpired: findExpired, - maxThreads: maxThreads, + routineCh: make(chan struct{}, maxThreads), wg: &sync.WaitGroup{}, reportDone: make(chan struct{}), } @@ -1578,25 +1576,42 @@ func (st *ScanTask) Report(d time.Duration) { for { select { case <-timer.C: - log.Info("Scan trie stats", "unexpired", st.UnexpiredStat(), "expired", st.ExpiredStat(), "go routine", runtime.NumGoroutine(), "elapsed", common.PrettyDuration(time.Since(start))) + log.Info("Scan trie stats", "total", st.TotalScan(), "unexpired", st.UnexpiredStat(), "expired", st.ExpiredStat(), "go routine", runtime.NumGoroutine(), "elapsed", common.PrettyDuration(time.Since(start))) timer.Reset(d) case <-st.reportDone: - log.Info("Scan trie done", "unexpired", st.UnexpiredStat(), "expired", st.ExpiredStat(), "elapsed", common.PrettyDuration(time.Since(start))) + log.Info("Scan trie done", "total", st.TotalScan(), "unexpired", st.UnexpiredStat(), "expired", st.ExpiredStat(), "elapsed", common.PrettyDuration(time.Since(start))) return } } } -func (st *ScanTask) moreThread() bool { - total := st.expiredStat.Load() + st.unexpiredStat.Load() - record := st.record.Load() - // every increase 10000, will add a new tread to handle other trie path - if total-record > 10000 && uint64(runtime.NumGoroutine()) < st.maxThreads { - st.record.Store(total) +func (st *ScanTask) Schedule(f func()) { + log.Debug("schedule", "total", st.TotalScan(), "go routine", runtime.NumGoroutine()) + st.wg.Add(1) + go func() { + defer func() { + st.wg.Done() + select { + case <-st.routineCh: + log.Debug("task Schedule done", "routine", len(st.routineCh)) + default: + } + }() + f() + }() +} + +func (st *ScanTask) TotalScan() uint64 { + return st.expiredStat.Load() + st.unexpiredStat.Load() +} + +func (st *ScanTask) MoreThread() bool { + select { + case st.routineCh <- struct{}{}: return true + default: + return false } - - return false } // ScanForPrune traverses the storage trie and prunes all expired or unexpired nodes. @@ -1671,16 +1686,15 @@ func (t *Trie) findExpiredSubTree(n node, path []byte, epoch types.StateEpoch, p if err = t.resolveEpochMeta(resolve, epoch, path); err != nil { return err } - if st.moreThread() { - st.wg.Add(1) + if st.TotalScan()%1000 == 0 && st.MoreThread() { path := common.CopyBytes(path) - go func() { - defer st.wg.Done() - t.findExpiredSubTree(resolve, path, epoch, pruner, st) - }() + st.Schedule(func() { + if err := t.findExpiredSubTree(resolve, path, epoch, pruner, st); err != nil { + log.Error("recursePruneExpiredNode err", "addr", t.owner, "path", path, "epoch", epoch, "err", err) + } + }) return nil } - return t.findExpiredSubTree(resolve, path, epoch, pruner, st) case valueNode: return nil @@ -1749,13 +1763,14 @@ func (t *Trie) recursePruneExpiredNode(n node, path []byte, epoch types.StateEpo if err = t.resolveEpochMeta(rn, epoch, path); err != nil { return err } - if st.moreThread() { - st.wg.Add(1) + + if st.TotalScan()%1000 == 0 && st.MoreThread() { path := common.CopyBytes(path) - go func() { - defer st.wg.Done() - t.recursePruneExpiredNode(rn, path, epoch, st) - }() + st.Schedule(func() { + if err := t.recursePruneExpiredNode(rn, path, epoch, st); err != nil { + log.Error("recursePruneExpiredNode err", "addr", t.owner, "path", path, "epoch", epoch, "err", err) + } + }) return nil } return t.recursePruneExpiredNode(rn, path, epoch, st) diff --git a/trie/typed_trie_node_test.go b/trie/typed_trie_node_test.go index 603b18b665..dc88be1958 100644 --- a/trie/typed_trie_node_test.go +++ b/trie/typed_trie_node_test.go @@ -3,7 +3,6 @@ package trie import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rlp" "github.com/stretchr/testify/assert" "math/rand" "testing" @@ -11,6 +10,7 @@ import ( var ( fullNode1 = fullNode{ + EpochMap: randomEpochMap(), Children: [17]node{ &shortNode{ Key: common.FromHex("0x2e2"), @@ -25,6 +25,7 @@ var ( }, } fullNode2 = fullNode{ + EpochMap: randomEpochMap(), Children: [17]node{ hashNode(common.FromHex("0xac51f786e6cee2f4575d19789c1e7ae91da54f2138f415c0f95f127c2893eff9")), hashNode(common.FromHex("0x83254958a3640af7a740dfcb32a02edfa1224e0ef65c28b1ff60c0b17eacb5d1")), @@ -55,45 +56,39 @@ func TestSimpleTypedNode_Encode_Decode(t *testing.T) { err: true, }, { - n: types.TrieNodeRaw(encodeNode(&shortNode1)), + n: types.TrieNodeRaw(nodeToBytes(&shortNode1)), }, { - n: types.TrieNodeRaw(encodeNode(&shortNode2)), + n: types.TrieNodeRaw(nodeToBytes(&shortNode2)), }, { - n: types.TrieNodeRaw(encodeNode(&fullNode1)), + n: types.TrieNodeRaw(nodeToBytes(&fullNode1)), }, { - n: types.TrieNodeRaw(encodeNode(&fullNode2)), + n: types.TrieNodeRaw(nodeToBytes(&fullNode2)), }, { n: &types.TrieBranchNodeWithEpoch{ - EpochMap: randomEpochMap(), - Blob: common.FromHex("0x2465176C461AfB316ebc773C61fAEe85A6515DAA"), - }, - }, - { - n: &types.TrieBranchNodeWithEpoch{ - EpochMap: randomEpochMap(), - Blob: encodeNode(&fullNode1), + EpochMap: fullNode1.EpochMap, + Blob: nodeToBytes(&fullNode1), }, }, { n: &types.TrieBranchNodeWithEpoch{ - EpochMap: randomEpochMap(), - Blob: encodeNode(&fullNode2), + EpochMap: fullNode2.EpochMap, + Blob: nodeToBytes(&fullNode2), }, }, { n: &types.TrieBranchNodeWithEpoch{ EpochMap: randomEpochMap(), - Blob: encodeNode(&shortNode1), + Blob: nodeToBytes(&shortNode1), }, }, { n: &types.TrieBranchNodeWithEpoch{ EpochMap: randomEpochMap(), - Blob: encodeNode(&shortNode2), + Blob: nodeToBytes(&shortNode2), }, }, } @@ -111,10 +106,34 @@ func TestSimpleTypedNode_Encode_Decode(t *testing.T) { } } -func encodeNode(n node) []byte { - buf := rlp.NewEncoderBuffer(nil) - n.encode(buf) - return buf.ToBytes() +func TestNode2Bytes_Encode(t *testing.T) { + tests := []struct { + tn types.TypedTrieNode + n node + err bool + }{ + { + tn: &types.TrieBranchNodeWithEpoch{ + EpochMap: fullNode1.EpochMap, + Blob: nodeToBytes(&fullNode1), + }, + n: &fullNode1, + }, + { + tn: &types.TrieBranchNodeWithEpoch{ + EpochMap: fullNode2.EpochMap, + Blob: nodeToBytes(&fullNode2), + }, + n: &fullNode2, + }, + } + + for i, item := range tests { + enc1 := nodeToBytesWithEpoch(item.n) + enc2 := types.EncodeTypedTrieNode(item.tn) + t.Log(common.Bytes2Hex(enc1), common.Bytes2Hex(enc2)) + assert.Equal(t, enc2, enc1, i) + } } func randomEpochMap() [16]types.StateEpoch { From c502191b873cc334e901887d6ba3b68bb508ea6b Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Tue, 24 Oct 2023 18:05:13 +0800 Subject: [PATCH 59/65] state/state_object: add more expired metrics; state/state_object: add more expired metrics; --- consensus/parlia/parlia.go | 3 ++- core/state/state_object.go | 12 ++++++++---- core/state/statedb.go | 11 +++++++---- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/consensus/parlia/parlia.go b/consensus/parlia/parlia.go index 8ce71f76a1..bc8362bdb3 100644 --- a/consensus/parlia/parlia.go +++ b/consensus/parlia/parlia.go @@ -1692,13 +1692,14 @@ func (p *Parlia) applyTransaction( } actualTx := (*receivedTxs)[0] if !bytes.Equal(p.signer.Hash(actualTx).Bytes(), expectedHash.Bytes()) { - return fmt.Errorf("expected tx hash %v, get %v, nonce %d:%d, to %s:%s, value %s:%s, gas %d:%d, gasPrice %s:%s, data %s:%s", expectedHash.String(), actualTx.Hash().String(), + return fmt.Errorf("expected tx hash %v, get %v, nonce %d:%d, to %s:%s, value %s:%s, gas %d:%d, gasPrice %s:%s, data %s:%s, dbErr: %v", expectedHash.String(), actualTx.Hash().String(), expectedTx.Nonce(), actualTx.Nonce(), expectedTx.To().String(), actualTx.To().String(), expectedTx.Value().String(), actualTx.Value().String(), expectedTx.Gas(), actualTx.Gas(), expectedTx.GasPrice().String(), actualTx.GasPrice().String(), hex.EncodeToString(expectedTx.Data()), hex.EncodeToString(actualTx.Data()), + state.Error(), ) } expectedTx = actualTx diff --git a/core/state/state_object.go b/core/state/state_object.go index 23f75d52c6..317a70495e 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -319,11 +319,12 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { if enErr, ok := err.(*trie.ExpiredNodeError); ok { //log.Debug("GetCommittedState expired in trie", "addr", s.address, "key", key, "err", err) val, err = s.fetchExpiredFromRemote(enErr.Path, key, false) + getCommittedStorageExpiredMeter.Mark(1) + } else if err != nil { + getCommittedStorageUnexpiredMeter.Mark(1) + // TODO(0xbundler): add epoch record cache for prevent frequency access epoch update, may implement later + //s.originStorageEpoch[key] = epoch } - // TODO(0xbundler): add epoch record cache for prevent frequency access epoch update, may implement later - //if err != nil { - // s.originStorageEpoch[key] = epoch - //} } if err != nil { s.db.setError(fmt.Errorf("state object get storage err, contract: %v, key: %v, err: %v", s.address, key, err)) @@ -911,12 +912,15 @@ func (s *stateObject) getExpirySnapStorage(key common.Hash) ([]byte, error, erro s.originStorageEpoch[key] = val.GetEpoch() if !types.EpochExpired(val.GetEpoch(), s.db.Epoch()) { + getCommittedStorageUnexpiredMeter.Mark(1) return val.GetVal(), nil, nil } + getCommittedStorageExpiredMeter.Mark(1) // if found value not been pruned, just return, local revive later if s.db.EnableLocalRevive() && len(val.GetVal()) > 0 { s.futureReviveState(key) + getCommittedStorageExpiredLocalReviveMeter.Mark(1) //log.Debug("getExpirySnapStorage GetVal", "addr", s.address, "key", key, "val", hex.EncodeToString(val.GetVal())) return val.GetVal(), nil, nil } diff --git a/core/state/statedb.go b/core/state/statedb.go index 96c94b45f5..252df08074 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -46,10 +46,13 @@ import ( const defaultNumOfSlots = 100 var ( - getCommittedStorageMeter = metrics.NewRegisteredMeter("state/contract/committed", nil) - getCommittedStorageSnapMeter = metrics.NewRegisteredMeter("state/contract/committed/snap", nil) - getCommittedStorageTrieMeter = metrics.NewRegisteredMeter("state/contract/committed/trie", nil) - getCommittedStorageRemoteMeter = metrics.NewRegisteredMeter("state/contract/committed/remote", nil) + getCommittedStorageMeter = metrics.NewRegisteredMeter("state/contract/committed", nil) + getCommittedStorageSnapMeter = metrics.NewRegisteredMeter("state/contract/committed/snap", nil) + getCommittedStorageTrieMeter = metrics.NewRegisteredMeter("state/contract/committed/trie", nil) + getCommittedStorageExpiredMeter = metrics.NewRegisteredMeter("state/contract/committed/expired", nil) + getCommittedStorageExpiredLocalReviveMeter = metrics.NewRegisteredMeter("state/contract/committed/expired/localrevive", nil) + getCommittedStorageUnexpiredMeter = metrics.NewRegisteredMeter("state/contract/committed/unexpired", nil) + getCommittedStorageRemoteMeter = metrics.NewRegisteredMeter("state/contract/committed/remote", nil) ) type revision struct { From 749bcc2f1f5a3b7ab2a3ad9c69c9278728c7252e Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Wed, 25 Oct 2023 18:59:07 +0800 Subject: [PATCH 60/65] eth/downloader: state expiry remotedb keep behind the latest; --- cmd/geth/chaincmd.go | 21 +++++-------- cmd/geth/config.go | 2 +- cmd/geth/consolecmd.go | 2 +- cmd/geth/dbcmd.go | 48 ++++++++++++------------------ cmd/geth/main.go | 1 + cmd/geth/snapshot.go | 29 +++++++----------- cmd/geth/verkle.go | 4 +-- cmd/utils/flags.go | 13 ++++++++ core/state/statedb.go | 2 +- core/types/state_expiry.go | 51 ++++++++++++++++++++++++++++++-- eth/backend.go | 2 +- eth/downloader/downloader.go | 57 ++++++++++++++++++++++++------------ eth/fetcher/block_fetcher.go | 15 ++++++++++ eth/handler.go | 11 ++++--- 14 files changed, 164 insertions(+), 94 deletions(-) diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go index 59246d2c37..816df6f2e0 100644 --- a/cmd/geth/chaincmd.go +++ b/cmd/geth/chaincmd.go @@ -56,8 +56,7 @@ var ( Flags: flags.Merge([]cli.Flag{ utils.CachePreimagesFlag, utils.StateSchemeFlag, - utils.StateExpiryEnableFlag, - }, utils.DatabasePathFlags), + }, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: ` The init command initializes a new genesis block and definition for the network. This is a destructive action and changes the network in which you will be @@ -87,8 +86,7 @@ It expects the genesis file as argument.`, Name: "dumpgenesis", Usage: "Dumps genesis block JSON configuration to stdout", ArgsUsage: "", - Flags: append([]cli.Flag{utils.DataDirFlag, - utils.StateExpiryEnableFlag}, utils.NetworkFlags...), + Flags: flags.Merge([]cli.Flag{utils.DataDirFlag}, utils.NetworkFlags, utils.StateExpiryBaseFlags), Description: ` The dumpgenesis command prints the genesis configuration of the network preset if one is set. Otherwise it prints the genesis from the datadir.`, @@ -123,8 +121,7 @@ if one is set. Otherwise it prints the genesis from the datadir.`, utils.TransactionHistoryFlag, utils.StateSchemeFlag, utils.StateHistoryFlag, - utils.StateExpiryEnableFlag, - }, utils.DatabasePathFlags), + }, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: ` The import command imports blocks from an RLP-encoded form. The form can be one file with several RLP-encoded blocks, or several files can be used. @@ -141,8 +138,7 @@ processing will proceed even if an individual RLP-file import failure occurs.`, utils.CacheFlag, utils.SyncModeFlag, utils.StateSchemeFlag, - utils.StateExpiryEnableFlag, - }, utils.DatabasePathFlags), + }, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: ` Requires a first argument of the file to write to. Optional second and third arguments control the first and @@ -158,8 +154,7 @@ be gzipped.`, Flags: flags.Merge([]cli.Flag{ utils.CacheFlag, utils.SyncModeFlag, - utils.StateExpiryEnableFlag, - }, utils.DatabasePathFlags), + }, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: ` The import-preimages command imports hash preimages from an RLP encoded stream. It's deprecated, please use "geth db import" instead. @@ -173,8 +168,7 @@ It's deprecated, please use "geth db import" instead. Flags: flags.Merge([]cli.Flag{ utils.CacheFlag, utils.SyncModeFlag, - utils.StateExpiryEnableFlag, - }, utils.DatabasePathFlags), + }, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: ` The export-preimages command exports hash preimages to an RLP encoded stream. It's deprecated, please use "geth db export" instead. @@ -194,8 +188,7 @@ It's deprecated, please use "geth db export" instead. utils.StartKeyFlag, utils.DumpLimitFlag, utils.StateSchemeFlag, - utils.StateExpiryEnableFlag, - }, utils.DatabasePathFlags), + }, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: ` This command dumps out the state for a given block (or latest, if none provided). `, diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 7c0381b88f..1418ade4dc 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -49,7 +49,7 @@ var ( Name: "dumpconfig", Usage: "Export configuration values in a TOML format", ArgsUsage: "", - Flags: flags.Merge(nodeFlags, rpcFlags, []cli.Flag{utils.StateExpiryEnableFlag}), + Flags: flags.Merge(nodeFlags, rpcFlags, utils.StateExpiryBaseFlags), Description: `Export configuration values in TOML format (to stdout by default).`, } diff --git a/cmd/geth/consolecmd.go b/cmd/geth/consolecmd.go index e5d1e3503f..c6575d247e 100644 --- a/cmd/geth/consolecmd.go +++ b/cmd/geth/consolecmd.go @@ -33,7 +33,7 @@ var ( Action: localConsole, Name: "console", Usage: "Start an interactive JavaScript environment", - Flags: flags.Merge(nodeFlags, rpcFlags, consoleFlags, []cli.Flag{utils.StateExpiryEnableFlag}), + Flags: flags.Merge(nodeFlags, rpcFlags, consoleFlags, utils.StateExpiryBaseFlags), Description: ` The Geth console is an interactive shell for the JavaScript runtime environment which exposes a node admin interface as well as the Ðapp JavaScript API. diff --git a/cmd/geth/dbcmd.go b/cmd/geth/dbcmd.go index 729ea56257..2f176d14fd 100644 --- a/cmd/geth/dbcmd.go +++ b/cmd/geth/dbcmd.go @@ -50,7 +50,7 @@ var ( Name: "removedb", Usage: "Remove blockchain and state databases", ArgsUsage: "", - Flags: flags.Merge(utils.DatabasePathFlags, []cli.Flag{utils.StateExpiryEnableFlag}), + Flags: flags.Merge(utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: ` Remove blockchain and state databases`, } @@ -86,8 +86,7 @@ Remove blockchain and state databases`, ArgsUsage: " ", Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, - utils.StateExpiryEnableFlag, - }, utils.NetworkFlags, utils.DatabasePathFlags), + }, utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Usage: "Inspect the storage size for each type of data in the database", Description: `This commands iterates the entire database. If the optional 'prefix' and 'start' arguments are provided, then the iteration is limited to the given subset of data.`, } @@ -97,8 +96,7 @@ Remove blockchain and state databases`, ArgsUsage: " ", Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, - utils.StateExpiryEnableFlag, - }, utils.DatabasePathFlags), + }, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Usage: "Inspect the MPT tree of the account and contract.", Description: `This commands iterates the entrie WorldState.`, } @@ -106,7 +104,7 @@ Remove blockchain and state databases`, Action: checkStateContent, Name: "check-state-content", ArgsUsage: "", - Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags, []cli.Flag{utils.StateExpiryEnableFlag}), + Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Usage: "Verify that state data is cryptographically correct", Description: `This command iterates the entire database for 32-byte keys, looking for rlp-encoded trie nodes. For each trie node encountered, it checks that the key corresponds to the keccak256(value). If this is not true, this indicates @@ -157,8 +155,7 @@ a data corruption.`, Usage: "Print leveldb statistics", Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, - utils.StateExpiryEnableFlag, - }, utils.NetworkFlags, utils.DatabasePathFlags), + }, utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), } dbCompactCmd = &cli.Command{ Action: dbCompact, @@ -168,8 +165,7 @@ a data corruption.`, utils.SyncModeFlag, utils.CacheFlag, utils.CacheDatabaseFlag, - utils.StateExpiryEnableFlag, - }, utils.NetworkFlags, utils.DatabasePathFlags), + }, utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: `This command performs a database compaction. WARNING: This operation may take a very long time to finish, and may cause database corruption if it is aborted during execution'!`, @@ -181,8 +177,7 @@ corruption if it is aborted during execution'!`, ArgsUsage: "", Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, - utils.StateExpiryEnableFlag, - }, utils.NetworkFlags, utils.DatabasePathFlags), + }, utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: "This command looks up the specified database key from the database.", } dbDeleteCmd = &cli.Command{ @@ -192,8 +187,7 @@ corruption if it is aborted during execution'!`, ArgsUsage: "", Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, - utils.StateExpiryEnableFlag, - }, utils.NetworkFlags, utils.DatabasePathFlags), + }, utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: `This command deletes the specified database key from the database. WARNING: This is a low-level operation which may cause database corruption!`, } @@ -204,8 +198,7 @@ WARNING: This is a low-level operation which may cause database corruption!`, ArgsUsage: " ", Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, - utils.StateExpiryEnableFlag, - }, utils.NetworkFlags, utils.DatabasePathFlags), + }, utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: `This command sets a given database key to the given value. WARNING: This is a low-level operation which may cause database corruption!`, } @@ -217,8 +210,7 @@ WARNING: This is a low-level operation which may cause database corruption!`, Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, utils.StateSchemeFlag, - utils.StateExpiryEnableFlag, - }, utils.NetworkFlags, utils.DatabasePathFlags), + }, utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: "This command looks up the specified database key from the database.", } dbDumpFreezerIndex = &cli.Command{ @@ -228,8 +220,7 @@ WARNING: This is a low-level operation which may cause database corruption!`, ArgsUsage: " ", Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, - utils.StateExpiryEnableFlag, - }, utils.NetworkFlags, utils.DatabasePathFlags), + }, utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: "This command displays information about the freezer index.", } dbImportCmd = &cli.Command{ @@ -239,8 +230,7 @@ WARNING: This is a low-level operation which may cause database corruption!`, ArgsUsage: " ", Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, - utils.StateExpiryEnableFlag, - }, utils.NetworkFlags, utils.DatabasePathFlags), + }, utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: "Exports the specified chain data to an RLP encoded stream, optionally gzip-compressed.", } dbMetadataCmd = &cli.Command{ @@ -260,17 +249,16 @@ WARNING: This is a low-level operation which may cause database corruption!`, Usage: "Shows metadata about the chain status.", Flags: flags.Merge([]cli.Flag{ utils.SyncModeFlag, - utils.StateExpiryEnableFlag, - }, utils.NetworkFlags, utils.DatabasePathFlags), + }, utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: "Shows metadata about the chain status.", } ancientInspectCmd = &cli.Command{ Action: ancientInspect, Name: "inspect-reserved-oldest-blocks", - Flags: []cli.Flag{ - utils.DataDirFlag, - utils.StateExpiryEnableFlag, - }, + Flags: flags.Merge( + []cli.Flag{utils.DataDirFlag}, + utils.StateExpiryBaseFlags, + ), Usage: "Inspect the ancientStore information", Description: `This commands will read current offset from kvdb, which is the current offset and starting BlockNumber of ancientStore, will also displays the reserved number of blocks in ancientStore `, diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 6332506689..d35016e2a8 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -223,6 +223,7 @@ var ( utils.StateExpiryStateEpoch2BlockFlag, utils.StateExpiryStateEpochPeriodFlag, utils.StateExpiryEnableLocalReviveFlag, + utils.StateExpiryEnableRemoteModeFlag, } ) diff --git a/cmd/geth/snapshot.go b/cmd/geth/snapshot.go index 91675e467d..2134418ca7 100644 --- a/cmd/geth/snapshot.go +++ b/cmd/geth/snapshot.go @@ -61,10 +61,9 @@ var ( Flags: flags.Merge([]cli.Flag{ utils.BloomFilterSizeFlag, utils.TriesInMemoryFlag, - utils.StateExpiryEnableFlag, utils.StateExpiryMaxThreadFlag, configFileFlag, - }, utils.NetworkFlags, utils.DatabasePathFlags), + }, utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: ` geth snapshot prune-state will prune historical state data with the help of the state snapshot. @@ -82,14 +81,13 @@ WARNING: it's only supported in hash mode(--state.scheme=hash)". Usage: "Prune block data offline", Action: pruneBlock, Category: "MISCELLANEOUS COMMANDS", - Flags: []cli.Flag{ + Flags: flags.Merge([]cli.Flag{ utils.DataDirFlag, utils.AncientFlag, utils.BlockAmountReserved, utils.TriesInMemoryFlag, utils.CheckSnapshotWithMPT, - utils.StateExpiryEnableFlag, - }, + }, utils.StateExpiryBaseFlags), Description: ` geth offline prune-block for block data in ancientdb. The amount of blocks expected for remaining after prune can be specified via block-amount-reserved in this command, @@ -109,8 +107,7 @@ so it's very necessary to do block data prune, this feature will handle it. Action: verifyState, Flags: flags.Merge([]cli.Flag{ utils.StateSchemeFlag, - utils.StateExpiryEnableFlag, - }, utils.NetworkFlags, utils.DatabasePathFlags), + }, utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: ` geth snapshot verify-state will traverse the whole accounts and storages set based on the specified @@ -125,11 +122,10 @@ In other words, this command does the snapshot to trie conversion. ArgsUsage: "", Action: pruneAllState, Category: "MISCELLANEOUS COMMANDS", - Flags: []cli.Flag{ + Flags: flags.Merge([]cli.Flag{ utils.DataDirFlag, utils.AncientFlag, - utils.StateExpiryEnableFlag, - }, + }, utils.StateExpiryBaseFlags), Description: ` will prune all historical trie state data except genesis block. All trie nodes will be deleted from the database. @@ -147,7 +143,7 @@ the trie clean cache with default directory will be deleted. Usage: "Check that there is no 'dangling' snap storage", ArgsUsage: "", Action: checkDanglingStorage, - Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags, []cli.Flag{utils.StateExpiryEnableFlag}), + Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: ` geth snapshot check-dangling-storage traverses the snap storage data, and verifies that all snapshot storage data has a corresponding account. @@ -158,7 +154,7 @@ data, and verifies that all snapshot storage data has a corresponding account. Usage: "Check all snapshot layers for the a specific account", ArgsUsage: "
", Action: checkAccount, - Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags, []cli.Flag{utils.StateExpiryEnableFlag}), + Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: ` geth snapshot inspect-account
checks all snapshot layers and prints out information about the specified address. @@ -171,8 +167,7 @@ information about the specified address. Action: traverseState, Flags: flags.Merge([]cli.Flag{ utils.StateSchemeFlag, - utils.StateExpiryEnableFlag, - }, utils.NetworkFlags, utils.DatabasePathFlags), + }, utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: ` geth snapshot traverse-state will traverse the whole state from the given state root and will abort if any @@ -189,8 +184,7 @@ It's also usable without snapshot enabled. Action: traverseRawState, Flags: flags.Merge([]cli.Flag{ utils.StateSchemeFlag, - utils.StateExpiryEnableFlag, - }, utils.NetworkFlags, utils.DatabasePathFlags), + }, utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: ` geth snapshot traverse-rawstate will traverse the whole state from the given root and will abort if any referenced @@ -213,8 +207,7 @@ It's also usable without snapshot enabled. utils.DumpLimitFlag, utils.TriesInMemoryFlag, utils.StateSchemeFlag, - utils.StateExpiryEnableFlag, - }, utils.NetworkFlags, utils.DatabasePathFlags), + }, utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: ` This command is semantically equivalent to 'geth dump', but uses the snapshots as the backend data source, making this command a lot faster. diff --git a/cmd/geth/verkle.go b/cmd/geth/verkle.go index 97851b0f85..aebee0c1cd 100644 --- a/cmd/geth/verkle.go +++ b/cmd/geth/verkle.go @@ -45,7 +45,7 @@ var ( Usage: "verify the conversion of a MPT into a verkle tree", ArgsUsage: "", Action: verifyVerkle, - Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags, []cli.Flag{utils.StateExpiryEnableFlag}), + Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: ` geth verkle verify This command takes a root commitment and attempts to rebuild the tree. @@ -56,7 +56,7 @@ This command takes a root commitment and attempts to rebuild the tree. Usage: "Dump a verkle tree to a DOT file", ArgsUsage: " [ ...]", Action: expandVerkle, - Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags, []cli.Flag{utils.StateExpiryEnableFlag}), + Flags: flags.Merge(utils.NetworkFlags, utils.DatabasePathFlags, utils.StateExpiryBaseFlags), Description: ` geth verkle dump [ ...] This command will produce a dot file representing the tree, rooted at . diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index e39252d20f..7687cf87fd 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1118,6 +1118,10 @@ var ( RemoteDBFlag, HttpHeaderFlag, } + StateExpiryBaseFlags = []cli.Flag{ + StateExpiryEnableFlag, + StateExpiryEnableRemoteModeFlag, + } ) var ( @@ -1158,6 +1162,12 @@ var ( Value: 10000, Category: flags.StateExpiryCategory, } + StateExpiryEnableRemoteModeFlag = &cli.BoolFlag{ + Name: "state-expiry.remotemode", + Usage: "set state expiry in remotemode", + Value: false, + Category: flags.StateExpiryCategory, + } ) func init() { @@ -2573,6 +2583,9 @@ func ParseStateExpiryConfig(ctx *cli.Context, disk ethdb.Database, scheme string if ctx.IsSet(StateExpiryEnableFlag.Name) { newCfg.Enable = ctx.Bool(StateExpiryEnableFlag.Name) } + if ctx.IsSet(StateExpiryEnableRemoteModeFlag.Name) { + newCfg.EnableRemoteMode = ctx.Bool(StateExpiryEnableRemoteModeFlag.Name) + } if ctx.IsSet(StateExpiryFullStateEndpointFlag.Name) { newCfg.FullStateEndpoint = ctx.String(StateExpiryFullStateEndpointFlag.Name) } diff --git a/core/state/statedb.go b/core/state/statedb.go index 252df08074..c703bb1b92 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -255,7 +255,7 @@ func (s *StateDB) InitStateExpiryFeature(config *types.StateExpiryConfig, remote } epoch := types.GetStateEpoch(config, expectHeight) s.expiryMeta = &stateExpiryMeta{ - enableStateExpiry: config.Enable, + enableStateExpiry: config.EnableExpiry(), enableLocalRevive: config.EnableLocalRevive, fullStateDB: remote, epoch: epoch, diff --git a/core/types/state_expiry.go b/core/types/state_expiry.go index 3945e4c6f1..35ce7cc8e4 100644 --- a/core/types/state_expiry.go +++ b/core/types/state_expiry.go @@ -22,13 +22,23 @@ type StateExpiryConfig struct { StateEpoch2Block uint64 StateEpochPeriod uint64 EnableLocalRevive bool + EnableRemoteMode bool // when enable remoteDB mode, it will register specific RPC for partial proof and keep sync behind for safety proof } +// EnableExpiry when enable remote mode, it just check param func (s *StateExpiryConfig) EnableExpiry() bool { if s == nil { return false } - return s.Enable + return s.Enable && !s.EnableRemoteMode +} + +// EnableRemote when enable remote mode, it just check param +func (s *StateExpiryConfig) EnableRemote() bool { + if s == nil { + return false + } + return s.Enable && s.EnableRemoteMode } func (s *StateExpiryConfig) Validation() error { @@ -63,6 +73,9 @@ func (s *StateExpiryConfig) CheckCompatible(newCfg *StateExpiryConfig) error { if s.Enable && !newCfg.Enable { return errors.New("disable state expiry is dangerous after enabled, expired state may pruned") } + if s.EnableRemoteMode && !newCfg.EnableRemoteMode { + return errors.New("disable state expiry EnableRemoteMode is dangerous after enabled") + } if err := s.CheckStateEpochCompatible(newCfg.StateEpoch1Block, newCfg.StateEpoch2Block, newCfg.StateEpochPeriod); err != nil { return err @@ -96,8 +109,40 @@ func (s *StateExpiryConfig) CheckStateEpochCompatible(StateEpoch1Block, StateEpo func (s *StateExpiryConfig) String() string { if !s.Enable { - return "State Expiry Disable" + return "State Expiry Disable." + } + if s.Enable && s.EnableRemoteMode { + return "State Expiry Enable in RemoteMode, it will not expired any state." } - return fmt.Sprintf("Enable State Expiry, RemoteEndpoint: %v, StateEpoch: [%v|%v|%v], StateScheme: %v, PruneLevel: %v, EnableLocalRevive: %v", + return fmt.Sprintf("Enable State Expiry, RemoteEndpoint: %v, StateEpoch: [%v|%v|%v], StateScheme: %v, PruneLevel: %v, EnableLocalRevive: %v.", s.FullStateEndpoint, s.StateEpoch1Block, s.StateEpoch2Block, s.StateEpochPeriod, s.StateScheme, s.PruneLevel, s.EnableLocalRevive) } + +// ShouldKeep1EpochBehind when enable state expiry, keep remoteDB behind the latest only 1 epoch blocks +func (s *StateExpiryConfig) ShouldKeep1EpochBehind(remote uint64, local uint64) (bool, uint64) { + if !s.EnableRemoteMode { + return false, remote + } + if remote <= local { + return false, remote + } + + // if in epoch0, just sync + if remote < s.StateEpoch1Block { + return false, remote + } + + // if in epoch1, behind StateEpoch2Block-StateEpoch1Block + if remote < s.StateEpoch2Block { + if remote-(s.StateEpoch2Block-s.StateEpoch1Block) <= local { + return true, 0 + } + return false, remote - (s.StateEpoch2Block - s.StateEpoch1Block) + } + + // if in >= epoch2, behind StateEpochPeriod + if remote-s.StateEpochPeriod <= local { + return true, 0 + } + return false, remote - s.StateEpochPeriod +} diff --git a/eth/backend.go b/eth/backend.go index 19c6c094ec..879757cf04 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -276,7 +276,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { DirectBroadcast: config.DirectBroadcast, DisablePeerTxBroadcast: config.DisablePeerTxBroadcast, PeerSet: peers, - EnableStateExpiry: config.StateExpiryCfg.EnableExpiry(), + expiryConfig: config.StateExpiryCfg, }); err != nil { return nil, err } diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go index 1fe92de837..860d85d540 100644 --- a/eth/downloader/downloader.go +++ b/eth/downloader/downloader.go @@ -103,6 +103,9 @@ type Downloader struct { stateDB ethdb.Database // Database to state sync into (and deduplicate via) + // state expiry + expiryConfig *types.StateExpiryConfig + // Statistics syncStatsChainOrigin uint64 // Origin block number where syncing started at syncStatsChainHeight uint64 // Highest block number known when syncing started @@ -131,8 +134,6 @@ type Downloader struct { SnapSyncer *snap.Syncer // TODO(karalabe): make private! hack for now stateSyncStart chan *stateSync - enableStateExpiry bool - // Cancellation and termination cancelPeer string // Identifier of the peer currently being used as the master (cancel on drop) cancelCh chan struct{} // Channel to cancel mid-flight syncs @@ -241,24 +242,24 @@ func New(stateDb ethdb.Database, mux *event.TypeMux, chain BlockChain, lightchai return dl } -func NewWithExpiry(stateDb ethdb.Database, mux *event.TypeMux, chain BlockChain, lightchain LightChain, enableStateExpiry bool, dropPeer peerDropFn, options ...DownloadOption) *Downloader { +func NewWithExpiry(stateDb ethdb.Database, mux *event.TypeMux, chain BlockChain, lightchain LightChain, expiryConfig *types.StateExpiryConfig, dropPeer peerDropFn, options ...DownloadOption) *Downloader { if lightchain == nil { lightchain = chain } dl := &Downloader{ - stateDB: stateDb, - mux: mux, - queue: newQueue(blockCacheMaxItems, blockCacheInitialItems), - peers: newPeerSet(), - blockchain: chain, - lightchain: lightchain, - dropPeer: dropPeer, - enableStateExpiry: enableStateExpiry, - headerProcCh: make(chan *headerTask, 1), - quitCh: make(chan struct{}), - SnapSyncer: snap.NewSyncerWithStateExpiry(stateDb, chain.TrieDB().Scheme(), enableStateExpiry), - stateSyncStart: make(chan *stateSync), - syncStartBlock: chain.CurrentSnapBlock().Number.Uint64(), + stateDB: stateDb, + mux: mux, + queue: newQueue(blockCacheMaxItems, blockCacheInitialItems), + peers: newPeerSet(), + blockchain: chain, + lightchain: lightchain, + dropPeer: dropPeer, + expiryConfig: expiryConfig, + headerProcCh: make(chan *headerTask, 1), + quitCh: make(chan struct{}), + SnapSyncer: snap.NewSyncerWithStateExpiry(stateDb, chain.TrieDB().Scheme(), expiryConfig.EnableExpiry()), + stateSyncStart: make(chan *stateSync), + syncStartBlock: chain.CurrentSnapBlock().Number.Uint64(), } go dl.stateFetcher() @@ -524,6 +525,15 @@ func (d *Downloader) syncWithPeer(p *peerConnection, hash common.Hash, td, ttd * localHeight = d.lightchain.CurrentHeader().Number.Uint64() } + if d.expiryConfig.EnableRemote() { + var keep bool + keep, remoteHeight = d.expiryConfig.ShouldKeep1EpochBehind(remoteHeight, localHeight) + log.Debug("EnableRemote wait remote more blocks", "remoteHeight", remoteHeader.Number, "request", remoteHeight, "localHeight", localHeight, "config", d.expiryConfig) + if keep { + return errCanceled + } + } + origin, err := d.findAncestor(p, localHeight, remoteHeader) if err != nil { return err @@ -611,9 +621,9 @@ func (d *Downloader) syncWithPeer(p *peerConnection, hash common.Hash, td, ttd * } fetchers := []func() error{ - func() error { return d.fetchHeaders(p, origin+1, remoteHeader.Number.Uint64()) }, // Headers are always retrieved - func() error { return d.fetchBodies(origin+1, beaconMode) }, // Bodies are retrieved during normal and snap sync - func() error { return d.fetchReceipts(origin+1, beaconMode) }, // Receipts are retrieved during snap sync + func() error { return d.fetchHeaders(p, origin+1, remoteHeight) }, // Headers are always retrieved + func() error { return d.fetchBodies(origin+1, beaconMode) }, // Bodies are retrieved during normal and snap sync + func() error { return d.fetchReceipts(origin+1, beaconMode) }, // Receipts are retrieved during snap sync func() error { return d.processHeaders(origin+1, td, ttd, beaconMode) }, } if mode == SnapSync { @@ -1199,6 +1209,15 @@ func (d *Downloader) fetchHeaders(p *peerConnection, from uint64, head uint64) e return errCanceled } from += uint64(len(headers)) + // if EnableRemote, just return if ahead the head + if d.expiryConfig.EnableRemote() && from > head { + select { + case d.headerProcCh <- nil: + return nil + case <-d.cancelCh: + return errCanceled + } + } } // If we're still skeleton filling snap sync, check pivot staleness // before continuing to the next skeleton filling diff --git a/eth/fetcher/block_fetcher.go b/eth/fetcher/block_fetcher.go index b2879cd25f..652fa3c5b5 100644 --- a/eth/fetcher/block_fetcher.go +++ b/eth/fetcher/block_fetcher.go @@ -203,6 +203,9 @@ type BlockFetcher struct { fetchingHook func([]common.Hash) // Method to call upon starting a block (eth/61) or header (eth/62) fetch completingHook func([]common.Hash) // Method to call upon starting a block body fetch (eth/62) importedHook func(*types.Header, *types.Block) // Method to call upon successful header or block import (both eth/61 and eth/62) + + // state expiry + expiryConfig *types.StateExpiryConfig } // NewBlockFetcher creates a block fetcher to retrieve blocks based on hash announcements. @@ -383,6 +386,14 @@ func (f *BlockFetcher) loop() { f.forgetBlock(hash) continue } + + if f.expiryConfig.EnableRemote() { + if keep, _ := f.expiryConfig.ShouldKeep1EpochBehind(number, height); keep { + log.Debug("BlockFetcher EnableRemote wait remote more blocks", "remoteHeight", number, "localHeight", height, "config", f.expiryConfig) + break + } + } + if f.light { f.importHeaders(op) } else { @@ -994,3 +1005,7 @@ func (f *BlockFetcher) forgetBlock(hash common.Hash) { delete(f.queued, hash) } } + +func (f *BlockFetcher) InitExpiryConfig(config *types.StateExpiryConfig) { + f.expiryConfig = config +} diff --git a/eth/handler.go b/eth/handler.go index 72bfd8993e..f784629117 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -123,7 +123,7 @@ type handlerConfig struct { DirectBroadcast bool DisablePeerTxBroadcast bool PeerSet *peerSet - EnableStateExpiry bool + expiryConfig *types.StateExpiryConfig } type handler struct { @@ -135,7 +135,7 @@ type handler struct { acceptTxs atomic.Bool // Flag whether we're considered synchronised (enables transaction processing) directBroadcast bool - enableStateExpiry bool + expiryConfig *types.StateExpiryConfig database ethdb.Database txpool txPool @@ -199,7 +199,7 @@ func newHandler(config *handlerConfig) (*handler, error) { peersPerIP: make(map[string]int), requiredBlocks: config.RequiredBlocks, directBroadcast: config.DirectBroadcast, - enableStateExpiry: config.EnableStateExpiry, + expiryConfig: config.expiryConfig, quitSync: make(chan struct{}), handlerDoneCh: make(chan struct{}), handlerStartCh: make(chan struct{}), @@ -253,7 +253,7 @@ func newHandler(config *handlerConfig) (*handler, error) { downloadOptions = append(downloadOptions, success) */ - h.downloader = downloader.NewWithExpiry(config.Database, h.eventMux, h.chain, nil, config.EnableStateExpiry, h.removePeer, downloadOptions...) + h.downloader = downloader.NewWithExpiry(config.Database, h.eventMux, h.chain, nil, config.expiryConfig, h.removePeer, downloadOptions...) // Construct the fetcher (short sync) validator := func(header *types.Header) error { @@ -339,6 +339,9 @@ func newHandler(config *handlerConfig) (*handler, error) { } h.blockFetcher = fetcher.NewBlockFetcher(false, nil, h.chain.GetBlockByHash, validator, h.BroadcastBlock, heighter, finalizeHeighter, nil, inserter, h.removePeer) + if config.expiryConfig != nil { + h.blockFetcher.InitExpiryConfig(config.expiryConfig) + } fetchTx := func(peer string, hashes []common.Hash) error { p := h.peers.peer(peer) From d24d37953b09270f50493a13683231779154c1de Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Thu, 26 Oct 2023 11:21:28 +0800 Subject: [PATCH 61/65] ethapi: opt storage proof logic; --- core/types/revive_state.go | 25 ++++++++++++++---- core/types/state_expiry.go | 2 +- eth/downloader/downloader.go | 2 +- ethdb/fullstatedb.go | 9 ++++--- internal/ethapi/api.go | 51 +++++++++++------------------------- trie/errors.go | 6 +++-- trie/proof.go | 2 +- trie/trie.go | 8 +++--- 8 files changed, 52 insertions(+), 53 deletions(-) diff --git a/core/types/revive_state.go b/core/types/revive_state.go index 85da6f7cb9..684f1a06b1 100644 --- a/core/types/revive_state.go +++ b/core/types/revive_state.go @@ -1,9 +1,5 @@ package types -import ( - "github.com/ethereum/go-ethereum/common/hexutil" -) - type ReviveStorageProof struct { Key string `json:"key"` PrefixKey string `json:"prefixKey"` @@ -11,6 +7,25 @@ type ReviveStorageProof struct { } type ReviveResult struct { + Err string `json:"err"` StorageProof []ReviveStorageProof `json:"storageProof"` - BlockNum hexutil.Uint64 `json:"blockNum"` + BlockNum uint64 `json:"blockNum"` +} + +func NewReviveErrResult(err error, block uint64) *ReviveResult { + var errRet string + if err != nil { + errRet = err.Error() + } + return &ReviveResult{ + Err: errRet, + BlockNum: block, + } +} + +func NewReviveResult(proof []ReviveStorageProof, block uint64) *ReviveResult { + return &ReviveResult{ + StorageProof: proof, + BlockNum: block, + } } diff --git a/core/types/state_expiry.go b/core/types/state_expiry.go index 35ce7cc8e4..ed2bb18e43 100644 --- a/core/types/state_expiry.go +++ b/core/types/state_expiry.go @@ -22,7 +22,7 @@ type StateExpiryConfig struct { StateEpoch2Block uint64 StateEpochPeriod uint64 EnableLocalRevive bool - EnableRemoteMode bool // when enable remoteDB mode, it will register specific RPC for partial proof and keep sync behind for safety proof + EnableRemoteMode bool `rlp:"optional"` // when enable remoteDB mode, it will register specific RPC for partial proof and keep sync behind for safety proof } // EnableExpiry when enable remote mode, it just check param diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go index 860d85d540..0c388dd9c8 100644 --- a/eth/downloader/downloader.go +++ b/eth/downloader/downloader.go @@ -528,7 +528,7 @@ func (d *Downloader) syncWithPeer(p *peerConnection, hash common.Hash, td, ttd * if d.expiryConfig.EnableRemote() { var keep bool keep, remoteHeight = d.expiryConfig.ShouldKeep1EpochBehind(remoteHeight, localHeight) - log.Debug("EnableRemote wait remote more blocks", "remoteHeight", remoteHeader.Number, "request", remoteHeight, "localHeight", localHeight, "config", d.expiryConfig) + log.Debug("EnableRemote wait remote more blocks", "remoteHeight", remoteHeader.Number, "request", remoteHeight, "localHeight", localHeight, "keep", keep, "config", d.expiryConfig) if keep { return errCanceled } diff --git a/ethdb/fullstatedb.go b/ethdb/fullstatedb.go index 373c2d734b..500beac1fb 100644 --- a/ethdb/fullstatedb.go +++ b/ethdb/fullstatedb.go @@ -93,15 +93,16 @@ func (f *FullStateRPCServer) GetStorageReviveProof(stateRoot common.Hash, accoun if err != nil { return nil, fmt.Errorf("failed to get storage revive proof, err: %v, remote's block number: %v", err, result.BlockNum) } - - proofs := result.StorageProof + if len(result.Err) > 0 { + return nil, fmt.Errorf("failed to get storage revive proof, err: %v, remote's block number: %v", result.Err, result.BlockNum) + } // add to cache - for _, proof := range proofs { + for _, proof := range result.StorageProof { f.cache.Add(ProofCacheKey(account, root, proof.PrefixKey, proof.Key), proof) } - ret = append(ret, proofs...) + ret = append(ret, result.StorageProof...) return ret, err } diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index d0ceb2c55a..49148619fe 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -51,7 +51,6 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" - "github.com/ethereum/go-ethereum/trie" lru "github.com/hashicorp/golang-lru" "github.com/tyler-smith/go-bip39" ) @@ -789,57 +788,42 @@ func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, stateRoot com } var ( - blockNum hexutil.Uint64 - err error - stateDb *state.StateDB - header *types.Header - storageTrie state.Trie + blockNum uint64 keys = make([]common.Hash, len(storageKeys)) keyLengths = make([]int, len(storageKeys)) prefixKeys = make([][]byte, len(storagePrefixKeys)) storageProof = make([]types.ReviveStorageProof, len(storageKeys)) ) - openStorageTrie := func(stateDb *state.StateDB, header *types.Header, address common.Address) (state.Trie, error) { - id := trie.StorageTrieID(header.Root, crypto.Keccak256Hash(address.Bytes()), root) - tr, err := trie.NewStateTrie(id, stateDb.Database().TrieDB()) + // try open target state root trie + storageTrie, err := s.b.StorageTrie(stateRoot, address, root) + if err != nil { + // if there cannot find target state root, try latest trie + stateDb, header, err := s.b.StateAndHeaderByNumber(ctx, rpc.LatestBlockNumber) if err != nil { return nil, err } - return tr, nil - } - - stateDb, header, _ = s.b.StateAndHeaderByNumber(ctx, rpc.LatestBlockNumber) - blockNum = hexutil.Uint64(header.Number.Uint64()) - - storageTrie, err = s.b.StorageTrie(stateRoot, address, root) - if (err != nil || storageTrie == nil) && stateDb != nil { - storageTrie, err = openStorageTrie(stateDb, header, address) - log.Info("GetStorageReviveProof from latest block number", "blockNum", blockNum, "blockHash", header.Hash().Hex()) - } - - if err != nil || storageTrie == nil { - return &types.ReviveResult{ - StorageProof: nil, - BlockNum: blockNum, - }, err + blockNum = header.Number.Uint64() + storageTrie, err = stateDb.StorageTrie(address) + if err != nil { + return types.NewReviveErrResult(err, blockNum), nil + } + log.Debug("GetStorageReviveProof from latest block number", "blockNum", blockNum, "blockHash", header.Hash()) } // Deserialize all keys. This prevents state access on invalid input. for i, hexKey := range storageKeys { - var err error keys[i], keyLengths[i], err = decodeHash(hexKey) if err != nil { - return nil, err + return types.NewReviveErrResult(err, blockNum), nil } } // Decode prefix keys for i, prefixKey := range storagePrefixKeys { - var err error prefixKeys[i], err = hex.DecodeString(prefixKey) if err != nil { - return nil, err + return types.NewReviveErrResult(err, blockNum), nil } } @@ -867,7 +851,7 @@ func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, stateRoot com } if err := storageTrie.ProveByPath(crypto.Keccak256(key.Bytes()), prefixKey, &proof); err != nil { - return nil, err + return types.NewReviveErrResult(err, blockNum), nil } storageProof[i] = types.ReviveStorageProof{ Key: outputKey, @@ -877,10 +861,7 @@ func (s *BlockChainAPI) GetStorageReviveProof(ctx context.Context, stateRoot com s.cache.Add(ethdb.ProofCacheKey(address, root, storagePrefixKeys[i], storageKeys[i]), storageProof[i]) } - return &types.ReviveResult{ - StorageProof: storageProof, - BlockNum: blockNum, - }, nil + return types.NewReviveResult(storageProof, blockNum), nil } // decodeHash parses a hex-encoded 32-byte hash. The input may optionally diff --git a/trie/errors.go b/trie/errors.go index 0c1b785bf0..0be1ac0778 100644 --- a/trie/errors.go +++ b/trie/errors.go @@ -71,15 +71,17 @@ func (e *ReviveNotExpiredError) Error() string { type ExpiredNodeError struct { Path []byte // hex-encoded path to the expired node Epoch types.StateEpoch + Node node } -func NewExpiredNodeError(path []byte, epoch types.StateEpoch) error { +func NewExpiredNodeError(path []byte, epoch types.StateEpoch, n node) error { return &ExpiredNodeError{ Path: path, Epoch: epoch, + Node: n, } } func (err *ExpiredNodeError) Error() string { - return fmt.Sprintf("expired trie node, path: %v, epoch: %v", err.Path, err.Epoch) + return fmt.Sprintf("expired trie node, path: %v, epoch: %v, node: %v", err.Path, err.Epoch, err.Node.fstring("")) } diff --git a/trie/proof.go b/trie/proof.go index 6b8a080a1e..cc19b84e37 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -57,7 +57,7 @@ func (t *Trie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error { } for len(key) > 0 && tn != nil { if t.enableExpiry && t.epochExpired(tn, nodeEpoch) { - return NewExpiredNodeError(prefix, nodeEpoch) + return NewExpiredNodeError(prefix, nodeEpoch, tn) } switch n := tn.(type) { case *shortNode: diff --git a/trie/trie.go b/trie/trie.go index 75f0ef5e4d..39b1dbd4ca 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -260,7 +260,7 @@ func (t *Trie) get(origNode node, key []byte, pos int) (value []byte, newnode no func (t *Trie) getWithEpoch(origNode node, key []byte, pos int, epoch types.StateEpoch, updateEpoch bool) (value []byte, newnode node, didResolve bool, err error) { if t.epochExpired(origNode, epoch) { - return nil, nil, false, NewExpiredNodeError(key[:pos], epoch) + return nil, nil, false, NewExpiredNodeError(key[:pos], epoch, origNode) } switch n := (origNode).(type) { case nil: @@ -596,7 +596,7 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error func (t *Trie) insertWithEpoch(n node, prefix, key []byte, value node, epoch types.StateEpoch) (bool, node, error) { if t.epochExpired(n, epoch) { - return false, nil, NewExpiredNodeError(prefix, epoch) + return false, nil, NewExpiredNodeError(prefix, epoch, n) } if len(key) == 0 { @@ -865,7 +865,7 @@ func (t *Trie) delete(n node, prefix, key []byte) (bool, node, error) { func (t *Trie) deleteWithEpoch(n node, prefix, key []byte, epoch types.StateEpoch) (bool, node, error) { if t.epochExpired(n, epoch) { - return false, nil, NewExpiredNodeError(prefix, epoch) + return false, nil, NewExpiredNodeError(prefix, epoch, n) } switch n := n.(type) { @@ -1352,7 +1352,7 @@ func (t *Trie) tryRevive(n node, key []byte, targetPrefixKey []byte, nub MPTProo } if isExpired { - return nil, false, NewExpiredNodeError(key[:pos], epoch) + return nil, false, NewExpiredNodeError(key[:pos], epoch, n) } switch n := n.(type) { From 52d8606a9f3d9875f2ca2ed8150671d0dc0bc28c Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Mon, 30 Oct 2023 16:11:06 +0800 Subject: [PATCH 62/65] state/stateobject: add dirtystorage touch & revive logic; trie: fix local review leaf expand bug; --- core/state/state_expiry.go | 6 ------ core/state/state_object.go | 18 ++++++++++++++++++ trie/trie_expiry.go | 8 +++++++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index d0742ff65f..75b1b9ec60 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -41,9 +41,6 @@ func fetchExpiredStorageFromRemote(meta *stateExpiryMeta, addr common.Address, r // if there need revive expired state, try to revive locally, when the node is not being pruned, just renew the epoch val, err := tr.TryLocalRevive(addr, key.Bytes()) //log.Debug("fetchExpiredStorageFromRemote TryLocalRevive", "addr", addr, "key", key, "val", val, "err", err) - if _, ok := err.(*trie.MissingNodeError); !ok { - return nil, err - } switch err.(type) { case *trie.MissingNodeError: // cannot revive locally, request from remote @@ -86,9 +83,6 @@ func batchFetchExpiredFromRemote(expiryMeta *stateExpiryMeta, addr common.Addres for i, key := range keys { val, err := tr.TryLocalRevive(addr, key.Bytes()) //log.Debug("fetchExpiredStorageFromRemote TryLocalRevive", "addr", addr, "key", key, "val", val, "err", err) - if _, ok := err.(*trie.MissingNodeError); !ok { - return nil, err - } switch err.(type) { case *trie.MissingNodeError: expiredKeys = append(expiredKeys, key) diff --git a/core/state/state_object.go b/core/state/state_object.go index 317a70495e..e137eb4a36 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -500,6 +500,24 @@ func (s *stateObject) updateTrie() (Trie, error) { } //log.Debug("updateTrie pendingFutureReviveState", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "tr.epoch", tr.Epoch(), "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) } + // TODO(0xbundler): find some trie node with wrong epoch, temporary add get op, will fix later + //for key, val := range dirtyStorage { + // _, err = tr.GetStorage(s.address, key.Bytes()) + // if err == nil { + // continue + // } + // log.Error("EnableExpire GetStorage error", "addr", s.address, "key", key, "val", val, "origin", s.originStorage[key], "err", err) + // enErr, ok := err.(*trie.ExpiredNodeError) + // if !ok { + // s.db.setError(fmt.Errorf("state object dirtyStorage err, contract: %v, key: %v, err: %v", s.address, key, err)) + // continue + // } + // if _, err = fetchExpiredStorageFromRemote(s.db.expiryMeta, s.address, s.data.Root, tr, enErr.Path, key); err != nil { + // log.Error("EnableExpire GetStorage fetchExpiredStorageFromRemote error", "addr", s.address, "key", key, "val", val, "origin", s.originStorage[key], "err", err) + // s.db.setError(fmt.Errorf("state object dirtyStorage fetchExpiredStorageFromRemote err, contract: %v, key: %v, path: %v, err: %v", s.address, key, enErr.Path, err)) + // } + // //log.Debug("updateTrie dirtyStorage", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "tr.epoch", tr.Epoch(), "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) + //} } for key, value := range dirtyStorage { if len(value) == 0 { diff --git a/trie/trie_expiry.go b/trie/trie_expiry.go index 94ee7927ee..4a31086574 100644 --- a/trie/trie_expiry.go +++ b/trie/trie_expiry.go @@ -30,7 +30,13 @@ func (t *Trie) tryLocalRevive(origNode node, key []byte, pos int, epoch types.St return n, n, expired, nil case *shortNode: if len(key)-pos < len(n.Key) || !bytes.Equal(n.Key, key[pos:pos+len(n.Key)]) { - // key not found in trie + // key not found in trie, but just revive for expand + if t.renewNode(epoch, false, expired) { + n = n.copy() + n.setEpoch(t.currentEpoch) + n.flags = t.newFlag() + return nil, n, true, nil + } return nil, n, false, nil } value, newnode, didResolve, err := t.tryLocalRevive(n.Val, key, pos+len(n.Key), epoch) From 40a6e3094d49425b46cdef0833d7b42322de5c42 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Wed, 1 Nov 2023 14:52:43 +0800 Subject: [PATCH 63/65] state/stateobject: fix revive in diff & delete shrink miss node bug; trie: support revive from prefix; --- core/state/state_expiry.go | 21 +++--- core/state/state_object.go | 70 +++++++++++++------ core/state/trie_prefetcher.go | 6 +- eth/protocols/snap/handler.go | 8 +-- ethclient/gethclient/gethclient.go | 2 +- trie/errors.go | 14 ++++ trie/trie.go | 108 ++++++++++++----------------- 7 files changed, 124 insertions(+), 105 deletions(-) diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index 75b1b9ec60..2779e13e41 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -33,22 +33,23 @@ func defaultStateExpiryMeta() *stateExpiryMeta { return &stateExpiryMeta{enableStateExpiry: false} } -// fetchExpiredStorageFromRemote request expired state from remote full state node; -func fetchExpiredStorageFromRemote(meta *stateExpiryMeta, addr common.Address, root common.Hash, tr Trie, prefixKey []byte, key common.Hash) (map[string][]byte, error) { +// tryReviveState request expired state from remote full state node; +func tryReviveState(meta *stateExpiryMeta, addr common.Address, root common.Hash, tr Trie, prefixKey []byte, key common.Hash, force bool) (map[string][]byte, error) { + if !meta.enableStateExpiry { + return nil, nil + } //log.Debug("fetching expired storage from remoteDB", "addr", addr, "prefix", prefixKey, "key", key) reviveTrieMeter.Mark(1) - if meta.enableLocalRevive { + if meta.enableLocalRevive && !force { // if there need revive expired state, try to revive locally, when the node is not being pruned, just renew the epoch val, err := tr.TryLocalRevive(addr, key.Bytes()) - //log.Debug("fetchExpiredStorageFromRemote TryLocalRevive", "addr", addr, "key", key, "val", val, "err", err) + //log.Debug("tryReviveState TryLocalRevive", "addr", addr, "key", key, "val", val, "err", err) switch err.(type) { case *trie.MissingNodeError: // cannot revive locally, request from remote case nil: reviveFromLocalMeter.Mark(1) - ret := make(map[string][]byte, 1) - ret[key.String()] = val - return ret, nil + return map[string][]byte{key.String(): val}, nil default: return nil, err } @@ -57,7 +58,7 @@ func fetchExpiredStorageFromRemote(meta *stateExpiryMeta, addr common.Address, r reviveFromRemoteMeter.Mark(1) // cannot revive locally, fetch remote proof proofs, err := meta.fullStateDB.GetStorageReviveProof(meta.originalRoot, addr, root, []string{common.Bytes2Hex(prefixKey)}, []string{common.Bytes2Hex(key[:])}) - //log.Debug("fetchExpiredStorageFromRemote GetStorageReviveProof", "addr", addr, "key", key, "proofs", len(proofs), "err", err) + //log.Debug("tryReviveState GetStorageReviveProof", "addr", addr, "key", key, "proofs", len(proofs), "err", err) if err != nil { return nil, err } @@ -82,7 +83,7 @@ func batchFetchExpiredFromRemote(expiryMeta *stateExpiryMeta, addr common.Addres var expiredPrefixKeys [][]byte for i, key := range keys { val, err := tr.TryLocalRevive(addr, key.Bytes()) - //log.Debug("fetchExpiredStorageFromRemote TryLocalRevive", "addr", addr, "key", key, "val", val, "err", err) + //log.Debug("tryReviveState TryLocalRevive", "addr", addr, "key", key, "val", val, "err", err) switch err.(type) { case *trie.MissingNodeError: expiredKeys = append(expiredKeys, key) @@ -119,7 +120,7 @@ func batchFetchExpiredFromRemote(expiryMeta *stateExpiryMeta, addr common.Addres // cannot revive locally, fetch remote proof reviveFromRemoteMeter.Mark(int64(len(keysStr))) proofs, err := expiryMeta.fullStateDB.GetStorageReviveProof(expiryMeta.originalRoot, addr, root, prefixKeysStr, keysStr) - //log.Debug("fetchExpiredStorageFromRemote GetStorageReviveProof", "addr", addr, "keys", keysStr, "prefixKeys", prefixKeysStr, "proofs", len(proofs), "err", err) + //log.Debug("tryReviveState GetStorageReviveProof", "addr", addr, "keys", keysStr, "prefixKeys", prefixKeysStr, "proofs", len(proofs), "err", err) if err != nil { return nil, err } diff --git a/core/state/state_object.go b/core/state/state_object.go index e137eb4a36..aa70191a4c 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -316,9 +316,9 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { } // handle state expiry situation if s.db.EnableExpire() { - if enErr, ok := err.(*trie.ExpiredNodeError); ok { + if path, ok := trie.ParseExpiredNodeErr(err); ok { //log.Debug("GetCommittedState expired in trie", "addr", s.address, "key", key, "err", err) - val, err = s.fetchExpiredFromRemote(enErr.Path, key, false) + val, err = s.tryReviveState(path, key, false) getCommittedStorageExpiredMeter.Mark(1) } else if err != nil { getCommittedStorageUnexpiredMeter.Mark(1) @@ -490,13 +490,14 @@ func (s *stateObject) updateTrie() (Trie, error) { if err == nil { continue } - enErr, ok := err.(*trie.ExpiredNodeError) + path, ok := trie.ParseExpiredNodeErr(err) if !ok { - s.db.setError(fmt.Errorf("state object pendingFutureReviveState err, contract: %v, key: %v, err: %v", s.address, key, err)) + s.db.setError(fmt.Errorf("updateTrie pendingFutureReviveState err, contract: %v, key: %v, err: %v", s.address, key, err)) + //log.Debug("updateTrie pendingFutureReviveState", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "tr.epoch", tr.Epoch(), "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s), "err", err) continue } - if _, err = fetchExpiredStorageFromRemote(s.db.expiryMeta, s.address, s.data.Root, tr, enErr.Path, key); err != nil { - s.db.setError(fmt.Errorf("state object pendingFutureReviveState fetchExpiredStorageFromRemote err, contract: %v, key: %v, path: %v, err: %v", s.address, key, enErr.Path, err)) + if _, err = tryReviveState(s.db.expiryMeta, s.address, s.data.Root, tr, path, key, false); err != nil { + s.db.setError(fmt.Errorf("updateTrie pendingFutureReviveState tryReviveState err, contract: %v, key: %v, path: %v, err: %v", s.address, key, path, err)) } //log.Debug("updateTrie pendingFutureReviveState", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "tr.epoch", tr.Epoch(), "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) } @@ -507,35 +508,57 @@ func (s *stateObject) updateTrie() (Trie, error) { // continue // } // log.Error("EnableExpire GetStorage error", "addr", s.address, "key", key, "val", val, "origin", s.originStorage[key], "err", err) - // enErr, ok := err.(*trie.ExpiredNodeError) + // path, ok := trie.ParseExpiredNodeErr(err) // if !ok { // s.db.setError(fmt.Errorf("state object dirtyStorage err, contract: %v, key: %v, err: %v", s.address, key, err)) // continue // } - // if _, err = fetchExpiredStorageFromRemote(s.db.expiryMeta, s.address, s.data.Root, tr, enErr.Path, key); err != nil { - // log.Error("EnableExpire GetStorage fetchExpiredStorageFromRemote error", "addr", s.address, "key", key, "val", val, "origin", s.originStorage[key], "err", err) - // s.db.setError(fmt.Errorf("state object dirtyStorage fetchExpiredStorageFromRemote err, contract: %v, key: %v, path: %v, err: %v", s.address, key, enErr.Path, err)) + // if _, err = tryReviveState(s.db.expiryMeta, s.address, s.data.Root, tr, path key); err != nil { + // log.Error("EnableExpire GetStorage tryReviveState error", "addr", s.address, "key", key, "val", val, "origin", s.originStorage[key], "err", err) + // s.db.setError(fmt.Errorf("state object dirtyStorage tryReviveState err, contract: %v, key: %v, path: %v, err: %v", s.address, key, enErr.Path, err)) // } // //log.Debug("updateTrie dirtyStorage", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "tr.epoch", tr.Epoch(), "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) //} } + touchExpiredStorage := make(map[common.Hash][]byte) for key, value := range dirtyStorage { if len(value) == 0 { - if err := tr.DeleteStorage(s.address, key[:]); err != nil { - s.db.setError(fmt.Errorf("state object update trie DeleteStorage err, contract: %v, key: %v, err: %v", s.address, key, err)) + err := tr.DeleteStorage(s.address, key[:]) + if path, ok := trie.ParseExpiredNodeErr(err); ok { + touchExpiredStorage[key] = value + if _, err = tryReviveState(s.db.expiryMeta, s.address, s.data.Root, tr, path, key, true); err != nil { + s.db.setError(fmt.Errorf("updateTrie DeleteStorage tryReviveState err, contract: %v, key: %v, path: %v, err: %v", s.address, key, path, err)) + } + } else if err != nil { + s.db.setError(fmt.Errorf("updateTrie DeleteStorage err, contract: %v, key: %v, err: %v", s.address, key, err)) } - //log.Debug("updateTrie DeleteStorage", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "value", value, "tr.epoch", tr.Epoch(), "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) + //log.Debug("updateTrie DeleteStorage", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "value", value, "tr.epoch", tr.Epoch(), "err", err, "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) s.db.StorageDeleted += 1 } else { if err := tr.UpdateStorage(s.address, key[:], value); err != nil { - s.db.setError(fmt.Errorf("state object update trie UpdateStorage err, contract: %v, key: %v, err: %v", s.address, key, err)) + s.db.setError(fmt.Errorf("updateTrie UpdateStorage err, contract: %v, key: %v, err: %v", s.address, key, err)) } - //log.Debug("updateTrie UpdateStorage", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "value", value, "tr.epoch", tr.Epoch(), "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) + //log.Debug("updateTrie UpdateStorage", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "value", value, "tr.epoch", tr.Epoch(), "err", err, "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) s.db.StorageUpdated += 1 } // Cache the items for preloading usedStorage = append(usedStorage, common.CopyBytes(key[:])) } + + // re-execute touched expired storage + for key, value := range touchExpiredStorage { + if len(value) == 0 { + if err := tr.DeleteStorage(s.address, key[:]); err != nil { + s.db.setError(fmt.Errorf("updateTrie DeleteStorage in touchExpiredStorage err, contract: %v, key: %v, err: %v", s.address, key, err)) + } + //log.Debug("updateTrie DeleteStorage in touchExpiredStorage", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "value", value, "tr.epoch", tr.Epoch(), "err", err, "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) + } else { + if err := tr.UpdateStorage(s.address, key[:], value); err != nil { + s.db.setError(fmt.Errorf("updateTrie UpdateStorage in touchExpiredStorage err, contract: %v, key: %v, err: %v", s.address, key, err)) + } + //log.Debug("updateTrie UpdateStorage in touchExpiredStorage", "contract", s.address, "key", key, "epoch", s.db.Epoch(), "value", value, "tr.epoch", tr.Epoch(), "err", err, "tr", fmt.Sprintf("%p", tr), "ins", fmt.Sprintf("%p", s)) + } + } }() // If state snapshotting is active, cache the data til commit wg.Add(1) @@ -873,8 +896,8 @@ func (s *stateObject) queryFromReviveState(reviveState map[string]common.Hash, k return val, ok } -// fetchExpiredStorageFromRemote request expired state from remote full state node; -func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash, resolvePath bool) ([]byte, error) { +// tryReviveState request expired state from remote full state node; +func (s *stateObject) tryReviveState(prefixKey []byte, key common.Hash, resolvePath bool) ([]byte, error) { tr, err := s.getPendingReviveTrie() if err != nil { return nil, err @@ -883,19 +906,19 @@ func (s *stateObject) fetchExpiredFromRemote(prefixKey []byte, key common.Hash, // if no prefix, query from revive trie, got the newest expired info if resolvePath { val, err := tr.GetStorage(s.address, key.Bytes()) - // TODO(asyukii): temporary fix snap expired, but trie not expire, may investigate more later. - if val != nil { + if err == nil { + // TODO(asyukii): temporary fix snap expired, but trie not expire, may investigate more later. s.pendingReviveState[string(crypto.Keccak256(key[:]))] = common.BytesToHash(val) return val, nil } - enErr, ok := err.(*trie.ExpiredNodeError) + path, ok := trie.ParseExpiredNodeErr(err) if !ok { return nil, fmt.Errorf("cannot find expired state from trie, err: %v", err) } - prefixKey = enErr.Path + prefixKey = path } - kvs, err := fetchExpiredStorageFromRemote(s.db.expiryMeta, s.address, s.data.Root, tr, prefixKey, key) + kvs, err := tryReviveState(s.db.expiryMeta, s.address, s.data.Root, tr, prefixKey, key, false) if err != nil { return nil, err } @@ -924,6 +947,7 @@ func (s *stateObject) getExpirySnapStorage(key common.Hash) ([]byte, error, erro if val == nil { // record access empty kv, try touch in updateTrie for duplication + //log.Debug("getExpirySnapStorage nil val", "addr", s.address, "key", key, "val", val) s.futureReviveState(key) return nil, nil, nil } @@ -945,7 +969,7 @@ func (s *stateObject) getExpirySnapStorage(key common.Hash) ([]byte, error, erro //log.Debug("GetCommittedState expired in snapshot", "addr", s.address, "key", key, "val", val, "enc", enc, "err", err) // handle from remoteDB, if got err just setError, or return to revert in consensus version. - valRaw, err := s.fetchExpiredFromRemote(nil, key, true) + valRaw, err := s.tryReviveState(nil, key, true) if err != nil { return nil, nil, err } diff --git a/core/state/trie_prefetcher.go b/core/state/trie_prefetcher.go index 91307bd917..ccd62c395b 100644 --- a/core/state/trie_prefetcher.go +++ b/core/state/trie_prefetcher.go @@ -576,11 +576,11 @@ func (sf *subfetcher) loop() { // handle expired state if sf.expiryMeta.enableStateExpiry { // TODO(0xbundler): revert to single fetch, because tasks is a channel - if exErr, match := err.(*trie2.ExpiredNodeError); match { + if path, match := trie2.ParseExpiredNodeErr(err); match { key := common.BytesToHash(task) - _, err = fetchExpiredStorageFromRemote(sf.expiryMeta, sf.addr, sf.root, sf.trie, exErr.Path, key) + _, err = tryReviveState(sf.expiryMeta, sf.addr, sf.root, sf.trie, path, key, false) if err != nil { - log.Error("subfetcher fetchExpiredStorageFromRemote err", "addr", sf.addr, "path", exErr.Path, "err", err) + log.Error("subfetcher tryReviveState err", "addr", sf.addr, "path", path, "err", err) } } } diff --git a/eth/protocols/snap/handler.go b/eth/protocols/snap/handler.go index 0eaf6a28e4..e60fc0fafc 100644 --- a/eth/protocols/snap/handler.go +++ b/eth/protocols/snap/handler.go @@ -445,8 +445,8 @@ func ServiceGetStorageRangesQuery(chain *core.BlockChain, req *GetStorageRangesP } proof := light.NewNodeSet() if err := stTrie.Prove(origin[:], proof); err != nil { - if enErr, ok := err.(*trie.ExpiredNodeError); ok { - err := reviveAndGetProof(chain.FullStateDB(), stTrie, req.Root, common.BytesToAddress(account[:]), acc.Root, enErr.Path, origin, proof) + if path, ok := trie.ParseExpiredNodeErr(err); ok { + err := reviveAndGetProof(chain.FullStateDB(), stTrie, req.Root, common.BytesToAddress(account[:]), acc.Root, path, origin, proof) if err != nil { log.Warn("Failed to prove storage range", "origin", origin, "err", err) return nil, nil @@ -455,8 +455,8 @@ func ServiceGetStorageRangesQuery(chain *core.BlockChain, req *GetStorageRangesP } if last != (common.Hash{}) { if err := stTrie.Prove(last[:], proof); err != nil { - if enErr, ok := err.(*trie.ExpiredNodeError); ok { - err := reviveAndGetProof(chain.FullStateDB(), stTrie, req.Root, common.BytesToAddress(account[:]), acc.Root, enErr.Path, last, proof) + if path, ok := trie.ParseExpiredNodeErr(err); ok { + err := reviveAndGetProof(chain.FullStateDB(), stTrie, req.Root, common.BytesToAddress(account[:]), acc.Root, path, last, proof) if err != nil { log.Warn("Failed to prove storage range", "origin", origin, "err", err) return nil, nil diff --git a/ethclient/gethclient/gethclient.go b/ethclient/gethclient/gethclient.go index 7b9c8587fd..87f93d1625 100644 --- a/ethclient/gethclient/gethclient.go +++ b/ethclient/gethclient/gethclient.go @@ -140,7 +140,7 @@ func (ec *Client) GetStorageReviveProof(ctx context.Context, stateRoot common.Ha return &types.ReviveResult{ StorageProof: res.StorageProof, - BlockNum: res.BlockNum, + BlockNum: uint64(res.BlockNum), }, err } diff --git a/trie/errors.go b/trie/errors.go index 0be1ac0778..0ef3187ebd 100644 --- a/trie/errors.go +++ b/trie/errors.go @@ -85,3 +85,17 @@ func NewExpiredNodeError(path []byte, epoch types.StateEpoch, n node) error { func (err *ExpiredNodeError) Error() string { return fmt.Sprintf("expired trie node, path: %v, epoch: %v, node: %v", err.Path, err.Epoch, err.Node.fstring("")) } + +func ParseExpiredNodeErr(err error) ([]byte, bool) { + var path []byte + switch enErr := err.(type) { + case *ExpiredNodeError: + path = enErr.Path + case *MissingNodeError: // when meet MissingNodeError, try revive or fail + path = enErr.Path + default: + return nil, false + } + + return path, true +} diff --git a/trie/trie.go b/trie/trie.go index 39b1dbd4ca..d7149b23a5 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -314,38 +314,29 @@ func (t *Trie) getWithEpoch(origNode node, key []byte, pos int, epoch types.Stat } } -// updateChildNodeEpoch traverses the trie and update the node epoch for each accessed trie node. +// refreshNubEpoch traverses the trie and update the node epoch for each accessed trie node. // Under an expiry scheme where a hash node is accessed, its parent node's epoch will not be updated. -func (t *Trie) updateChildNodeEpoch(origNode node, key []byte, pos int, epoch types.StateEpoch) (newnode node, updateEpoch bool, err error) { +func refreshNubEpoch(origNode node, epoch types.StateEpoch) node { switch n := (origNode).(type) { case nil: - return nil, false, nil + return nil case valueNode: - return n, true, nil + return n case *shortNode: - if len(key)-pos < len(n.Key) || !bytes.Equal(n.Key, key[pos:pos+len(n.Key)]) { - return n, false, nil - } - newnode, updateEpoch, err = t.updateChildNodeEpoch(n.Val, key, pos+len(n.Key), epoch) - if err == nil && updateEpoch { - n = n.copy() - n.Val = newnode - n.setEpoch(t.currentEpoch) - n.flags = t.newFlag() - } - return n, true, err + n.Val = refreshNubEpoch(n.Val, epoch) + n.setEpoch(epoch) + n.flags = nodeFlag{dirty: true} + return n case *fullNode: - newnode, updateEpoch, err = t.updateChildNodeEpoch(n.Children[key[pos]], key, pos+1, epoch) - if err == nil && updateEpoch { - n = n.copy() - n.Children[key[pos]] = newnode - n.setEpoch(t.currentEpoch) - n.UpdateChildEpoch(int(key[pos]), t.currentEpoch) - n.flags = t.newFlag() + for i := 0; i < BranchNodeLength-1; i++ { + n.Children[i] = refreshNubEpoch(n.Children[i], epoch) + n.UpdateChildEpoch(i, epoch) } - return n, true, err + n.setEpoch(epoch) + n.flags = nodeFlag{dirty: true} + return n case hashNode: - return n, false, err + return n default: panic(fmt.Sprintf("%T: invalid node: %v", origNode, origNode)) } @@ -954,6 +945,7 @@ func (t *Trie) deleteWithEpoch(n node, prefix, key []byte, epoch types.StateEpoc // shortNode{..., shortNode{...}}. Since the entry // might not be loaded yet, resolve it just for this // check. + // Attention: if Children[pos] has pruned, just fetch from remote cnode, err := t.resolve(n.Children[pos], append(prefix, byte(pos)), n.GetChildEpoch(pos)) if err != nil { return false, nil, err @@ -1283,6 +1275,7 @@ func (t *Trie) TryRevive(key []byte, proof []*MPTProofNub) ([]*MPTProofNub, erro for _, nub := range proof { rootExpired := types.EpochExpired(t.getRootEpoch(), t.currentEpoch) newNode, didRevive, err := t.tryRevive(t.root, key, nub.n1PrefixKey, *nub, 0, t.currentEpoch, rootExpired) + //log.Debug("tryRevive", "key", key, "nub.n1PrefixKey", nub.n1PrefixKey, "nub", nub, "err", err) if _, ok := err.(*ReviveNotExpiredError); ok { reviveNotExpiredMeter.Mark(1) continue @@ -1300,6 +1293,7 @@ func (t *Trie) TryRevive(key []byte, proof []*MPTProofNub) ([]*MPTProofNub, erro return successNubs, nil } +// tryRevive it just revive from targetPrefixKey func (t *Trie) tryRevive(n node, key []byte, targetPrefixKey []byte, nub MPTProofNub, pos int, epoch types.StateEpoch, isExpired bool) (node, bool, error) { if pos > len(targetPrefixKey) { @@ -1307,9 +1301,8 @@ func (t *Trie) tryRevive(n node, key []byte, targetPrefixKey []byte, nub MPTProo } if pos == len(targetPrefixKey) { - - if !isExpired { - return nil, false, NewReviveNotExpiredErr(key[:pos], epoch) + if !t.isExpiredNode(n, targetPrefixKey, epoch, isExpired) { + return nil, false, NewReviveNotExpiredErr(targetPrefixKey[:pos], epoch) } hn, ok := n.(hashNode) if !ok { @@ -1326,39 +1319,20 @@ func (t *Trie) tryRevive(n node, key []byte, targetPrefixKey []byte, nub MPTProo if !ok { return nil, false, fmt.Errorf("invalid node type") } - tryUpdateNodeEpoch(nub.n2, t.currentEpoch) - newnode, _, err := t.updateChildNodeEpoch(nub.n2, key, pos+len(n1.Key), t.currentEpoch) - if err != nil { - return nil, false, fmt.Errorf("update child node epoch while reviving failed, err: %v", err) - } - n1 = n1.copy() - n1.Val = newnode - n1.flags = t.newFlag() - tryUpdateNodeEpoch(n1, t.currentEpoch) - renew, _, err := t.updateChildNodeEpoch(n1, key, pos, t.currentEpoch) - if err != nil { - return nil, false, fmt.Errorf("update child node epoch while reviving failed, err: %v", err) - } - return renew, true, nil - } - - tryUpdateNodeEpoch(nub.n1, t.currentEpoch) - newnode, _, err := t.updateChildNodeEpoch(nub.n1, key, pos, t.currentEpoch) - if err != nil { - return nil, false, fmt.Errorf("update child node epoch while reviving failed, err: %v", err) + n1.Val = nub.n2 + return refreshNubEpoch(n1, t.currentEpoch), true, nil } - - return newnode, true, nil + return refreshNubEpoch(nub.n1, t.currentEpoch), true, nil } if isExpired { - return nil, false, NewExpiredNodeError(key[:pos], epoch, n) + return nil, false, NewExpiredNodeError(targetPrefixKey[:pos], epoch, n) } switch n := n.(type) { case *shortNode: - if len(key)-pos < len(n.Key) || !bytes.Equal(n.Key, key[pos:pos+len(n.Key)]) { - return nil, false, fmt.Errorf("key %v not found", key) + if len(targetPrefixKey)-pos < len(n.Key) || !bytes.Equal(n.Key, targetPrefixKey[pos:pos+len(n.Key)]) { + return nil, false, fmt.Errorf("key %v not found", targetPrefixKey) } newNode, didRevive, err := t.tryRevive(n.Val, key, targetPrefixKey, nub, pos+len(n.Key), epoch, isExpired) if didRevive && err == nil { @@ -1369,7 +1343,7 @@ func (t *Trie) tryRevive(n node, key []byte, targetPrefixKey []byte, nub MPTProo } return n, didRevive, err case *fullNode: - childIndex := int(key[pos]) + childIndex := int(targetPrefixKey[pos]) isExpired, _ := n.ChildExpired(nil, childIndex, t.currentEpoch) newNode, didRevive, err := t.tryRevive(n.Children[childIndex], key, targetPrefixKey, nub, pos+1, epoch, isExpired) if didRevive && err == nil { @@ -1387,11 +1361,11 @@ func (t *Trie) tryRevive(n node, key []byte, targetPrefixKey []byte, nub MPTProo return n, didRevive, err case hashNode: - child, err := t.resolveAndTrack(n, key[:pos]) + child, err := t.resolveAndTrack(n, targetPrefixKey[:pos]) if err != nil { return nil, false, err } - if err = t.resolveEpochMetaAndTrack(child, epoch, key[:pos]); err != nil { + if err = t.resolveEpochMetaAndTrack(child, epoch, targetPrefixKey[:pos]); err != nil { return nil, false, err } @@ -1419,15 +1393,6 @@ func (t *Trie) ExpireByPrefix(prefixKeyHex []byte) error { return nil } -func tryUpdateNodeEpoch(origin node, epoch types.StateEpoch) { - switch n := origin.(type) { - case *shortNode: - n.setEpoch(epoch) - case *fullNode: - n.setEpoch(epoch) - } -} - func (t *Trie) expireByPrefix(n node, prefixKeyHex []byte) (node, bool, error) { // Loop through prefix key // When prefix key is empty, generate the hash node of the current node @@ -1791,3 +1756,18 @@ func (t *Trie) UpdateRootEpoch(epoch types.StateEpoch) { func (t *Trie) UpdateCurrentEpoch(epoch types.StateEpoch) { t.currentEpoch = epoch } + +// isExpiredNode check if expired or missed, it may prune by old state snap +func (t *Trie) isExpiredNode(n node, targetPrefixKey []byte, epoch types.StateEpoch, expired bool) bool { + if expired { + return true + } + + // check if there miss the trie node + _, err := t.resolve(n, targetPrefixKey, epoch) + if _, ok := err.(*MissingNodeError); ok { + return true + } + + return false +} From 128eb98aa9f90baa63d77468dd482410bf149d91 Mon Sep 17 00:00:00 2001 From: 0xbundler <124862913+0xbundler@users.noreply.github.com> Date: Fri, 3 Nov 2023 15:03:41 +0800 Subject: [PATCH 64/65] ut: fix some broken ut; lint: fix some golang lint; --- cmd/geth/dbcmd.go | 3 +++ cmd/geth/snapshot.go | 3 ++- cmd/utils/flags.go | 5 +++-- core/rawdb/accessors_trie.go | 3 ++- core/state/database.go | 3 ++- core/state/pruner/pruner.go | 7 ++++--- core/state/snapshot/conversion.go | 2 +- core/state/snapshot/generate_test.go | 1 - core/state/snapshot/snapshot_expire_test.go | 10 ++++------ core/state/snapshot/snapshot_value.go | 1 + core/state/snapshot/snapshot_value_test.go | 6 +++--- core/state/state_expiry.go | 4 ++-- core/state/state_object.go | 21 +++------------------ core/state/statedb.go | 14 ++------------ core/txpool/blobpool/interface.go | 3 ++- core/types/gen_account_rlp.go | 7 +++++-- core/types/gen_header_rlp.go | 7 +++++-- core/types/gen_log_rlp.go | 7 +++++-- core/types/gen_withdrawal_rlp.go | 7 +++++-- core/types/meta_test.go | 3 ++- core/types/state_expiry.go | 3 ++- core/types/typed_trie_node.go | 3 ++- eth/ethconfig/config.go | 3 ++- ethclient/gethclient/gethclient.go | 4 ++-- ethdb/fullstatedb.go | 5 +++-- internal/ethapi/api.go | 3 ++- miner/worker.go | 3 ++- trie/committer.go | 1 + trie/database.go | 3 ++- trie/epochmeta/database.go | 9 +++------ trie/epochmeta/difflayer.go | 7 ++++--- trie/epochmeta/difflayer_test.go | 20 ++++++++------------ trie/epochmeta/disklayer.go | 5 +++-- trie/epochmeta/snapshot.go | 7 ++++--- trie/epochmeta/snapshot_test.go | 5 +++-- trie/inspect_trie.go | 7 ++----- trie/node.go | 8 -------- trie/proof.go | 3 +-- trie/tracer.go | 5 ++--- trie/trie.go | 12 +++++------- trie/trie_expiry.go | 1 + trie/trie_reader.go | 5 +++-- trie/trie_test.go | 2 -- trie/triedb/pathdb/difflayer.go | 3 ++- trie/triedb/pathdb/disklayer.go | 3 ++- trie/triedb/pathdb/layertree.go | 3 ++- trie/triedb/pathdb/nodebuffer.go | 3 ++- trie/triedb/pathdb/testutils.go | 3 +++ trie/trienode/node.go | 3 ++- trie/typed_trie_node_test.go | 5 +++-- 50 files changed, 129 insertions(+), 135 deletions(-) diff --git a/cmd/geth/dbcmd.go b/cmd/geth/dbcmd.go index 2f176d14fd..cb54c7694d 100644 --- a/cmd/geth/dbcmd.go +++ b/cmd/geth/dbcmd.go @@ -429,6 +429,9 @@ func inspectTrie(ctx *cli.Context) error { return err } theInspect, err := trie.NewInspector(trieDB, theTrie, blockNumber, jobnum) + if err != nil { + return err + } theInspect.Run() theInspect.DisplayResult() } diff --git a/cmd/geth/snapshot.go b/cmd/geth/snapshot.go index 2134418ca7..99b9bc5161 100644 --- a/cmd/geth/snapshot.go +++ b/cmd/geth/snapshot.go @@ -21,11 +21,12 @@ import ( "encoding/json" "errors" "fmt" - "github.com/ethereum/go-ethereum/core" "os" "path/filepath" "time" + "github.com/ethereum/go-ethereum/core" + "github.com/prometheus/tsdb/fileutil" "github.com/ethereum/go-ethereum/cmd/utils" diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 7687cf87fd..de013fbe5a 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -23,8 +23,6 @@ import ( "encoding/hex" "errors" "fmt" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rlp" "math" "math/big" "net" @@ -37,6 +35,9 @@ import ( "strings" "time" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" + "github.com/fatih/structs" pcsclite "github.com/gballet/go-libpcsclite" gopsutil "github.com/shirou/gopsutil/mem" diff --git a/core/rawdb/accessors_trie.go b/core/rawdb/accessors_trie.go index 3cbffc64df..2911901ab4 100644 --- a/core/rawdb/accessors_trie.go +++ b/core/rawdb/accessors_trie.go @@ -18,9 +18,10 @@ package rawdb import ( "fmt" - "github.com/ethereum/go-ethereum/core/types" "sync" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" diff --git a/core/state/database.go b/core/state/database.go index 58366be0ad..ab17685f84 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -19,9 +19,10 @@ package state import ( "errors" "fmt" - "github.com/ethereum/go-ethereum/log" "time" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/lru" "github.com/ethereum/go-ethereum/core/rawdb" diff --git a/core/state/pruner/pruner.go b/core/state/pruner/pruner.go index bf91553c3a..7550e4743b 100644 --- a/core/state/pruner/pruner.go +++ b/core/state/pruner/pruner.go @@ -22,8 +22,6 @@ import ( "encoding/hex" "errors" "fmt" - "github.com/ethereum/go-ethereum/params" - bloomfilter "github.com/holiman/bloomfilter/v2" "math" "math/big" "os" @@ -32,6 +30,9 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum/params" + bloomfilter "github.com/holiman/bloomfilter/v2" + "github.com/prometheus/tsdb/fileutil" "github.com/ethereum/go-ethereum/common" @@ -683,7 +684,7 @@ func (p *Pruner) Prune(root common.Hash) error { // find target header header := p.chainHeader - for header != nil && header.Number.Uint64() >= 0 && header.Root != root { + for header != nil && header.Root != root { header = rawdb.ReadHeader(p.db, header.ParentHash, header.Number.Uint64()-1) } if header == nil || header.Root != root { diff --git a/core/state/snapshot/conversion.go b/core/state/snapshot/conversion.go index 9e272a4d98..0c61e168d6 100644 --- a/core/state/snapshot/conversion.go +++ b/core/state/snapshot/conversion.go @@ -142,7 +142,7 @@ func TraverseContractTrie(snaptree *Tree, root common.Hash, pruneExpiredTrieCh c // Start to feed leaves for acctIt.Next() { // Fetch the next account and process it concurrently - account, err = types.FullAccount(acctIt.(AccountIterator).Account()) + account, err = types.FullAccount(acctIt.Account()) if err != nil { break } diff --git a/core/state/snapshot/generate_test.go b/core/state/snapshot/generate_test.go index dc86636488..40a73f5bd8 100644 --- a/core/state/snapshot/generate_test.go +++ b/core/state/snapshot/generate_test.go @@ -582,7 +582,6 @@ func testGenerateWithExtraAccounts(t *testing.T, scheme string) { rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("b-key-2")), val) val, _ = rlp.EncodeToBytes([]byte("b-val-3")) rawdb.WriteStorageSnapshot(helper.diskdb, key, hashData([]byte("b-key-3")), val) - } root := helper.Commit() diff --git a/core/state/snapshot/snapshot_expire_test.go b/core/state/snapshot/snapshot_expire_test.go index ac20e9c90f..0bb2dd762c 100644 --- a/core/state/snapshot/snapshot_expire_test.go +++ b/core/state/snapshot/snapshot_expire_test.go @@ -1,20 +1,18 @@ package snapshot import ( + "testing" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb/memorydb" "github.com/stretchr/testify/assert" - "testing" ) var ( - accountHash = common.HexToHash("0x31b67165f56d0ac50814cafa06748fb3b8fccd3c611a8117350e7a49b44ce130") - storageHash1 = common.HexToHash("0x0bb2f3e66816c6fd12513f053d5ee034b1fa2d448a1dc8ee7f56e4c87d6c53fe") - storageHash2 = common.HexToHash("0x0bb2f3e66816c93e142cd336c411ebd5576a90739bad7ec1ec0d4a63ea0ec1dc") - storageShrink1 = common.FromHex("0x0bb2f3e66816c") - storageNodeHash1 = common.HexToHash("0xcf0e24cb9417a38ff61d11def23fb0ec041257c81c04dec6156d5e6b30f4ec28") + accountHash = common.HexToHash("0x31b67165f56d0ac50814cafa06748fb3b8fccd3c611a8117350e7a49b44ce130") + storageHash1 = common.HexToHash("0x0bb2f3e66816c6fd12513f053d5ee034b1fa2d448a1dc8ee7f56e4c87d6c53fe") ) func TestShrinkExpiredLeaf(t *testing.T) { diff --git a/core/state/snapshot/snapshot_value.go b/core/state/snapshot/snapshot_value.go index e487889b70..b9086476d2 100644 --- a/core/state/snapshot/snapshot_value.go +++ b/core/state/snapshot/snapshot_value.go @@ -3,6 +3,7 @@ package snapshot import ( "bytes" "errors" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" ) diff --git a/core/state/snapshot/snapshot_value_test.go b/core/state/snapshot/snapshot_value_test.go index d316c95fbb..cb5088b20b 100644 --- a/core/state/snapshot/snapshot_value_test.go +++ b/core/state/snapshot/snapshot_value_test.go @@ -2,16 +2,16 @@ package snapshot import ( "encoding/hex" + "testing" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" "github.com/stretchr/testify/assert" - "testing" ) var ( - val, _ = hex.DecodeString("0000f9eef0150e074b32e3b3b6d34d2534222292e3953019a41d714d135763a6") - hash, _ = hex.DecodeString("2b6fad2e1335b0b4debd3de01c91f3f45d2b88465ff42ae2c53900f2f702101d") + val, _ = hex.DecodeString("0000f9eef0150e074b32e3b3b6d34d2534222292e3953019a41d714d135763a6") ) func TestRawValueEncode(t *testing.T) { diff --git a/core/state/state_expiry.go b/core/state/state_expiry.go index 2779e13e41..203ed2fa2e 100644 --- a/core/state/state_expiry.go +++ b/core/state/state_expiry.go @@ -3,13 +3,14 @@ package state import ( "bytes" "fmt" + "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/trie" - "time" ) var ( @@ -103,7 +104,6 @@ func batchFetchExpiredFromRemote(expiryMeta *stateExpiryMeta, addr common.Addres for i, key := range expiredKeys { keysStr[i] = common.Bytes2Hex(key[:]) } - } else { for i, prefix := range prefixKeys { prefixKeysStr[i] = common.Bytes2Hex(prefix) diff --git a/core/state/state_object.go b/core/state/state_object.go index aa70191a4c..7146146907 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -19,13 +19,14 @@ package state import ( "bytes" "fmt" - "github.com/ethereum/go-ethereum/core/state/snapshot" - "github.com/ethereum/go-ethereum/trie" "io" "math/big" "sync" "time" + "github.com/ethereum/go-ethereum/core/state/snapshot" + "github.com/ethereum/go-ethereum/trie" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" @@ -336,22 +337,6 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { return value } -// needLoadFromTrie If not found in snap when EnableExpire(), need check insert duplication from trie. -func (s *stateObject) needLoadFromTrie(err error, sv snapshot.SnapValue) bool { - if s.db.snap == nil { - return true - } - if !s.db.EnableExpire() { - return err != nil - } - - if err != nil || sv == nil { - return true - } - - return false -} - // SetState updates a value in account storage. func (s *stateObject) SetState(key, value common.Hash) { // If the new value is the same as old, don't set diff --git a/core/state/statedb.go b/core/state/statedb.go index c703bb1b92..24afa2256b 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -18,16 +18,16 @@ package state import ( - "bytes" "errors" "fmt" - "github.com/ethereum/go-ethereum/ethdb" "math/big" "runtime" "sort" "sync" "time" + "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/gopool" "github.com/ethereum/go-ethereum/core/rawdb" @@ -1829,16 +1829,6 @@ func (s *StateDB) Commit(block uint64, failPostCommitFunc func(), postCommitFunc return root, diffLayer, nil } -func stringfyEpochMeta(meta map[common.Hash]map[string][]byte) string { - buf := bytes.NewBuffer(nil) - for hash, m := range meta { - for k, v := range m { - buf.WriteString(fmt.Sprintf("%v: %v|%v, ", hash, []byte(k), common.Bytes2Hex(v))) - } - } - return buf.String() -} - func (s *StateDB) SnapToDiffLayer() ([]common.Address, []types.DiffAccount, []types.DiffStorage) { destructs := make([]common.Address, 0, len(s.stateObjectsDestruct)) for account := range s.stateObjectsDestruct { diff --git a/core/txpool/blobpool/interface.go b/core/txpool/blobpool/interface.go index f3c0658fc2..a1852a5bd8 100644 --- a/core/txpool/blobpool/interface.go +++ b/core/txpool/blobpool/interface.go @@ -17,11 +17,12 @@ package blobpool import ( + "math/big" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" - "math/big" ) // BlockChain defines the minimal set of methods needed to back a blob pool with diff --git a/core/types/gen_account_rlp.go b/core/types/gen_account_rlp.go index 5181d88411..9d07200e33 100644 --- a/core/types/gen_account_rlp.go +++ b/core/types/gen_account_rlp.go @@ -5,8 +5,11 @@ package types -import "github.com/ethereum/go-ethereum/rlp" -import "io" +import ( + "io" + + "github.com/ethereum/go-ethereum/rlp" +) func (obj *StateAccount) EncodeRLP(_w io.Writer) error { w := rlp.NewEncoderBuffer(_w) diff --git a/core/types/gen_header_rlp.go b/core/types/gen_header_rlp.go index a5ed5cd150..e05bde09f6 100644 --- a/core/types/gen_header_rlp.go +++ b/core/types/gen_header_rlp.go @@ -5,8 +5,11 @@ package types -import "github.com/ethereum/go-ethereum/rlp" -import "io" +import ( + "io" + + "github.com/ethereum/go-ethereum/rlp" +) func (obj *Header) EncodeRLP(_w io.Writer) error { w := rlp.NewEncoderBuffer(_w) diff --git a/core/types/gen_log_rlp.go b/core/types/gen_log_rlp.go index 4a6c6b0094..78fa783cee 100644 --- a/core/types/gen_log_rlp.go +++ b/core/types/gen_log_rlp.go @@ -5,8 +5,11 @@ package types -import "github.com/ethereum/go-ethereum/rlp" -import "io" +import ( + "io" + + "github.com/ethereum/go-ethereum/rlp" +) func (obj *rlpLog) EncodeRLP(_w io.Writer) error { w := rlp.NewEncoderBuffer(_w) diff --git a/core/types/gen_withdrawal_rlp.go b/core/types/gen_withdrawal_rlp.go index d0b4e0147a..e3fa001eb6 100644 --- a/core/types/gen_withdrawal_rlp.go +++ b/core/types/gen_withdrawal_rlp.go @@ -5,8 +5,11 @@ package types -import "github.com/ethereum/go-ethereum/rlp" -import "io" +import ( + "io" + + "github.com/ethereum/go-ethereum/rlp" +) func (obj *Withdrawal) EncodeRLP(_w io.Writer) error { w := rlp.NewEncoderBuffer(_w) diff --git a/core/types/meta_test.go b/core/types/meta_test.go index e03a5b5097..ac03a5595f 100644 --- a/core/types/meta_test.go +++ b/core/types/meta_test.go @@ -2,8 +2,9 @@ package types import ( "encoding/hex" - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestMetaEncodeDecode(t *testing.T) { diff --git a/core/types/state_expiry.go b/core/types/state_expiry.go index ed2bb18e43..bb83a31f2c 100644 --- a/core/types/state_expiry.go +++ b/core/types/state_expiry.go @@ -3,8 +3,9 @@ package types import ( "errors" "fmt" - "github.com/ethereum/go-ethereum/log" "strings" + + "github.com/ethereum/go-ethereum/log" ) const ( diff --git a/core/types/typed_trie_node.go b/core/types/typed_trie_node.go index 01eb120edf..dedb94eddd 100644 --- a/core/types/typed_trie_node.go +++ b/core/types/typed_trie_node.go @@ -3,8 +3,9 @@ package types import ( "errors" "fmt" - "github.com/ethereum/go-ethereum/rlp" "io" + + "github.com/ethereum/go-ethereum/rlp" ) const ( diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 091e7c65ca..a8278c4ebf 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -19,9 +19,10 @@ package ethconfig import ( "errors" - "github.com/ethereum/go-ethereum/core/types" "time" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/beacon" diff --git a/ethclient/gethclient/gethclient.go b/ethclient/gethclient/gethclient.go index 87f93d1625..b219d0a9bc 100644 --- a/ethclient/gethclient/gethclient.go +++ b/ethclient/gethclient/gethclient.go @@ -130,7 +130,7 @@ func (ec *Client) GetProof(ctx context.Context, account common.Address, keys []s func (ec *Client) GetStorageReviveProof(ctx context.Context, stateRoot common.Hash, account common.Address, root common.Hash, keys []string, prefixKeys []string) (*types.ReviveResult, error) { type reviveResult struct { StorageProof []types.ReviveStorageProof `json:"storageProof"` - BlockNum hexutil.Uint64 `json:"blockNum"` + BlockNum uint64 `json:"blockNum"` } var err error @@ -140,7 +140,7 @@ func (ec *Client) GetStorageReviveProof(ctx context.Context, stateRoot common.Ha return &types.ReviveResult{ StorageProof: res.StorageProof, - BlockNum: uint64(res.BlockNum), + BlockNum: res.BlockNum, }, err } diff --git a/ethdb/fullstatedb.go b/ethdb/fullstatedb.go index 500beac1fb..367351f5e4 100644 --- a/ethdb/fullstatedb.go +++ b/ethdb/fullstatedb.go @@ -5,14 +5,15 @@ import ( "context" "errors" "fmt" + "strings" + "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/rpc" lru "github.com/hashicorp/golang-lru" - "strings" - "time" ) var ( diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 49148619fe..496502b14e 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -21,11 +21,12 @@ import ( "encoding/hex" "errors" "fmt" - "github.com/ethereum/go-ethereum/metrics" "math/big" "strings" "time" + "github.com/ethereum/go-ethereum/metrics" + "github.com/davecgh/go-spew/spew" "github.com/ethereum/go-ethereum/accounts" diff --git a/miner/worker.go b/miner/worker.go index d641eb899a..6892da7d35 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -19,12 +19,13 @@ package miner import ( "errors" "fmt" - "github.com/ethereum/go-ethereum/metrics" "math/big" "sync" "sync/atomic" "time" + "github.com/ethereum/go-ethereum/metrics" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/misc/eip1559" diff --git a/trie/committer.go b/trie/committer.go index 2ad8d9f4f5..f43f58419b 100644 --- a/trie/committer.go +++ b/trie/committer.go @@ -18,6 +18,7 @@ package trie import ( "fmt" + "github.com/ethereum/go-ethereum/trie/epochmeta" "github.com/ethereum/go-ethereum/common" diff --git a/trie/database.go b/trie/database.go index 904b485119..d41056b687 100644 --- a/trie/database.go +++ b/trie/database.go @@ -19,10 +19,11 @@ package trie import ( "errors" "fmt" - "github.com/ethereum/go-ethereum/trie/epochmeta" "math/big" "strings" + "github.com/ethereum/go-ethereum/trie/epochmeta" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/ethdb" diff --git a/trie/epochmeta/database.go b/trie/epochmeta/database.go index 8feb858991..566c33915c 100644 --- a/trie/epochmeta/database.go +++ b/trie/epochmeta/database.go @@ -3,9 +3,10 @@ package epochmeta import ( "bytes" "fmt" + "math/big" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" - "math/big" "github.com/ethereum/go-ethereum/rlp" @@ -91,9 +92,5 @@ func AccountMeta2Bytes(meta types.StateMeta) ([]byte, error) { // IsEpochMetaPath add some skip hash check rule func IsEpochMetaPath(path []byte) bool { - if bytes.Equal(AccountMetadataPath, path) { - return true - } - - return false + return bytes.Equal(AccountMetadataPath, path) } diff --git a/trie/epochmeta/difflayer.go b/trie/epochmeta/difflayer.go index 2838e8a9d0..2e2f73050a 100644 --- a/trie/epochmeta/difflayer.go +++ b/trie/epochmeta/difflayer.go @@ -4,13 +4,14 @@ import ( "bytes" "encoding/binary" "errors" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/rlp" - bloomfilter "github.com/holiman/bloomfilter/v2" "math" "math/big" "math/rand" "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + bloomfilter "github.com/holiman/bloomfilter/v2" ) const ( diff --git a/trie/epochmeta/difflayer_test.go b/trie/epochmeta/difflayer_test.go index 54cea5f3c3..ebe1c99818 100644 --- a/trie/epochmeta/difflayer_test.go +++ b/trie/epochmeta/difflayer_test.go @@ -1,9 +1,10 @@ package epochmeta import ( - "github.com/ethereum/go-ethereum/core/types" "testing" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethdb/memorydb" "github.com/stretchr/testify/assert" @@ -12,17 +13,12 @@ import ( const hashLen = len(common.Hash{}) var ( - blockRoot0 = makeHash("b0") - blockRoot1 = makeHash("b1") - blockRoot2 = makeHash("b2") - blockRoot3 = makeHash("b3") - storageRoot0 = makeHash("s0") - storageRoot1 = makeHash("s1") - storageRoot2 = makeHash("s2") - storageRoot3 = makeHash("s3") - contract1 = makeHash("c1") - contract2 = makeHash("c2") - contract3 = makeHash("c3") + blockRoot0 = makeHash("b0") + blockRoot1 = makeHash("b1") + blockRoot2 = makeHash("b2") + contract1 = makeHash("c1") + contract2 = makeHash("c2") + contract3 = makeHash("c3") ) func TestEpochMetaDiffLayer_whenGenesis(t *testing.T) { diff --git a/trie/epochmeta/disklayer.go b/trie/epochmeta/disklayer.go index cc7894736c..4d71de4c3b 100644 --- a/trie/epochmeta/disklayer.go +++ b/trie/epochmeta/disklayer.go @@ -3,14 +3,15 @@ package epochmeta import ( "bytes" "errors" + "math/big" + "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" lru "github.com/hashicorp/golang-lru" - "math/big" - "sync" ) const ( diff --git a/trie/epochmeta/snapshot.go b/trie/epochmeta/snapshot.go index 71201f961d..decbdaaf87 100644 --- a/trie/epochmeta/snapshot.go +++ b/trie/epochmeta/snapshot.go @@ -4,15 +4,16 @@ import ( "bytes" "errors" "fmt" + "io" + "math/big" + "sync" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" - "io" - "math/big" - "sync" ) // snapshot record diff layer and disk layer of shadow nodes, support mini reorg diff --git a/trie/epochmeta/snapshot_test.go b/trie/epochmeta/snapshot_test.go index 35099d5ba2..0cc4145eee 100644 --- a/trie/epochmeta/snapshot_test.go +++ b/trie/epochmeta/snapshot_test.go @@ -1,11 +1,12 @@ package epochmeta import ( - "github.com/ethereum/go-ethereum/ethdb/memorydb" - "github.com/stretchr/testify/assert" "math/big" "strconv" "testing" + + "github.com/ethereum/go-ethereum/ethdb/memorydb" + "github.com/stretchr/testify/assert" ) func TestEpochMetaDiffLayer_capDiffLayers(t *testing.T) { diff --git a/trie/inspect_trie.go b/trie/inspect_trie.go index a6a9df7b7d..5d4cc00d04 100644 --- a/trie/inspect_trie.go +++ b/trie/inspect_trie.go @@ -4,7 +4,6 @@ import ( "bytes" "errors" "fmt" - "github.com/ethereum/go-ethereum/core/types" "math/big" "os" "runtime" @@ -14,6 +13,8 @@ import ( "sync/atomic" "time" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" @@ -32,7 +33,6 @@ type Inspector struct { trie *Trie // traverse trie blocknum uint64 root node // root of triedb - num uint64 // block number result *TotalTrieTreeStat // inspector result totalNum uint64 concurrentQueue chan struct{} @@ -206,7 +206,6 @@ func (inspect *Inspector) SubConcurrentTraversal(theTrie *Trie, theTrieTreeStat inspect.ConcurrentTraversal(theTrie, theTrieTreeStat, theNode, height, path) <-inspect.concurrentQueue inspect.wg.Done() - return } func (inspect *Inspector) ConcurrentTraversal(theTrie *Trie, theTrieTreeStat *TrieTreeStat, theNode node, height uint32, path []byte) { @@ -274,7 +273,6 @@ func (inspect *Inspector) ConcurrentTraversal(theTrie *Trie, theTrieTreeStat *Tr panic(errors.New("Invalid node type to traverse.")) } theTrieTreeStat.AtomicAdd(theNode, height) - return } func (inspect *Inspector) DisplayResult() { @@ -312,6 +310,5 @@ func (inspect *Inspector) DisplayResult() { } stat, _ := inspect.result.theTrieTreeStats.Get(cntHash[1]) stat.Display(cntHash[1], "ContractTrie") - i++ } } diff --git a/trie/node.go b/trie/node.go index 235344bcca..1ec7f00855 100644 --- a/trie/node.go +++ b/trie/node.go @@ -89,14 +89,6 @@ func (n *shortNode) setEpoch(epoch types.StateEpoch) { n.epoch = epoch } -func (n *fullNode) getEpoch() types.StateEpoch { - return n.epoch -} - -func (n *shortNode) getEpoch() types.StateEpoch { - return n.epoch -} - func (n *fullNode) GetChildEpoch(index int) types.StateEpoch { if index < 16 { return n.EpochMap[index] diff --git a/trie/proof.go b/trie/proof.go index cc19b84e37..5abee92cf9 100644 --- a/trie/proof.go +++ b/trie/proof.go @@ -21,6 +21,7 @@ import ( "encoding/hex" "errors" "fmt" + "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/common" @@ -37,7 +38,6 @@ import ( // nodes of the longest existing prefix of the key (at least the root node), ending // with the node that proves the absence of the key. func (t *Trie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error { - var nodeEpoch types.StateEpoch // Short circuit if the trie is already committed and not usable. @@ -201,7 +201,6 @@ func (t *Trie) traverseNodes(tn node, prefixKey, suffixKey []byte, nodes *[]node } func (t *Trie) ProveByPath(key []byte, prefixKeyHex []byte, proofDb ethdb.KeyValueWriter) error { - if t.committed { return ErrCommitted } diff --git a/trie/tracer.go b/trie/tracer.go index fc37f70698..a4321e5345 100644 --- a/trie/tracer.go +++ b/trie/tracer.go @@ -18,6 +18,7 @@ package trie import ( "bytes" + "github.com/ethereum/go-ethereum/common" ) @@ -95,9 +96,7 @@ func (t *tracer) onExpandToBranchNode(path []byte) { if !t.tagEpochMeta { return } - if _, present := t.deleteEpochMetas[string(path)]; present { - delete(t.deleteEpochMetas, string(path)) - } + delete(t.deleteEpochMetas, string(path)) } // onDelete tracks the newly deleted trie node. If it's already diff --git a/trie/trie.go b/trie/trie.go index d7149b23a5..0e5928673d 100644 --- a/trie/trie.go +++ b/trie/trie.go @@ -21,16 +21,17 @@ import ( "bytes" "errors" "fmt" + "runtime" + "sync" + "sync/atomic" + "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/trie/epochmeta" "github.com/ethereum/go-ethereum/trie/trienode" - "runtime" - "sync" - "sync/atomic" - "time" ) var ( @@ -997,7 +998,6 @@ func (t *Trie) deleteWithEpoch(n node, prefix, key []byte, epoch types.StateEpoc default: panic(fmt.Sprintf("%T: invalid node: %v (%v)", n, n, key)) } - } func concat(s1 []byte, s2 ...byte) []byte { @@ -1295,7 +1295,6 @@ func (t *Trie) TryRevive(key []byte, proof []*MPTProofNub) ([]*MPTProofNub, erro // tryRevive it just revive from targetPrefixKey func (t *Trie) tryRevive(n node, key []byte, targetPrefixKey []byte, nub MPTProofNub, pos int, epoch types.StateEpoch, isExpired bool) (node, bool, error) { - if pos > len(targetPrefixKey) { return nil, false, fmt.Errorf("target revive node not found") } @@ -1581,7 +1580,6 @@ func (st *ScanTask) MoreThread() bool { // ScanForPrune traverses the storage trie and prunes all expired or unexpired nodes. func (t *Trie) ScanForPrune(st *ScanTask) error { - if !t.enableExpiry { return nil } diff --git a/trie/trie_expiry.go b/trie/trie_expiry.go index 4a31086574..8c7c8e32f4 100644 --- a/trie/trie_expiry.go +++ b/trie/trie_expiry.go @@ -3,6 +3,7 @@ package trie import ( "bytes" "fmt" + "github.com/ethereum/go-ethereum/core/types" ) diff --git a/trie/trie_reader.go b/trie/trie_reader.go index 4ec9ca27d0..3cad34c8de 100644 --- a/trie/trie_reader.go +++ b/trie/trie_reader.go @@ -19,14 +19,15 @@ package trie import ( "errors" "fmt" + "math/big" + "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/trie/epochmeta" "github.com/ethereum/go-ethereum/trie/triestate" - "math/big" - "time" ) var ( diff --git a/trie/trie_test.go b/trie/trie_test.go index aa704f8166..cb80fa8af2 100644 --- a/trie/trie_test.go +++ b/trie/trie_test.go @@ -1038,7 +1038,6 @@ func TestRevive(t *testing.T) { } func TestReviveCustom(t *testing.T) { - data := map[string]string{ "abcd": "A", "abce": "B", "abde": "C", "abdf": "D", "defg": "E", "defh": "F", "degh": "G", "degi": "H", @@ -1083,7 +1082,6 @@ func TestReviveCustom(t *testing.T) { // TestReviveBadProof tests that a trie cannot be revived from a bad proof func TestReviveBadProof(t *testing.T) { - dataA := map[string]string{ "abcd": "A", "abce": "B", "abde": "C", "abdf": "D", "defg": "E", "defh": "F", "degh": "G", "degi": "H", diff --git a/trie/triedb/pathdb/difflayer.go b/trie/triedb/pathdb/difflayer.go index 0ffa8a6d90..74ff69fedd 100644 --- a/trie/triedb/pathdb/difflayer.go +++ b/trie/triedb/pathdb/difflayer.go @@ -18,9 +18,10 @@ package pathdb import ( "fmt" - "github.com/ethereum/go-ethereum/trie/epochmeta" "sync" + "github.com/ethereum/go-ethereum/trie/epochmeta" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/trie/trienode" diff --git a/trie/triedb/pathdb/disklayer.go b/trie/triedb/pathdb/disklayer.go index 6bb2e05616..145f8a5e4f 100644 --- a/trie/triedb/pathdb/disklayer.go +++ b/trie/triedb/pathdb/disklayer.go @@ -19,9 +19,10 @@ package pathdb import ( "errors" "fmt" + "sync" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/trie/epochmeta" - "sync" "github.com/VictoriaMetrics/fastcache" "github.com/ethereum/go-ethereum/common" diff --git a/trie/triedb/pathdb/layertree.go b/trie/triedb/pathdb/layertree.go index f7fc4930bc..3590691d4c 100644 --- a/trie/triedb/pathdb/layertree.go +++ b/trie/triedb/pathdb/layertree.go @@ -19,9 +19,10 @@ package pathdb import ( "errors" "fmt" - "github.com/ethereum/go-ethereum/log" "sync" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/trie/trienode" diff --git a/trie/triedb/pathdb/nodebuffer.go b/trie/triedb/pathdb/nodebuffer.go index 851264cd54..3f6a17c078 100644 --- a/trie/triedb/pathdb/nodebuffer.go +++ b/trie/triedb/pathdb/nodebuffer.go @@ -18,9 +18,10 @@ package pathdb import ( "fmt" - "github.com/ethereum/go-ethereum/trie/epochmeta" "time" + "github.com/ethereum/go-ethereum/trie/epochmeta" + "github.com/VictoriaMetrics/fastcache" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" diff --git a/trie/triedb/pathdb/testutils.go b/trie/triedb/pathdb/testutils.go index d6fdacb421..070e13dea3 100644 --- a/trie/triedb/pathdb/testutils.go +++ b/trie/triedb/pathdb/testutils.go @@ -20,6 +20,8 @@ import ( "bytes" "fmt" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" @@ -130,6 +132,7 @@ func hash(states map[common.Hash][]byte) (common.Hash, []byte) { if len(input) == 0 { return types.EmptyRootHash, nil } + input, _ = rlp.EncodeToBytes(input) return crypto.Keccak256Hash(input), input } diff --git a/trie/trienode/node.go b/trie/trienode/node.go index 6c1660ff79..38bd67cfe5 100644 --- a/trie/trienode/node.go +++ b/trie/trienode/node.go @@ -18,10 +18,11 @@ package trienode import ( "fmt" - "github.com/ethereum/go-ethereum/trie/epochmeta" "sort" "strings" + "github.com/ethereum/go-ethereum/trie/epochmeta" + "github.com/ethereum/go-ethereum/common" ) diff --git a/trie/typed_trie_node_test.go b/trie/typed_trie_node_test.go index dc88be1958..490b101c4d 100644 --- a/trie/typed_trie_node_test.go +++ b/trie/typed_trie_node_test.go @@ -1,11 +1,12 @@ package trie import ( + "math/rand" + "testing" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/stretchr/testify/assert" - "math/rand" - "testing" ) var ( From 68a24ca1db811ed16acae23117d992ff6a3574a1 Mon Sep 17 00:00:00 2001 From: asyukii Date: Mon, 6 Nov 2023 10:49:13 +0800 Subject: [PATCH 65/65] feat: remoteDb only delay syncs for specified peers change logic --- cmd/utils/flags.go | 7 +++++++ core/types/state_expiry.go | 28 +++++++++++++++++----------- eth/downloader/downloader.go | 2 +- eth/fetcher/block_fetcher.go | 2 +- p2p/server.go | 4 ++++ 5 files changed, 30 insertions(+), 13 deletions(-) diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index de013fbe5a..9b13e1012a 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1994,6 +1994,13 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { if err != nil { Fatalf("%v", err) } + seCfg.AllowedPeerList = make([]string, 0) + for _, seNode := range stack.Config().P2P.StateExpiryAllowedNodes { + seCfg.AllowedPeerList = append(seCfg.AllowedPeerList, seNode.ID().String()) + } + if len(seCfg.AllowedPeerList) == 0 && seCfg.EnableRemoteMode { + log.Warn("State expiry remote mode is enabled but no allowed peers are specified.") + } cfg.StateExpiryCfg = seCfg chaindb.Close() diff --git a/core/types/state_expiry.go b/core/types/state_expiry.go index bb83a31f2c..7ae6870bba 100644 --- a/core/types/state_expiry.go +++ b/core/types/state_expiry.go @@ -23,7 +23,8 @@ type StateExpiryConfig struct { StateEpoch2Block uint64 StateEpochPeriod uint64 EnableLocalRevive bool - EnableRemoteMode bool `rlp:"optional"` // when enable remoteDB mode, it will register specific RPC for partial proof and keep sync behind for safety proof + EnableRemoteMode bool `rlp:"optional"` // when enable remoteDB mode, it will register specific RPC for partial proof and keep sync behind for safety proof + AllowedPeerList []string `rlp:"optional"` // when enable remoteDB mode, it will only delay its sync for the peers in the list } // EnableExpiry when enable remote mode, it just check param @@ -75,7 +76,7 @@ func (s *StateExpiryConfig) CheckCompatible(newCfg *StateExpiryConfig) error { return errors.New("disable state expiry is dangerous after enabled, expired state may pruned") } if s.EnableRemoteMode && !newCfg.EnableRemoteMode { - return errors.New("disable state expiry EnableRemoteMode is dangerous after enabled") + return errors.New("disable state expiry EnableRemoteMode is dangerous after enabled") } if err := s.CheckStateEpochCompatible(newCfg.StateEpoch1Block, newCfg.StateEpoch2Block, newCfg.StateEpochPeriod); err != nil { @@ -115,21 +116,25 @@ func (s *StateExpiryConfig) String() string { if s.Enable && s.EnableRemoteMode { return "State Expiry Enable in RemoteMode, it will not expired any state." } - return fmt.Sprintf("Enable State Expiry, RemoteEndpoint: %v, StateEpoch: [%v|%v|%v], StateScheme: %v, PruneLevel: %v, EnableLocalRevive: %v.", - s.FullStateEndpoint, s.StateEpoch1Block, s.StateEpoch2Block, s.StateEpochPeriod, s.StateScheme, s.PruneLevel, s.EnableLocalRevive) + return fmt.Sprintf("Enable State Expiry, RemoteEndpoint: %v, StateEpoch: [%v|%v|%v], StateScheme: %v, PruneLevel: %v, EnableLocalRevive: %v, AllowedPeerList: %v.", + s.FullStateEndpoint, s.StateEpoch1Block, s.StateEpoch2Block, s.StateEpochPeriod, s.StateScheme, s.PruneLevel, s.EnableLocalRevive, s.AllowedPeerList) } // ShouldKeep1EpochBehind when enable state expiry, keep remoteDB behind the latest only 1 epoch blocks -func (s *StateExpiryConfig) ShouldKeep1EpochBehind(remote uint64, local uint64) (bool, uint64) { - if !s.EnableRemoteMode { - return false, remote - } - if remote <= local { +func (s *StateExpiryConfig) ShouldKeep1EpochBehind(remote uint64, local uint64, peerId string) (bool, uint64) { + + if !s.EnableRemoteMode || remote <= local || remote < s.StateEpoch1Block { return false, remote } - // if in epoch0, just sync - if remote < s.StateEpoch1Block { + allowed := false + for _, allowPeer := range s.AllowedPeerList { + if allowPeer == peerId { + allowed = true + break + } + } + if !allowed { return false, remote } @@ -145,5 +150,6 @@ func (s *StateExpiryConfig) ShouldKeep1EpochBehind(remote uint64, local uint64) if remote-s.StateEpochPeriod <= local { return true, 0 } + return false, remote - s.StateEpochPeriod } diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go index 0c388dd9c8..f82bc7e346 100644 --- a/eth/downloader/downloader.go +++ b/eth/downloader/downloader.go @@ -527,7 +527,7 @@ func (d *Downloader) syncWithPeer(p *peerConnection, hash common.Hash, td, ttd * if d.expiryConfig.EnableRemote() { var keep bool - keep, remoteHeight = d.expiryConfig.ShouldKeep1EpochBehind(remoteHeight, localHeight) + keep, remoteHeight = d.expiryConfig.ShouldKeep1EpochBehind(remoteHeight, localHeight, p.id) log.Debug("EnableRemote wait remote more blocks", "remoteHeight", remoteHeader.Number, "request", remoteHeight, "localHeight", localHeight, "keep", keep, "config", d.expiryConfig) if keep { return errCanceled diff --git a/eth/fetcher/block_fetcher.go b/eth/fetcher/block_fetcher.go index 652fa3c5b5..5d94395f3d 100644 --- a/eth/fetcher/block_fetcher.go +++ b/eth/fetcher/block_fetcher.go @@ -388,7 +388,7 @@ func (f *BlockFetcher) loop() { } if f.expiryConfig.EnableRemote() { - if keep, _ := f.expiryConfig.ShouldKeep1EpochBehind(number, height); keep { + if keep, _ := f.expiryConfig.ShouldKeep1EpochBehind(number, height, op.origin); keep { log.Debug("BlockFetcher EnableRemote wait remote more blocks", "remoteHeight", number, "localHeight", height, "config", f.expiryConfig) break } diff --git a/p2p/server.go b/p2p/server.go index 0c6e0b7ee0..cd076692a4 100644 --- a/p2p/server.go +++ b/p2p/server.go @@ -134,6 +134,10 @@ type Config struct { // allowed to connect, even above the peer limit. TrustedNodes []*enode.Node + // StateExpiryAllowedNodes are used to ensure that the state expiry remoteDb + // will only delay its sync for the peers in the list. + StateExpiryAllowedNodes []*enode.Node + // Connectivity can be restricted to certain IP networks. // If this option is set to a non-nil value, only hosts which match one of the // IP networks contained in the list are considered.