Skip to content

Commit

Permalink
Merge pull request #254 from cosmos/feature/historical-queries
Browse files Browse the repository at this point in the history
Historical queries support
  • Loading branch information
cloudhead authored Oct 10, 2017
2 parents f40fa5b + 7050862 commit 240f262
Show file tree
Hide file tree
Showing 12 changed files with 100 additions and 121 deletions.
4 changes: 3 additions & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ func (app *Basecoin) GetState() sm.SimpleDB {
// Info - ABCI
func (app *Basecoin) Info(req abci.RequestInfo) abci.ResponseInfo {
resp := app.state.Info()
app.logger.Debug("Info",
"height", resp.LastBlockHeight,
"hash", fmt.Sprintf("%X", resp.LastBlockAppHash))
app.height = resp.LastBlockHeight
return abci.ResponseInfo{
Data: fmt.Sprintf("Basecoin v%v", version.Version),
Expand All @@ -70,7 +73,6 @@ func (app *Basecoin) Info(req abci.RequestInfo) abci.ResponseInfo {
// InitState - used to setup state (was SetOption)
// to be used by InitChain later
func (app *Basecoin) InitState(key string, value string) string {

module, key := splitKey(key)
state := app.state.Append()

Expand Down
7 changes: 4 additions & 3 deletions app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

abci "github.com/tendermint/abci/types"
sdk "github.com/cosmos/cosmos-sdk"
"github.com/cosmos/cosmos-sdk/modules/auth"
"github.com/cosmos/cosmos-sdk/modules/base"
Expand All @@ -18,6 +17,7 @@ import (
"github.com/cosmos/cosmos-sdk/modules/roles"
"github.com/cosmos/cosmos-sdk/stack"
"github.com/cosmos/cosmos-sdk/state"
abci "github.com/tendermint/abci/types"
wire "github.com/tendermint/go-wire"
"github.com/tendermint/tmlibs/log"
)
Expand Down Expand Up @@ -294,9 +294,10 @@ func TestQuery(t *testing.T) {
res = at.app.Commit()
assert.True(res.IsOK(), res)

key := stack.PrefixedKey(coin.NameCoin, at.acctIn.Address())
resQueryPostCommit := at.app.Query(abci.RequestQuery{
Path: "/account",
Data: at.acctIn.Address(),
Path: "/key",
Data: key,
})
assert.NotEqual(resQueryPreCommit, resQueryPostCommit, "Query should change before/after commit")
}
Expand Down
116 changes: 42 additions & 74 deletions app/store.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package app

import (
"bytes"
"fmt"
"path"
"path/filepath"
"strings"

"github.com/pkg/errors"
abci "github.com/tendermint/abci/types"
"github.com/tendermint/go-wire"
"github.com/tendermint/iavl"
cmn "github.com/tendermint/tmlibs/common"
dbm "github.com/tendermint/tmlibs/db"
Expand All @@ -21,21 +19,10 @@ import (
// Store contains the merkle tree, and all info to handle abci requests
type Store struct {
state.State
height uint64
hash []byte
persisted bool

height uint64
logger log.Logger
}

var stateKey = []byte("merkle:state") // Database key for merkle tree save value db values

// ChainState contains the latest Merkle root hash and the number of times `Commit` has been called
type ChainState struct {
Hash []byte
Height uint64
}

// MockStore returns an in-memory store only intended for testing
func MockStore() *Store {
res, err := NewStore("", 0, log.NewNopLogger())
Expand All @@ -46,22 +33,17 @@ func MockStore() *Store {
return res
}

// NewStore initializes an in-memory IAVLTree, or attempts to load a persistant
// tree from disk
// NewStore initializes an in-memory iavl.VersionedTree, or attempts to load a
// persistant tree from disk
func NewStore(dbName string, cacheSize int, logger log.Logger) (*Store, error) {
// start at 1 so the height returned by query is for the
// next block, ie. the one that includes the AppHash for our current state
initialHeight := uint64(1)

// Non-persistent case
// memory backed case, just for testing
if dbName == "" {
tree := iavl.NewIAVLTree(
tree := iavl.NewVersionedTree(
0,
nil,
dbm.NewMemDB(),
)
store := &Store{
State: state.NewState(tree, false),
height: initialHeight,
State: state.NewState(tree),
logger: logger,
}
return store, nil
Expand All @@ -85,102 +67,88 @@ func NewStore(dbName string, cacheSize int, logger log.Logger) (*Store, error) {

// Open database called "dir/name.db", if it doesn't exist it will be created
db := dbm.NewDB(name, dbm.LevelDBBackendStr, dir)
tree := iavl.NewIAVLTree(cacheSize, db)
tree := iavl.NewVersionedTree(cacheSize, db)

var chainState ChainState
if empty {
logger.Info("no existing db, creating new db")
chainState = ChainState{
Hash: tree.Save(),
Height: initialHeight,
}
db.Set(stateKey, wire.BinaryBytes(chainState))
} else {
logger.Info("loading existing db")
eyesStateBytes := db.Get(stateKey)
err = wire.ReadBinaryBytes(eyesStateBytes, &chainState)
if err != nil {
return nil, errors.Wrap(err, "Reading MerkleEyesState")
if err = tree.Load(); err != nil {
return nil, errors.Wrap(err, "Loading tree")
}
tree.Load(chainState.Hash)
}

res := &Store{
State: state.NewState(tree, true),
height: chainState.Height,
hash: chainState.Hash,
persisted: true,
logger: logger,
State: state.NewState(tree),
logger: logger,
}
res.height = res.State.LatestHeight()
return res, nil
}

// Hash gets the last hash stored in the database
func (s *Store) Hash() []byte {
return s.State.LatestHash()
}

// Info implements abci.Application. It returns the height, hash and size (in the data).
// The height is the block that holds the transactions, not the apphash itself.
func (s *Store) Info() abci.ResponseInfo {
s.logger.Info("Info synced",
"height", s.height,
"hash", fmt.Sprintf("%X", s.hash))
"hash", fmt.Sprintf("%X", s.Hash()))
return abci.ResponseInfo{
Data: cmn.Fmt("size:%v", s.State.Size()),
LastBlockHeight: s.height - 1,
LastBlockAppHash: s.hash,
LastBlockHeight: s.height,
LastBlockAppHash: s.Hash(),
}
}

// Commit implements abci.Application
func (s *Store) Commit() abci.Result {
var err error
s.height++
s.hash, err = s.State.Hash()

hash, err := s.State.Commit(s.height)
if err != nil {
return abci.NewError(abci.CodeType_InternalError, err.Error())
}

s.logger.Debug("Commit synced",
"height", s.height,
"hash", fmt.Sprintf("%X", s.hash))

s.State.BatchSet(stateKey, wire.BinaryBytes(ChainState{
Hash: s.hash,
Height: s.height,
}))

hash, err := s.State.Commit()
if err != nil {
return abci.NewError(abci.CodeType_InternalError, err.Error())
}
if !bytes.Equal(hash, s.hash) {
return abci.NewError(abci.CodeType_InternalError, "AppHash is incorrect")
}
"hash", fmt.Sprintf("%X", hash),
)

if s.State.Size() == 0 {
return abci.NewResultOK(nil, "Empty hash for empty tree")
}
return abci.NewResultOK(s.hash, "")
return abci.NewResultOK(hash, "")
}

// Query implements abci.Application
func (s *Store) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) {

if reqQuery.Height != 0 {
// TODO: support older commits
resQuery.Code = abci.CodeType_InternalError
resQuery.Log = "merkleeyes only supports queries on latest commit"
return
}

// set the query response height to current
resQuery.Height = s.height

tree := s.State.Committed()

height := reqQuery.Height
if height == 0 {
// TODO: once the rpc actually passes in non-zero
// heights we can use to query right after a tx
// we must retrun most recent, even if apphash
// is not yet in the blockchain

// if tree.Tree.VersionExists(s.height - 1) {
// height = s.height - 1
// } else {
height = s.height
// }
}
resQuery.Height = height

switch reqQuery.Path {
case "/store", "/key": // Get by key
key := reqQuery.Data // Data holds the key bytes
resQuery.Key = key
if reqQuery.Prove {
value, proof, err := tree.GetWithProof(key)
value, proof, err := tree.GetVersionedWithProof(key, height)
if err != nil {
resQuery.Log = err.Error()
break
Expand Down
5 changes: 3 additions & 2 deletions client/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ func GetWithProof(key []byte, node client.Client, cert certifiers.Certifier) (
return
}

check, err := GetCertifiedCheckpoint(int(resp.Height), node, cert)
// AppHash for height H is in header H+1
var check lc.Checkpoint
check, err = GetCertifiedCheckpoint(int(resp.Height+1), node, cert)
if err != nil {
return
}
Expand Down Expand Up @@ -69,7 +71,6 @@ func GetWithProof(key []byte, node client.Client, cert certifiers.Certifier) (
err = errors.Wrap(err, "Error reading proof")
return
}

// Validate the proof against the certified header to ensure data integrity.
err = aproof.Verify(resp.Key, nil, check.Header.AppHash)
if err != nil {
Expand Down
20 changes: 12 additions & 8 deletions client/query_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package client

import (
"fmt"
"os"
"testing"

Expand Down Expand Up @@ -103,13 +104,14 @@ func TestTxProofs(t *testing.T) {
cl := client.NewLocal(node)
client.WaitForHeight(cl, 1, nil)

tx := eyes.SetTx{Key: []byte("key-a"), Value: []byte("value-a")}.Wrap()
tx := eyes.NewSetTx([]byte("key-a"), []byte("value-a"))

btx := types.Tx(wire.BinaryBytes(tx))
br, err := cl.BroadcastTxCommit(btx)
require.NoError(err, "%+v", err)
require.EqualValues(0, br.CheckTx.Code, "%#v", br.CheckTx)
require.EqualValues(0, br.DeliverTx.Code)
fmt.Printf("tx height: %d\n", br.Height)

source := certclient.New(cl)
seed, err := source.GetByHeight(br.Height - 2)
Expand All @@ -118,18 +120,20 @@ func TestTxProofs(t *testing.T) {

// First let's make sure a bogus transaction hash returns a valid non-existence proof.
key := types.Tx([]byte("bogus")).Hash()
bs, _, proof, err := GetWithProof(key, cl, cert)
assert.Nil(bs, "value should be nil")
require.True(lc.IsNoDataErr(err), "error should signal 'no data'")
require.NotNil(proof, "proof shouldn't be nil")
err = proof.Verify(key, nil, proof.Root())
require.NoError(err, "%+v", err)
res, err := cl.Tx(key, true)
require.NotNil(err)
require.Contains(err.Error(), "not found")

// Now let's check with the real tx hash.
key = btx.Hash()
res, err := cl.Tx(key, true)
res, err = cl.Tx(key, true)
require.NoError(err, "%+v", err)
require.NotNil(res)
err = res.Proof.Validate(key)
assert.NoError(err, "%+v", err)

check, err := GetCertifiedCheckpoint(int(br.Height), cl, cert)
require.Nil(err, "%+v", err)
require.Equal(res.Proof.RootHash, check.Header.DataHash)

}
14 changes: 7 additions & 7 deletions glide.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion glide.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import:
- package: github.com/spf13/viper
- package: github.com/tendermint/abci
version: develop
version:
subpackages:
- server
- types
Expand Down
3 changes: 2 additions & 1 deletion stack/checkpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/tendermint/iavl"
db "github.com/tendermint/tmlibs/db"
"github.com/tendermint/tmlibs/log"

sdk "github.com/cosmos/cosmos-sdk"
Expand All @@ -17,7 +18,7 @@ import (
func makeState() state.SimpleDB {
// return state.NewMemKVStore()

return state.NewBonsai(iavl.NewIAVLTree(0, nil))
return state.NewBonsai(iavl.NewVersionedTree(0, db.NewMemDB()))

// tree with persistence....
// tmpDir, err := ioutil.TempDir("", "state-tests")
Expand Down
Loading

0 comments on commit 240f262

Please sign in to comment.