diff --git a/cmd/thor/pruner/pruner_test.go b/cmd/thor/pruner/pruner_test.go index 195504dee..ba317eb53 100644 --- a/cmd/thor/pruner/pruner_test.go +++ b/cmd/thor/pruner/pruner_test.go @@ -17,6 +17,7 @@ import ( "time" "github.com/vechain/thor/v2/bft" + "github.com/vechain/thor/v2/test/testchain" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/rlp" @@ -307,3 +308,39 @@ func TestPrune(t *testing.T) { closeDB() } + +func TestGetAfterPrune(t *testing.T) { + chain, err := testchain.NewDefault() + assert.NoError(t, err) + + accounts := genesis.DevAccounts() + to := thor.BytesToAddress([]byte("to")) + + for range 10 { + err = chain.MintClauses(accounts[0], []*tx.Clause{tx.NewClause(&to).WithValue(big.NewInt(1))}) + assert.NoError(t, err) + } + + pruner := Pruner{ + repo: chain.Repo(), + db: chain.Database(), + } + // iterate best chain to 10 + + // prune [0, 10) + err = pruner.pruneTries(chain.Repo().NewBestChain(), 0, chain.Repo().BestBlockSummary().Header.Number()) + assert.NoError(t, err) + + st := chain.State() + balance, err := st.GetBalance(to) + assert.NoError(t, err) + assert.Equal(t, big.NewInt(10), balance) + + sum, err := chain.Repo().NewBestChain().GetBlockSummary(9) + assert.NoError(t, err) + + st = state.NewStater(chain.Database()).NewState(sum.Root()) + _, err = st.GetBalance(to) + assert.Error(t, err) + assert.Contains(t, err.Error(), "missing trie node") +} diff --git a/muxdb/cache.go b/muxdb/cache.go index 0c25bc944..ca650fcb8 100644 --- a/muxdb/cache.go +++ b/muxdb/cache.go @@ -82,6 +82,10 @@ func (c *cache) log() { // AddNodeBlob adds encoded node blob into the cache. func (c *cache) AddNodeBlob(keyBuf *[]byte, name string, path []byte, ver trie.Version, blob []byte, isCommitting bool) { + // skip root node cache, since root node is already cached in root node cache + if len(path) == 0 { + return + } // the version part v := binary.AppendUvarint((*keyBuf)[:0], uint64(ver.Major)) v = binary.AppendUvarint(v, uint64(ver.Minor)) @@ -102,6 +106,10 @@ func (c *cache) AddNodeBlob(keyBuf *[]byte, name string, path []byte, ver trie.V // GetNodeBlob returns the cached node blob. func (c *cache) GetNodeBlob(keyBuf *[]byte, name string, path []byte, ver trie.Version, peek bool) []byte { + // skip root node cache, since root node is already cached in root node cache + if len(path) == 0 { + return nil + } // the version part v := binary.AppendUvarint((*keyBuf)[:0], uint64(ver.Major)) v = binary.AppendUvarint(v, uint64(ver.Minor)) diff --git a/muxdb/cache_test.go b/muxdb/cache_test.go index 6e14a1d2b..ecaf3fb7e 100644 --- a/muxdb/cache_test.go +++ b/muxdb/cache_test.go @@ -43,16 +43,16 @@ func TestCacheNodeBlob(t *testing.T) { ) // add to committing cache - cache.AddNodeBlob(&keyBuf, "", nil, ver, blob, true) - assert.Equal(t, blob, cache.GetNodeBlob(&keyBuf, "", nil, ver, false)) + cache.AddNodeBlob(&keyBuf, "", []byte{0x0a}, ver, blob, true) + assert.Equal(t, blob, cache.GetNodeBlob(&keyBuf, "", []byte{0x0a}, ver, false)) // minor ver not matched assert.Nil(t, cache.GetNodeBlob(&keyBuf, "", nil, trie.Version{Major: 1}, false)) cache = newCache(1, 0) // add to querying cache - cache.AddNodeBlob(&keyBuf, "", nil, ver, blob, false) - assert.Equal(t, blob, cache.GetNodeBlob(&keyBuf, "", nil, ver, false)) + cache.AddNodeBlob(&keyBuf, "", []byte{0x0b}, ver, blob, false) + assert.Equal(t, blob, cache.GetNodeBlob(&keyBuf, "", []byte{0x0b}, ver, false)) // minor ver not matched assert.Nil(t, cache.GetNodeBlob(&keyBuf, "", nil, trie.Version{Major: 1}, false)) } diff --git a/muxdb/trie.go b/muxdb/trie.go index f0da76e5e..d6e9b9e02 100644 --- a/muxdb/trie.go +++ b/muxdb/trie.go @@ -7,6 +7,7 @@ package muxdb import ( "context" + "errors" "github.com/vechain/thor/v2/thor" "github.com/vechain/thor/v2/trie" @@ -73,6 +74,12 @@ func (t *Trie) newDatabaseReader() trie.DatabaseReader { return } + // enforce root node to be only fetched from hist space + // to prevent accessing root node of a revision that has been pruned + if len(path) == 0 { + return nil, errors.New("not found") + } + // then from deduped space keyBuf = t.back.AppendDedupedNodeKey(keyBuf[:0], t.name, path, ver) return snapshot.Get(keyBuf) diff --git a/muxdb/trie_test.go b/muxdb/trie_test.go index 98933482e..adbf44a22 100644 --- a/muxdb/trie_test.go +++ b/muxdb/trie_test.go @@ -8,6 +8,8 @@ package muxdb import ( "context" "encoding/binary" + "math" + "strconv" "testing" "github.com/stretchr/testify/assert" @@ -15,6 +17,7 @@ import ( "github.com/syndtr/goleveldb/leveldb/storage" "github.com/vechain/thor/v2/muxdb/engine" + "github.com/vechain/thor/v2/thor" "github.com/vechain/thor/v2/trie" ) @@ -242,3 +245,80 @@ func TestContextChecker(t *testing.T) { } t.Error("Expected context canceled error") } + +func TestGetAfterPrune(t *testing.T) { + name := "prune-test" + + db, _ := leveldb.Open(storage.NewMemStorage(), nil) + engine := engine.NewLevelEngine(db) + back := &backend{ + Store: engine, + Cache: &dummyCache{}, + HistPtnFactor: 1, + DedupedPtnFactor: math.MaxUint32, + CachedNodeTTL: 100, + } + + mux := &MuxDB{ + engine: engine, + trieBackend: back, + done: make(chan struct{}), + } + + key := thor.Blake2b([]byte("key")).Bytes() + root := trie.Root{} + root3 := trie.Root{} + root4 := trie.Root{} + for i := range uint32(5) { + tr := newTrie(name, back, root) + + value := []byte("checkpoint-" + strconv.Itoa(int(i))) + + err := tr.Update(key, value, nil) + assert.Nil(t, err) + + ver := trie.Version{Major: i} + err = tr.Commit(ver, false) + assert.Nil(t, err) + root = trie.Root{ + Hash: tr.Hash(), + Ver: ver, + } + if i == 3 { + root3 = root + } + if i == 4 { + root4 = root + } + } + + tr3 := newTrie(name, back, root3) + value, _, err := tr3.Get(key) + assert.Nil(t, err) + assert.Equal(t, "checkpoint-3", string(value)) + + // checkpoint(squash) for [0,3], keep 4 in the hist space + err = tr3.Checkpoint(context.Background(), 0, nil) + assert.Nil(t, err) + + tr3 = newTrie(name, back, root3) + value, _, err = tr3.Get(key) + assert.Nil(t, err) + assert.Equal(t, "checkpoint-3", string(value)) + + // delete [0,4) in hist space + err = mux.DeleteTrieHistoryNodes(context.Background(), 0, 4) + assert.Nil(t, err) + + // version 4 can be still read + tr4 := newTrie(name, back, root4) + value, _, err = tr4.Get(key) + assert.Nil(t, err) + assert.Equal(t, "checkpoint-4", string(value)) + + // version 3 can not be read since it's been pruned + tr3 = newTrie(name, back, root3) + _, _, err = tr3.Get(key) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "missing trie node") +}