Skip to content

Commit

Permalink
Merge PR #4382: Support height queries for queriers
Browse files Browse the repository at this point in the history
  • Loading branch information
alexanderbez authored May 29, 2019
1 parent 61d0f88 commit 8b1d75c
Show file tree
Hide file tree
Showing 11 changed files with 309 additions and 31 deletions.
2 changes: 2 additions & 0 deletions .pending/features/sdk/4318-Support-height-
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#4318 Support height queries. Queries against nodes that have the queried
height pruned will return an error.
9 changes: 9 additions & 0 deletions baseapp/baseapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,15 @@ func handleQueryCustom(app *BaseApp, path []string, req abci.RequestQuery) (res
app.cms.CacheMultiStore(), app.checkState.ctx.BlockHeader(), true, app.logger,
).WithMinGasPrices(app.minGasPrices)

if req.Height > 0 {
cacheMS, err := app.cms.CacheMultiStoreWithVersion(req.Height)
if err != nil {
return sdk.ErrInternal(fmt.Sprintf("failed to load state at height %d; %s", req.Height, err)).QueryResult()
}

ctx = ctx.WithMultiStore(cacheMS)
}

// Passes the rest of the path as an argument to the querier.
//
// For example, in the path "custom/gov/proposal/test", the gov querier gets
Expand Down
4 changes: 3 additions & 1 deletion client/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ func GetCommands(cmds ...*cobra.Command) []*cobra.Command {
c.Flags().Bool(FlagIndentResponse, false, "Add indent to JSON response")
c.Flags().Bool(FlagTrustNode, false, "Trust connected full node (don't verify proofs for responses)")
c.Flags().Bool(FlagUseLedger, false, "Use a connected Ledger device")
c.Flags().String(FlagNode, "tcp://localhost:26657", "<host>:<port> to tendermint rpc interface for this chain")
c.Flags().String(FlagNode, "tcp://localhost:26657", "<host>:<port> to Tendermint RPC interface for this chain")
c.Flags().Int64(FlagHeight, 0, "Use a specific height to query state at (this can error if the node is pruning state)")

viper.BindPFlag(FlagTrustNode, c.Flags().Lookup(FlagTrustNode))
viper.BindPFlag(FlagUseLedger, c.Flags().Lookup(FlagUseLedger))
viper.BindPFlag(FlagNode, c.Flags().Lookup(FlagNode))
Expand Down
4 changes: 4 additions & 0 deletions server/mock/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ func (ms multiStore) CacheMultiStore() sdk.CacheMultiStore {
panic("not implemented")
}

func (kv multiStore) CacheMultiStoreWithVersion(_ int64) (sdk.CacheMultiStore, error) {
panic("not implemented")
}

func (ms multiStore) CacheWrap() sdk.CacheWrap {
panic("not implemented")
}
Expand Down
9 changes: 9 additions & 0 deletions store/cachemulti/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ func (cms Store) CacheMultiStore() types.CacheMultiStore {
return newCacheMultiStoreFromCMS(cms)
}

// CacheMultiStoreWithVersion implements the MultiStore interface. It will panic
// as an already cached multi-store cannot load previous versions.
//
// TODO: The store implementation can possibly be modified to support this as it
// seems safe to load previous versions (heights).
func (cms Store) CacheMultiStoreWithVersion(_ int64) (types.CacheMultiStore, error) {
panic("cannot cache-wrap cached multi-store with a version")
}

// GetStore returns an underlying Store by key.
func (cms Store) GetStore(key types.StoreKey) types.Store {
return cms.stores[key].(types.Store)
Expand Down
52 changes: 47 additions & 5 deletions store/iavl/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ const (
// load the iavl store
func LoadStore(db dbm.DB, id types.CommitID, pruning types.PruningOptions) (types.CommitStore, error) {
tree := iavl.NewMutableTree(db, defaultIAVLCacheSize)

_, err := tree.LoadVersion(id.Version)
if err != nil {
return nil, err
}

iavl := UnsafeNewStore(tree, int64(0), int64(0))
iavl.SetPruning(pruning)

return iavl, nil
}

Expand All @@ -41,8 +44,7 @@ var _ types.Queryable = (*Store)(nil)

// Store Implements types.KVStore and CommitStore.
type Store struct {
// The underlying tree.
tree *iavl.MutableTree
tree Tree

// How many old versions we hold onto.
// A value of 0 means keep no recent states.
Expand All @@ -68,6 +70,28 @@ func UnsafeNewStore(tree *iavl.MutableTree, numRecent int64, storeEvery int64) *
return st
}

// GetImmutable returns a reference to a new store backed by an immutable IAVL
// tree at a specific version (height) without any pruning options. This should
// be used for querying and iteration only. If the version does not exist or has
// been pruned, an error will be returned. Any mutable operations executed will
// result in a panic.
func (st *Store) GetImmutable(version int64) (*Store, error) {
if !st.VersionExists(version) {
return nil, iavl.ErrVersionDoesNotExist
}

iTree, err := st.tree.GetImmutable(version)
if err != nil {
return nil, err
}

return &Store{
tree: &immutableTree{iTree},
numRecent: 0,
storeEvery: 0,
}, nil
}

// Implements Committer.
func (st *Store) Commit() types.CommitID {
// Save a new version.
Expand Down Expand Up @@ -153,16 +177,34 @@ func (st *Store) Delete(key []byte) {

// Implements types.KVStore.
func (st *Store) Iterator(start, end []byte) types.Iterator {
return newIAVLIterator(st.tree.ImmutableTree, start, end, true)
var iTree *iavl.ImmutableTree

switch tree := st.tree.(type) {
case *immutableTree:
iTree = tree.ImmutableTree
case *iavl.MutableTree:
iTree = tree.ImmutableTree
}

return newIAVLIterator(iTree, start, end, true)
}

// Implements types.KVStore.
func (st *Store) ReverseIterator(start, end []byte) types.Iterator {
return newIAVLIterator(st.tree.ImmutableTree, start, end, false)
var iTree *iavl.ImmutableTree

switch tree := st.tree.(type) {
case *immutableTree:
iTree = tree.ImmutableTree
case *iavl.MutableTree:
iTree = tree.ImmutableTree
}

return newIAVLIterator(iTree, start, end, false)
}

// Handle gatest the latest height, if height is 0
func getHeight(tree *iavl.MutableTree, req abci.RequestQuery) int64 {
func getHeight(tree Tree, req abci.RequestQuery) int64 {
height := req.Height
if height == 0 {
latest := tree.Version()
Expand Down
53 changes: 53 additions & 0 deletions store/iavl/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,59 @@ func newAlohaTree(t *testing.T, db dbm.DB) (*iavl.MutableTree, types.CommitID) {
return tree, types.CommitID{ver, hash}
}

func TestGetImmutable(t *testing.T) {
db := dbm.NewMemDB()
tree, cID := newAlohaTree(t, db)
store := UnsafeNewStore(tree, 10, 10)

require.True(t, tree.Set([]byte("hello"), []byte("adios")))
hash, ver, err := tree.SaveVersion()
cID = types.CommitID{ver, hash}
require.Nil(t, err)

_, err = store.GetImmutable(cID.Version + 1)
require.Error(t, err)

newStore, err := store.GetImmutable(cID.Version - 1)
require.NoError(t, err)
require.Equal(t, newStore.Get([]byte("hello")), []byte("goodbye"))

newStore, err = store.GetImmutable(cID.Version)
require.NoError(t, err)
require.Equal(t, newStore.Get([]byte("hello")), []byte("adios"))

res := newStore.Query(abci.RequestQuery{Data: []byte("hello"), Height: cID.Version, Path: "/key", Prove: true})
require.Equal(t, res.Value, []byte("adios"))
require.NotNil(t, res.Proof)

require.Panics(t, func() { newStore.Set(nil, nil) })
require.Panics(t, func() { newStore.Delete(nil) })
require.Panics(t, func() { newStore.Commit() })
}

func TestTestGetImmutableIterator(t *testing.T) {
db := dbm.NewMemDB()
tree, cID := newAlohaTree(t, db)
store := UnsafeNewStore(tree, 10, 10)

newStore, err := store.GetImmutable(cID.Version)
require.NoError(t, err)

iter := newStore.Iterator([]byte("aloha"), []byte("hellz"))
expected := []string{"aloha", "hello"}
var i int

for i = 0; iter.Valid(); iter.Next() {
expectedKey := expected[i]
key, value := iter.Key(), iter.Value()
require.EqualValues(t, key, expectedKey)
require.EqualValues(t, value, treeData[expectedKey])
i++
}

require.Equal(t, len(expected), i)
}

func TestIAVLStoreGetSetHasDelete(t *testing.T) {
db := dbm.NewMemDB()
tree, _ := newAlohaTree(t, db)
Expand Down
84 changes: 84 additions & 0 deletions store/iavl/tree.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package iavl

import (
"fmt"

"github.com/tendermint/iavl"
)

var (
_ Tree = (*immutableTree)(nil)
_ Tree = (*iavl.MutableTree)(nil)
)

type (
// Tree defines an interface that both mutable and immutable IAVL trees
// must implement. For mutable IAVL trees, the interface is directly
// implemented by an iavl.MutableTree. For an immutable IAVL tree, a wrapper
// must be made.
Tree interface {
Has(key []byte) bool
Get(key []byte) (index int64, value []byte)
Set(key, value []byte) bool
Remove(key []byte) ([]byte, bool)
SaveVersion() ([]byte, int64, error)
DeleteVersion(version int64) error
Version() int64
Hash() []byte
VersionExists(version int64) bool
GetVersioned(key []byte, version int64) (int64, []byte)
GetVersionedWithProof(key []byte, version int64) ([]byte, *iavl.RangeProof, error)
GetImmutable(version int64) (*iavl.ImmutableTree, error)
}

// immutableTree is a simple wrapper around a reference to an iavl.ImmutableTree
// that implements the Tree interface. It should only be used for querying
// and iteration, specifically at previous heights.
immutableTree struct {
*iavl.ImmutableTree
}
)

func (it *immutableTree) Set(_, _ []byte) bool {
panic("cannot call 'Set' on an immutable IAVL tree")
}

func (it *immutableTree) Remove(_ []byte) ([]byte, bool) {
panic("cannot call 'Remove' on an immutable IAVL tree")
}

func (it *immutableTree) SaveVersion() ([]byte, int64, error) {
panic("cannot call 'SaveVersion' on an immutable IAVL tree")
}

func (it *immutableTree) DeleteVersion(_ int64) error {
panic("cannot call 'DeleteVersion' on an immutable IAVL tree")
}

func (it *immutableTree) VersionExists(version int64) bool {
return it.Version() == version
}

func (it *immutableTree) GetVersioned(key []byte, version int64) (int64, []byte) {
if it.Version() != version {
return -1, nil
}

return it.Get(key)
}

func (it *immutableTree) GetVersionedWithProof(key []byte, version int64) ([]byte, *iavl.RangeProof, error) {
if it.Version() != version {
return nil, nil, fmt.Errorf("version mismatch on immutable IAVL tree; got: %d, expected: %d", version, it.Version())
}

return it.GetWithProof(key)
}

func (it *immutableTree) GetImmutable(version int64) (*iavl.ImmutableTree, error) {
if it.Version() != version {
return nil, fmt.Errorf("version mismatch on immutable IAVL tree; got: %d, expected: %d", version, it.Version())
}

return it.ImmutableTree, nil
}
Loading

0 comments on commit 8b1d75c

Please sign in to comment.