Merge pull request #254 from cosmos/feature/historical-queries

Historical queries support
This commit is contained in:
Alexis Sellier 2017-10-10 18:32:53 +02:00 committed by GitHub
commit 240f262cff
12 changed files with 100 additions and 121 deletions

View File

@ -59,6 +59,9 @@ func (app *Basecoin) GetState() sm.SimpleDB {
// Info - ABCI // Info - ABCI
func (app *Basecoin) Info(req abci.RequestInfo) abci.ResponseInfo { func (app *Basecoin) Info(req abci.RequestInfo) abci.ResponseInfo {
resp := app.state.Info() resp := app.state.Info()
app.logger.Debug("Info",
"height", resp.LastBlockHeight,
"hash", fmt.Sprintf("%X", resp.LastBlockAppHash))
app.height = resp.LastBlockHeight app.height = resp.LastBlockHeight
return abci.ResponseInfo{ return abci.ResponseInfo{
Data: fmt.Sprintf("Basecoin v%v", version.Version), Data: fmt.Sprintf("Basecoin v%v", version.Version),
@ -70,7 +73,6 @@ func (app *Basecoin) Info(req abci.RequestInfo) abci.ResponseInfo {
// InitState - used to setup state (was SetOption) // InitState - used to setup state (was SetOption)
// to be used by InitChain later // to be used by InitChain later
func (app *Basecoin) InitState(key string, value string) string { func (app *Basecoin) InitState(key string, value string) string {
module, key := splitKey(key) module, key := splitKey(key)
state := app.state.Append() state := app.state.Append()

View File

@ -7,7 +7,6 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
abci "github.com/tendermint/abci/types"
sdk "github.com/cosmos/cosmos-sdk" sdk "github.com/cosmos/cosmos-sdk"
"github.com/cosmos/cosmos-sdk/modules/auth" "github.com/cosmos/cosmos-sdk/modules/auth"
"github.com/cosmos/cosmos-sdk/modules/base" "github.com/cosmos/cosmos-sdk/modules/base"
@ -18,6 +17,7 @@ import (
"github.com/cosmos/cosmos-sdk/modules/roles" "github.com/cosmos/cosmos-sdk/modules/roles"
"github.com/cosmos/cosmos-sdk/stack" "github.com/cosmos/cosmos-sdk/stack"
"github.com/cosmos/cosmos-sdk/state" "github.com/cosmos/cosmos-sdk/state"
abci "github.com/tendermint/abci/types"
wire "github.com/tendermint/go-wire" wire "github.com/tendermint/go-wire"
"github.com/tendermint/tmlibs/log" "github.com/tendermint/tmlibs/log"
) )
@ -294,9 +294,10 @@ func TestQuery(t *testing.T) {
res = at.app.Commit() res = at.app.Commit()
assert.True(res.IsOK(), res) assert.True(res.IsOK(), res)
key := stack.PrefixedKey(coin.NameCoin, at.acctIn.Address())
resQueryPostCommit := at.app.Query(abci.RequestQuery{ resQueryPostCommit := at.app.Query(abci.RequestQuery{
Path: "/account", Path: "/key",
Data: at.acctIn.Address(), Data: key,
}) })
assert.NotEqual(resQueryPreCommit, resQueryPostCommit, "Query should change before/after commit") assert.NotEqual(resQueryPreCommit, resQueryPostCommit, "Query should change before/after commit")
} }

View File

@ -1,7 +1,6 @@
package app package app
import ( import (
"bytes"
"fmt" "fmt"
"path" "path"
"path/filepath" "path/filepath"
@ -9,7 +8,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
abci "github.com/tendermint/abci/types" abci "github.com/tendermint/abci/types"
"github.com/tendermint/go-wire"
"github.com/tendermint/iavl" "github.com/tendermint/iavl"
cmn "github.com/tendermint/tmlibs/common" cmn "github.com/tendermint/tmlibs/common"
dbm "github.com/tendermint/tmlibs/db" dbm "github.com/tendermint/tmlibs/db"
@ -22,20 +20,9 @@ import (
type Store struct { type Store struct {
state.State state.State
height uint64 height uint64
hash []byte
persisted bool
logger log.Logger 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 // MockStore returns an in-memory store only intended for testing
func MockStore() *Store { func MockStore() *Store {
res, err := NewStore("", 0, log.NewNopLogger()) res, err := NewStore("", 0, log.NewNopLogger())
@ -46,22 +33,17 @@ func MockStore() *Store {
return res return res
} }
// NewStore initializes an in-memory IAVLTree, or attempts to load a persistant // NewStore initializes an in-memory iavl.VersionedTree, or attempts to load a
// tree from disk // persistant tree from disk
func NewStore(dbName string, cacheSize int, logger log.Logger) (*Store, error) { func NewStore(dbName string, cacheSize int, logger log.Logger) (*Store, error) {
// start at 1 so the height returned by query is for the // memory backed case, just for testing
// next block, ie. the one that includes the AppHash for our current state
initialHeight := uint64(1)
// Non-persistent case
if dbName == "" { if dbName == "" {
tree := iavl.NewIAVLTree( tree := iavl.NewVersionedTree(
0, 0,
nil, dbm.NewMemDB(),
) )
store := &Store{ store := &Store{
State: state.NewState(tree, false), State: state.NewState(tree),
height: initialHeight,
logger: logger, logger: logger,
} }
return store, nil return store, nil
@ -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 // Open database called "dir/name.db", if it doesn't exist it will be created
db := dbm.NewDB(name, dbm.LevelDBBackendStr, dir) db := dbm.NewDB(name, dbm.LevelDBBackendStr, dir)
tree := iavl.NewIAVLTree(cacheSize, db) tree := iavl.NewVersionedTree(cacheSize, db)
var chainState ChainState
if empty { if empty {
logger.Info("no existing db, creating new db") logger.Info("no existing db, creating new db")
chainState = ChainState{
Hash: tree.Save(),
Height: initialHeight,
}
db.Set(stateKey, wire.BinaryBytes(chainState))
} else { } else {
logger.Info("loading existing db") logger.Info("loading existing db")
eyesStateBytes := db.Get(stateKey) if err = tree.Load(); err != nil {
err = wire.ReadBinaryBytes(eyesStateBytes, &chainState) return nil, errors.Wrap(err, "Loading tree")
if err != nil {
return nil, errors.Wrap(err, "Reading MerkleEyesState")
} }
tree.Load(chainState.Hash)
} }
res := &Store{ res := &Store{
State: state.NewState(tree, true), State: state.NewState(tree),
height: chainState.Height,
hash: chainState.Hash,
persisted: true,
logger: logger, logger: logger,
} }
res.height = res.State.LatestHeight()
return res, nil 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). // 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. // The height is the block that holds the transactions, not the apphash itself.
func (s *Store) Info() abci.ResponseInfo { func (s *Store) Info() abci.ResponseInfo {
s.logger.Info("Info synced", s.logger.Info("Info synced",
"height", s.height, "height", s.height,
"hash", fmt.Sprintf("%X", s.hash)) "hash", fmt.Sprintf("%X", s.Hash()))
return abci.ResponseInfo{ return abci.ResponseInfo{
Data: cmn.Fmt("size:%v", s.State.Size()), Data: cmn.Fmt("size:%v", s.State.Size()),
LastBlockHeight: s.height - 1, LastBlockHeight: s.height,
LastBlockAppHash: s.hash, LastBlockAppHash: s.Hash(),
} }
} }
// Commit implements abci.Application // Commit implements abci.Application
func (s *Store) Commit() abci.Result { func (s *Store) Commit() abci.Result {
var err error
s.height++ s.height++
s.hash, err = s.State.Hash()
hash, err := s.State.Commit(s.height)
if err != nil { if err != nil {
return abci.NewError(abci.CodeType_InternalError, err.Error()) return abci.NewError(abci.CodeType_InternalError, err.Error())
} }
s.logger.Debug("Commit synced", s.logger.Debug("Commit synced",
"height", s.height, "height", s.height,
"hash", fmt.Sprintf("%X", s.hash)) "hash", fmt.Sprintf("%X", 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")
}
if s.State.Size() == 0 { if s.State.Size() == 0 {
return abci.NewResultOK(nil, "Empty hash for empty tree") return abci.NewResultOK(nil, "Empty hash for empty tree")
} }
return abci.NewResultOK(s.hash, "") return abci.NewResultOK(hash, "")
} }
// Query implements abci.Application // Query implements abci.Application
func (s *Store) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) { 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 // set the query response height to current
resQuery.Height = s.height
tree := s.State.Committed() 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 { switch reqQuery.Path {
case "/store", "/key": // Get by key case "/store", "/key": // Get by key
key := reqQuery.Data // Data holds the key bytes key := reqQuery.Data // Data holds the key bytes
resQuery.Key = key resQuery.Key = key
if reqQuery.Prove { if reqQuery.Prove {
value, proof, err := tree.GetWithProof(key) value, proof, err := tree.GetVersionedWithProof(key, height)
if err != nil { if err != nil {
resQuery.Log = err.Error() resQuery.Log = err.Error()
break break

View File

@ -39,7 +39,9 @@ func GetWithProof(key []byte, node client.Client, cert certifiers.Certifier) (
return 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 { if err != nil {
return return
} }
@ -69,7 +71,6 @@ func GetWithProof(key []byte, node client.Client, cert certifiers.Certifier) (
err = errors.Wrap(err, "Error reading proof") err = errors.Wrap(err, "Error reading proof")
return return
} }
// Validate the proof against the certified header to ensure data integrity. // Validate the proof against the certified header to ensure data integrity.
err = aproof.Verify(resp.Key, nil, check.Header.AppHash) err = aproof.Verify(resp.Key, nil, check.Header.AppHash)
if err != nil { if err != nil {

View File

@ -1,6 +1,7 @@
package client package client
import ( import (
"fmt"
"os" "os"
"testing" "testing"
@ -103,13 +104,14 @@ func TestTxProofs(t *testing.T) {
cl := client.NewLocal(node) cl := client.NewLocal(node)
client.WaitForHeight(cl, 1, nil) 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)) btx := types.Tx(wire.BinaryBytes(tx))
br, err := cl.BroadcastTxCommit(btx) br, err := cl.BroadcastTxCommit(btx)
require.NoError(err, "%+v", err) require.NoError(err, "%+v", err)
require.EqualValues(0, br.CheckTx.Code, "%#v", br.CheckTx) require.EqualValues(0, br.CheckTx.Code, "%#v", br.CheckTx)
require.EqualValues(0, br.DeliverTx.Code) require.EqualValues(0, br.DeliverTx.Code)
fmt.Printf("tx height: %d\n", br.Height)
source := certclient.New(cl) source := certclient.New(cl)
seed, err := source.GetByHeight(br.Height - 2) seed, err := source.GetByHeight(br.Height - 2)
@ -118,18 +120,20 @@ func TestTxProofs(t *testing.T) {
// First let's make sure a bogus transaction hash returns a valid non-existence proof. // First let's make sure a bogus transaction hash returns a valid non-existence proof.
key := types.Tx([]byte("bogus")).Hash() key := types.Tx([]byte("bogus")).Hash()
bs, _, proof, err := GetWithProof(key, cl, cert) res, err := cl.Tx(key, true)
assert.Nil(bs, "value should be nil") require.NotNil(err)
require.True(lc.IsNoDataErr(err), "error should signal 'no data'") require.Contains(err.Error(), "not found")
require.NotNil(proof, "proof shouldn't be nil")
err = proof.Verify(key, nil, proof.Root())
require.NoError(err, "%+v", err)
// Now let's check with the real tx hash. // Now let's check with the real tx hash.
key = btx.Hash() key = btx.Hash()
res, err := cl.Tx(key, true) res, err = cl.Tx(key, true)
require.NoError(err, "%+v", err) require.NoError(err, "%+v", err)
require.NotNil(res) require.NotNil(res)
err = res.Proof.Validate(key) err = res.Proof.Validate(key)
assert.NoError(err, "%+v", err) 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
glide.lock generated
View File

@ -1,5 +1,5 @@
hash: 647d25291b8e9a85cb4a49abc972a41537e8a286514bf41180845785e3b180a4 hash: fbfdd03c0367bb0785ceb81ed34059df219e55d5a9c71c12597e505fbce14165
updated: 2017-10-02T23:59:53.784455453-04:00 updated: 2017-10-10T17:14:33.612302321+02:00
imports: imports:
- name: github.com/bgentry/speakeasy - name: github.com/bgentry/speakeasy
version: 4aabc24848ce5fd31929f7d1e4ea74d3709c14cd version: 4aabc24848ce5fd31929f7d1e4ea74d3709c14cd
@ -108,7 +108,7 @@ imports:
- leveldb/table - leveldb/table
- leveldb/util - leveldb/util
- name: github.com/tendermint/abci - name: github.com/tendermint/abci
version: 191c4b6d176169ffc7f9972d490fa362a3b7d940 version: 15cd7fb1e3b75c436b6dee89a44db35f3d265bd0
subpackages: subpackages:
- client - client
- example/dummy - example/dummy
@ -128,12 +128,12 @@ imports:
- keys/storage/memstorage - keys/storage/memstorage
- keys/wordlist - keys/wordlist
- name: github.com/tendermint/go-wire - name: github.com/tendermint/go-wire
version: 5f88da3dbc1a72844e6dfaf274ce87f851d488eb version: ddbcd39cf68f7026d12f81c66a3cb45fc38ac48b
subpackages: subpackages:
- data - data
- data/base58 - data/base58
- name: github.com/tendermint/iavl - name: github.com/tendermint/iavl
version: 2d3ca4f466c32953641d4c49cad3d93eb7876a5e version: 372f484952449aae18cce33b82c13329a9009acf
- name: github.com/tendermint/light-client - name: github.com/tendermint/light-client
version: ac2e4bf47b31aaf5d3d336691ac786ec751bfc32 version: ac2e4bf47b31aaf5d3d336691ac786ec751bfc32
subpackages: subpackages:
@ -146,7 +146,7 @@ imports:
subpackages: subpackages:
- iavl - iavl
- name: github.com/tendermint/tendermint - name: github.com/tendermint/tendermint
version: 97e980225530133c7b7e2b45e5e65c1f78ace89b version: 49653d3e31e34a5da83e16e9ffdcd95a68acd9be
subpackages: subpackages:
- blockchain - blockchain
- cmd/tendermint/commands - cmd/tendermint/commands
@ -173,7 +173,7 @@ imports:
- types - types
- version - version
- name: github.com/tendermint/tmlibs - name: github.com/tendermint/tmlibs
version: 096dcb90e60aa00b748b3fe49a4b95e48ebf1e13 version: 7dd6b3d3f8a7a998a79bdd0d8222252b309570f3
subpackages: subpackages:
- autofile - autofile
- cli - cli

View File

@ -8,7 +8,6 @@ import:
- package: github.com/spf13/viper - package: github.com/spf13/viper
- package: github.com/tendermint/abci - package: github.com/tendermint/abci
version: develop version: develop
version:
subpackages: subpackages:
- server - server
- types - types

View File

@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/tendermint/iavl" "github.com/tendermint/iavl"
db "github.com/tendermint/tmlibs/db"
"github.com/tendermint/tmlibs/log" "github.com/tendermint/tmlibs/log"
sdk "github.com/cosmos/cosmos-sdk" sdk "github.com/cosmos/cosmos-sdk"
@ -17,7 +18,7 @@ import (
func makeState() state.SimpleDB { func makeState() state.SimpleDB {
// return state.NewMemKVStore() // return state.NewMemKVStore()
return state.NewBonsai(iavl.NewIAVLTree(0, nil)) return state.NewBonsai(iavl.NewVersionedTree(0, db.NewMemDB()))
// tree with persistence.... // tree with persistence....
// tmpDir, err := ioutil.TempDir("", "state-tests") // tmpDir, err := ioutil.TempDir("", "state-tests")

View File

@ -12,7 +12,7 @@ type nonce int64
// Bonsai is a deformed tree forced to fit in a small pot // Bonsai is a deformed tree forced to fit in a small pot
type Bonsai struct { type Bonsai struct {
id nonce id nonce
Tree *iavl.IAVLTree Tree *iavl.VersionedTree
} }
func (b *Bonsai) String() string { func (b *Bonsai) String() string {
@ -22,7 +22,7 @@ func (b *Bonsai) String() string {
var _ SimpleDB = &Bonsai{} var _ SimpleDB = &Bonsai{}
// NewBonsai wraps a merkle tree and tags it to track children // NewBonsai wraps a merkle tree and tags it to track children
func NewBonsai(tree *iavl.IAVLTree) *Bonsai { func NewBonsai(tree *iavl.VersionedTree) *Bonsai {
return &Bonsai{ return &Bonsai{
id: nonce(rand.Int63()), id: nonce(rand.Int63()),
Tree: tree, Tree: tree,
@ -54,6 +54,10 @@ func (b *Bonsai) GetWithProof(key []byte) ([]byte, iavl.KeyProof, error) {
return b.Tree.GetWithProof(key) return b.Tree.GetWithProof(key)
} }
func (b *Bonsai) GetVersionedWithProof(key []byte, version uint64) ([]byte, iavl.KeyProof, error) {
return b.Tree.GetVersionedWithProof(key, version)
}
func (b *Bonsai) List(start, end []byte, limit int) []Model { func (b *Bonsai) List(start, end []byte, limit int) []Model {
res := []Model{} res := []Model{}
stopAtCount := func(key []byte, value []byte) (stop bool) { stopAtCount := func(key []byte, value []byte) (stop bool) {

View File

@ -11,13 +11,13 @@ type State struct {
persistent bool persistent bool
} }
func NewState(tree *iavl.IAVLTree, persistent bool) State { func NewState(tree *iavl.VersionedTree) State {
base := NewBonsai(tree) base := NewBonsai(tree)
return State{ return State{
committed: base, committed: base,
deliverTx: base.Checkpoint(), deliverTx: base.Checkpoint(),
checkTx: base.Checkpoint(), checkTx: base.Checkpoint(),
persistent: persistent, persistent: true,
} }
} }
@ -37,19 +37,12 @@ func (s State) Check() SimpleDB {
return s.checkTx return s.checkTx
} }
// Hash applies deliverTx to committed and calculates hash func (s State) LatestHeight() uint64 {
// return s.committed.Tree.LatestVersion()
// Note the usage: }
// Hash -> gets the calculated hash but doesn't save
// BatchSet -> adds some out-of-bounds data func (s State) LatestHash() []byte {
// Commit -> Save everything to disk and the same hash return s.committed.Tree.Hash()
func (s *State) Hash() ([]byte, error) {
err := s.committed.Commit(s.deliverTx)
if err != nil {
return nil, err
}
s.deliverTx = s.committed.Checkpoint()
return s.committed.Tree.Hash(), nil
} }
// BatchSet is used for some weird magic in storing the new height // BatchSet is used for some weird magic in storing the new height
@ -61,7 +54,7 @@ func (s *State) BatchSet(key, value []byte) {
} }
// Commit save persistent nodes to the database and re-copies the trees // Commit save persistent nodes to the database and re-copies the trees
func (s *State) Commit() ([]byte, error) { func (s *State) Commit(version uint64) ([]byte, error) {
// commit (if we didn't do hash earlier) // commit (if we didn't do hash earlier)
err := s.committed.Commit(s.deliverTx) err := s.committed.Commit(s.deliverTx)
if err != nil { if err != nil {
@ -70,7 +63,12 @@ func (s *State) Commit() ([]byte, error) {
var hash []byte var hash []byte
if s.persistent { if s.persistent {
hash = s.committed.Tree.Save() if s.committed.Tree.Size() > 0 || s.committed.Tree.LatestVersion() > 0 {
hash, err = s.committed.Tree.SaveVersion(version)
if err != nil {
return nil, err
}
}
} else { } else {
hash = s.committed.Tree.Hash() hash = s.committed.Tree.Hash()
} }

View File

@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/tendermint/iavl" "github.com/tendermint/iavl"
db "github.com/tendermint/tmlibs/db"
) )
type keyVal struct { type keyVal struct {
@ -64,8 +65,8 @@ func TestStateCommitHash(t *testing.T) {
result := make([][]byte, len(tc.rounds)) result := make([][]byte, len(tc.rounds))
// make the store... // make the store...
tree := iavl.NewIAVLTree(0, nil) tree := iavl.NewVersionedTree(0, db.NewMemDB())
store := NewState(tree, false) store := NewState(tree)
for n, r := range tc.rounds { for n, r := range tc.rounds {
// start the cache // start the cache
@ -76,7 +77,7 @@ func TestStateCommitHash(t *testing.T) {
deliver.Set(k, v) deliver.Set(k, v)
} }
// commit and add hash to result // commit and add hash to result
hash, err := store.Commit() hash, err := store.Commit(uint64(n + 1))
require.Nil(err, "tc:%d / rnd:%d - %+v", i, n, err) require.Nil(err, "tc:%d / rnd:%d - %+v", i, n, err)
result[n] = hash result[n] = hash
} }

View File

@ -17,11 +17,11 @@ func GetDBs() []SimpleDB {
panic(err) panic(err)
} }
db := dbm.NewDB("test-get-dbs", dbm.LevelDBBackendStr, tmpDir) db := dbm.NewDB("test-get-dbs", dbm.LevelDBBackendStr, tmpDir)
persist := iavl.NewIAVLTree(500, db) persist := iavl.NewVersionedTree(500, db)
return []SimpleDB{ return []SimpleDB{
NewMemKVStore(), NewMemKVStore(),
NewBonsai(iavl.NewIAVLTree(0, nil)), NewBonsai(iavl.NewVersionedTree(0, dbm.NewMemDB())),
NewBonsai(persist), NewBonsai(persist),
} }
} }