From f6e7d4b741c384c590c722bf4c3b555d0881bde1 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 25 Jul 2017 22:19:31 -0400 Subject: [PATCH] Pull in logic from merkleeyes, get it all working with trees --- app/app.go | 77 +++---- app/app_test.go | 30 +-- app/genesis_test.go | 25 ++- cmd/basecoin/commands/start.go | 29 +-- docs/guide/counter/plugins/counter/counter.go | 2 +- .../counter/plugins/counter/counter_test.go | 13 +- {modules/base => stack}/checkpoint.go | 16 +- stack/prefixstore.go | 8 + state/merkle/state.go | 83 ++++++++ state/merkle/store.go | 196 ++++++++++++++++++ state/state.go | 82 ++------ state/state_test.go | 142 ++++++------- 12 files changed, 454 insertions(+), 249 deletions(-) rename {modules/base => stack}/checkpoint.go (79%) create mode 100644 state/merkle/state.go create mode 100644 state/merkle/store.go diff --git a/app/app.go b/app/app.go index c5ff53739..e85322c5a 100644 --- a/app/app.go +++ b/app/app.go @@ -5,11 +5,10 @@ import ( "strings" abci "github.com/tendermint/abci/types" - "github.com/tendermint/basecoin" - eyes "github.com/tendermint/merkleeyes/client" cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/log" + "github.com/tendermint/basecoin" "github.com/tendermint/basecoin/errors" "github.com/tendermint/basecoin/modules/auth" "github.com/tendermint/basecoin/modules/base" @@ -19,6 +18,7 @@ import ( "github.com/tendermint/basecoin/modules/roles" "github.com/tendermint/basecoin/stack" sm "github.com/tendermint/basecoin/state" + "github.com/tendermint/basecoin/state/merkle" "github.com/tendermint/basecoin/version" ) @@ -30,27 +30,24 @@ const ( // Basecoin - The ABCI application type Basecoin struct { - eyesCli *eyes.Client - state *sm.State - cacheState *sm.State - handler basecoin.Handler - height uint64 - logger log.Logger + info *sm.ChainState + + state *merkle.Store + + handler basecoin.Handler + height uint64 + logger log.Logger } var _ abci.Application = &Basecoin{} // NewBasecoin - create a new instance of the basecoin application -func NewBasecoin(handler basecoin.Handler, eyesCli *eyes.Client, logger log.Logger) *Basecoin { - state := sm.NewState(eyesCli, logger.With("module", "state")) - +func NewBasecoin(handler basecoin.Handler, store *merkle.Store, logger log.Logger) *Basecoin { return &Basecoin{ - handler: handler, - eyesCli: eyesCli, - state: state, - cacheState: nil, - height: 0, - logger: logger, + handler: handler, + info: sm.NewChainState(), + state: store, + logger: logger, } } @@ -71,21 +68,23 @@ func DefaultHandler(feeDenom string) basecoin.Handler { nonce.ReplayCheck{}, roles.NewMiddleware(), fee.NewSimpleFeeMiddleware(coin.Coin{feeDenom, 0}, fee.Bank), - base.Checkpoint{}, + stack.Checkpoint{}, ).Use(d) } -// GetState - XXX For testing, not thread safe! -func (app *Basecoin) GetState() *sm.State { - return app.state.CacheWrap() +// GetChainID returns the currently stored chain +func (app *Basecoin) GetChainID() string { + return app.info.GetChainID(app.state.Committed()) +} + +// GetState is back... please kill me +func (app *Basecoin) GetState() sm.KVStore { + return app.state.Append() } // Info - ABCI func (app *Basecoin) Info() abci.ResponseInfo { - resp, err := app.eyesCli.InfoSync() - if err != nil { - cmn.PanicCrisis(err) - } + resp := app.state.Info() app.height = resp.LastBlockHeight return abci.ResponseInfo{ Data: fmt.Sprintf("Basecoin v%v", version.Version), @@ -98,16 +97,17 @@ func (app *Basecoin) Info() abci.ResponseInfo { func (app *Basecoin) SetOption(key string, value string) string { module, key := splitKey(key) + state := app.state.Append() if module == ModuleNameBase { if key == ChainKey { - app.state.SetChainID(value) + app.info.SetChainID(state, value) return "Success" } return fmt.Sprintf("Error: unknown base option: %s", key) } - log, err := app.handler.SetOption(app.logger, app.state, module, key, value) + log, err := app.handler.SetOption(app.logger, state, module, key, value) if err == nil { return log } @@ -122,12 +122,11 @@ func (app *Basecoin) DeliverTx(txBytes []byte) abci.Result { } ctx := stack.NewContext( - app.state.GetChainID(), + app.GetChainID(), app.height, app.logger.With("call", "delivertx"), ) - fmt.Printf("state: %#v\n", app.state) - res, err := app.handler.DeliverTx(ctx, app.state, tx) + res, err := app.handler.DeliverTx(ctx, app.state.Append(), tx) if err != nil { // discard the cache... @@ -144,12 +143,11 @@ func (app *Basecoin) CheckTx(txBytes []byte) abci.Result { } ctx := stack.NewContext( - app.state.GetChainID(), + app.GetChainID(), app.height, app.logger.With("call", "checktx"), ) - fmt.Printf("state: %#v\n", app.cacheState) - res, err := app.handler.CheckTx(ctx, app.cacheState, tx) + res, err := app.handler.CheckTx(ctx, app.state.Check(), tx) if err != nil { return errors.Result(err) @@ -165,24 +163,13 @@ func (app *Basecoin) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQu return } - resQuery, err := app.eyesCli.QuerySync(reqQuery) - if err != nil { - resQuery.Log = "Failed to query MerkleEyes: " + err.Error() - resQuery.Code = abci.CodeType_InternalError - return - } - return + return app.state.Query(reqQuery) } // Commit - ABCI func (app *Basecoin) Commit() (res abci.Result) { - // Commit state res = app.state.Commit() - - // Wrap the committed state in cache for CheckTx - app.cacheState = app.state.CacheWrap() - if res.IsErr() { cmn.PanicSanity("Error getting hash: " + res.Error()) } diff --git a/app/app_test.go b/app/app_test.go index 2342bf241..6a0fb5182 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -2,7 +2,6 @@ package app import ( "encoding/hex" - "os" "testing" "github.com/stretchr/testify/assert" @@ -17,8 +16,8 @@ import ( "github.com/tendermint/basecoin/modules/nonce" "github.com/tendermint/basecoin/stack" "github.com/tendermint/basecoin/state" + "github.com/tendermint/basecoin/state/merkle" wire "github.com/tendermint/go-wire" - eyes "github.com/tendermint/merkleeyes/client" "github.com/tendermint/tmlibs/log" ) @@ -82,14 +81,14 @@ func (at *appTest) reset() { at.acctIn = coin.NewAccountWithKey(coin.Coins{{"mycoin", 7}}) at.acctOut = coin.NewAccountWithKey(coin.Coins{{"mycoin", 7}}) - eyesCli := eyes.NewLocalClient("", 0) - // logger := log.TestingLogger().With("module", "app"), - logger := log.NewTMLogger(os.Stdout).With("module", "app") - logger = log.NewTracingLogger(logger) + // Note: switch logger if you want to get more info + logger := log.TestingLogger() + // logger := log.NewTracingLogger(log.NewTMLogger(os.Stdout)) + store := merkle.NewStore("", 0, logger.With("module", "store")) at.app = NewBasecoin( DefaultHandler("mycoin"), - eyesCli, - logger, + store, + logger.With("module", "app"), ) res := at.app.SetOption("base/chain_id", at.chainID) @@ -142,17 +141,18 @@ func TestSetOption(t *testing.T) { assert := assert.New(t) require := require.New(t) - eyesCli := eyes.NewLocalClient("", 0) + logger := log.TestingLogger() + store := merkle.NewStore("", 0, logger.With("module", "store")) app := NewBasecoin( DefaultHandler("atom"), - eyesCli, - log.TestingLogger().With("module", "app"), + store, + logger.With("module", "app"), ) //testing ChainID chainID := "testChain" res := app.SetOption("base/chain_id", chainID) - assert.EqualValues(app.GetState().GetChainID(), chainID) + assert.EqualValues(app.GetChainID(), chainID) assert.EqualValues(res, "Success") // make a nice account... @@ -162,7 +162,7 @@ func TestSetOption(t *testing.T) { require.EqualValues(res, "Success") // make sure it is set correctly, with some balance - coins, err := getBalance(acct.Actor(), app.state) + coins, err := getBalance(acct.Actor(), app.GetState()) require.Nil(err) assert.Equal(bal, coins) @@ -189,7 +189,7 @@ func TestSetOption(t *testing.T) { res = app.SetOption("coin/account", unsortAcc) require.EqualValues(res, "Success") - coins, err = getAddr(unsortAddr, app.state) + coins, err = getAddr(unsortAddr, app.GetState()) require.Nil(err) assert.True(coins.IsValid()) assert.Equal(unsortCoins, coins) @@ -213,6 +213,8 @@ func TestTx(t *testing.T) { //Bad Balance at.acctIn.Coins = coin.Coins{{"mycoin", 2}} at.initAccount(at.acctIn) + at.app.Commit() + res, _, _ := at.exec(t, at.getTx(coin.Coins{{"mycoin", 5}}, 1), true) assert.True(res.IsErr(), "ExecTx/Bad CheckTx: Expected error return from ExecTx, returned: %v", res) res, diffIn, diffOut := at.exec(t, at.getTx(coin.Coins{{"mycoin", 5}}, 1), false) diff --git a/app/genesis_test.go b/app/genesis_test.go index 57c895c8d..50ca77f0f 100644 --- a/app/genesis_test.go +++ b/app/genesis_test.go @@ -8,19 +8,20 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - eyescli "github.com/tendermint/merkleeyes/client" cmn "github.com/tendermint/tmlibs/common" "github.com/tendermint/tmlibs/log" "github.com/tendermint/basecoin/modules/coin" + "github.com/tendermint/basecoin/state/merkle" ) const genesisFilepath = "./testdata/genesis.json" const genesisAcctFilepath = "./testdata/genesis2.json" func TestLoadGenesisDoNotFailIfAppOptionsAreMissing(t *testing.T) { - eyesCli := eyescli.NewLocalClient("", 0) - app := NewBasecoin(DefaultHandler("mycoin"), eyesCli, log.TestingLogger()) + logger := log.TestingLogger() + store := merkle.NewStore("", 0, logger) + app := NewBasecoin(DefaultHandler("mycoin"), store, logger) err := app.LoadGenesis("./testdata/genesis3.json") require.Nil(t, err, "%+v", err) } @@ -28,18 +29,19 @@ func TestLoadGenesisDoNotFailIfAppOptionsAreMissing(t *testing.T) { func TestLoadGenesis(t *testing.T) { assert, require := assert.New(t), require.New(t) - eyesCli := eyescli.NewLocalClient("", 0) - app := NewBasecoin(DefaultHandler("mycoin"), eyesCli, log.TestingLogger()) + logger := log.TestingLogger() + store := merkle.NewStore("", 0, logger) + app := NewBasecoin(DefaultHandler("mycoin"), store, logger) err := app.LoadGenesis(genesisFilepath) require.Nil(err, "%+v", err) // check the chain id - assert.Equal("foo_bar_chain", app.GetState().GetChainID()) + assert.Equal("foo_bar_chain", app.GetChainID()) // and check the account info - previously calculated values addr, _ := hex.DecodeString("eb98e0688217cfdeb70eddf4b33cdcc37fc53197") - coins, err := getAddr(addr, app.state) + coins, err := getAddr(addr, app.GetState()) require.Nil(err) assert.True(coins.IsPositive()) @@ -57,13 +59,14 @@ func TestLoadGenesis(t *testing.T) { func TestLoadGenesisAccountAddress(t *testing.T) { assert, require := assert.New(t), require.New(t) - eyesCli := eyescli.NewLocalClient("", 0) - app := NewBasecoin(DefaultHandler("mycoin"), eyesCli, log.TestingLogger()) + logger := log.TestingLogger() + store := merkle.NewStore("", 0, logger) + app := NewBasecoin(DefaultHandler("mycoin"), store, logger) err := app.LoadGenesis(genesisAcctFilepath) require.Nil(err, "%+v", err) // check the chain id - assert.Equal("addr_accounts_chain", app.GetState().GetChainID()) + assert.Equal("addr_accounts_chain", app.GetChainID()) // make sure the accounts were set properly cases := []struct { @@ -86,7 +89,7 @@ func TestLoadGenesisAccountAddress(t *testing.T) { for i, tc := range cases { addr, err := hex.DecodeString(tc.addr) require.Nil(err, tc.addr) - coins, err := getAddr(addr, app.state) + coins, err := getAddr(addr, app.GetState()) require.Nil(err, "%+v", err) if !tc.exists { assert.True(coins.IsZero(), "%d", i) diff --git a/cmd/basecoin/commands/start.go b/cmd/basecoin/commands/start.go index 4fd2f19b2..53c6d2e9b 100644 --- a/cmd/basecoin/commands/start.go +++ b/cmd/basecoin/commands/start.go @@ -11,8 +11,6 @@ import ( "github.com/tendermint/abci/server" "github.com/tendermint/basecoin" - eyesApp "github.com/tendermint/merkleeyes/app" - eyes "github.com/tendermint/merkleeyes/client" "github.com/tendermint/tmlibs/cli" cmn "github.com/tendermint/tmlibs/common" @@ -22,6 +20,7 @@ import ( "github.com/tendermint/tendermint/types" "github.com/tendermint/basecoin/app" + "github.com/tendermint/basecoin/state/merkle" ) // StartCmd - command to start running the basecoin node! @@ -37,7 +36,6 @@ const EyesCacheSize = 10000 //nolint const ( FlagAddress = "address" - FlagEyes = "eyes" FlagWithoutTendermint = "without-tendermint" ) @@ -50,7 +48,6 @@ var ( func init() { flags := StartCmd.Flags() flags.String(FlagAddress, "tcp://0.0.0.0:46658", "Listen address") - flags.String(FlagEyes, "local", "MerkleEyes address, or 'local' for embedded") flags.Bool(FlagWithoutTendermint, false, "Only run basecoin abci app, assume external tendermint process") // add all standard 'tendermint node' flags tcmd.AddNodeFlags(StartCmd) @@ -58,27 +55,19 @@ func init() { func startCmd(cmd *cobra.Command, args []string) error { rootDir := viper.GetString(cli.HomeFlag) - meyes := viper.GetString(FlagEyes) - // Connect to MerkleEyes - var eyesCli *eyes.Client - if meyes == "local" { - eyesApp.SetLogger(logger.With("module", "merkleeyes")) - eyesCli = eyes.NewLocalClient(path.Join(rootDir, "data", "merkleeyes.db"), EyesCacheSize) - } else { - var err error - eyesCli, err = eyes.NewClient(meyes) - if err != nil { - return errors.Errorf("Error connecting to MerkleEyes: %v\n", err) - } - } + store := merkle.NewStore( + path.Join(rootDir, "data", "merkleeyes.db"), + EyesCacheSize, + logger.With("module", "store"), + ) // Create Basecoin app - basecoinApp := app.NewBasecoin(Handler, eyesCli, logger.With("module", "app")) + basecoinApp := app.NewBasecoin(Handler, store, logger.With("module", "app")) // if chain_id has not been set yet, load the genesis. // else, assume it's been loaded - if basecoinApp.GetState().GetChainID() == "" { + if basecoinApp.GetChainID() == "" { // If genesis file exists, set key-value options genesisFile := path.Join(rootDir, "genesis.json") if _, err := os.Stat(genesisFile); err == nil { @@ -91,7 +80,7 @@ func startCmd(cmd *cobra.Command, args []string) error { } } - chainID := basecoinApp.GetState().GetChainID() + chainID := basecoinApp.GetChainID() if viper.GetBool(FlagWithoutTendermint) { logger.Info("Starting Basecoin without Tendermint", "chain_id", chainID) // run just the abci app/server diff --git a/docs/guide/counter/plugins/counter/counter.go b/docs/guide/counter/plugins/counter/counter.go index 27863c527..fa806d519 100644 --- a/docs/guide/counter/plugins/counter/counter.go +++ b/docs/guide/counter/plugins/counter/counter.go @@ -104,7 +104,7 @@ func NewHandler(feeDenom string) basecoin.Handler { base.Chain{}, nonce.ReplayCheck{}, fee.NewSimpleFeeMiddleware(coin.Coin{feeDenom, 0}, fee.Bank), - base.Checkpoint{}, + stack.Checkpoint{}, ).Use(dispatcher) } diff --git a/docs/guide/counter/plugins/counter/counter_test.go b/docs/guide/counter/plugins/counter/counter_test.go index 5fa83d735..9e8149edd 100644 --- a/docs/guide/counter/plugins/counter/counter_test.go +++ b/docs/guide/counter/plugins/counter/counter_test.go @@ -12,8 +12,8 @@ import ( "github.com/tendermint/basecoin/modules/base" "github.com/tendermint/basecoin/modules/coin" "github.com/tendermint/basecoin/modules/nonce" + "github.com/tendermint/basecoin/state/merkle" "github.com/tendermint/go-wire" - eyescli "github.com/tendermint/merkleeyes/client" "github.com/tendermint/tmlibs/log" ) @@ -21,17 +21,16 @@ func TestCounterPlugin(t *testing.T) { assert := assert.New(t) // Basecoin initialization - eyesCli := eyescli.NewLocalClient("", 0) chainID := "test_chain_id" + logger := log.TestingLogger() + // logger := log.NewTracingLogger(log.NewTMLogger(os.Stdout)) - logger := log.TestingLogger().With("module", "app") - // logger := log.NewTMLogger(os.Stdout).With("module", "app") - logger = log.NewTracingLogger(logger) + store := merkle.NewStore("", 0, logger.With("module", "store")) h := NewHandler("gold") bcApp := app.NewBasecoin( h, - eyesCli, - logger, + store, + logger.With("module", "app"), ) bcApp.SetOption("base/chain_id", chainID) diff --git a/modules/base/checkpoint.go b/stack/checkpoint.go similarity index 79% rename from modules/base/checkpoint.go rename to stack/checkpoint.go index eea375834..a983ae88b 100644 --- a/modules/base/checkpoint.go +++ b/stack/checkpoint.go @@ -1,10 +1,7 @@ -package base +package stack import ( - "fmt" - "github.com/tendermint/basecoin" - "github.com/tendermint/basecoin/stack" "github.com/tendermint/basecoin/state" ) @@ -15,7 +12,7 @@ const ( // Checkpoint isolates all data store below this type Checkpoint struct { - stack.PassOption + PassOption } // Name of the module - fulfills Middleware interface @@ -23,11 +20,11 @@ func (Checkpoint) Name() string { return NameCheckpoint } -var _ stack.Middleware = Chain{} +var _ Middleware = Checkpoint{} // CheckTx reverts all data changes if there was an error func (c Checkpoint) CheckTx(ctx basecoin.Context, store state.KVStore, tx basecoin.Tx, next basecoin.Checker) (res basecoin.Result, err error) { - ps := state.NewKVCache(store) + ps := state.NewKVCache(unwrap(store)) res, err = next.CheckTx(ctx, ps, tx) if err == nil { ps.Sync() @@ -37,13 +34,10 @@ func (c Checkpoint) CheckTx(ctx basecoin.Context, store state.KVStore, tx baseco // DeliverTx reverts all data changes if there was an error func (c Checkpoint) DeliverTx(ctx basecoin.Context, store state.KVStore, tx basecoin.Tx, next basecoin.Deliver) (res basecoin.Result, err error) { - ps := state.NewKVCache(store) + ps := state.NewKVCache(unwrap(store)) res, err = next.DeliverTx(ctx, ps, tx) if err == nil { - fmt.Println("sync") ps.Sync() - } else { - fmt.Println("reject") } return res, err } diff --git a/stack/prefixstore.go b/stack/prefixstore.go index f6934d6e7..f366b2fa2 100644 --- a/stack/prefixstore.go +++ b/stack/prefixstore.go @@ -31,6 +31,14 @@ func stateSpace(store state.KVStore, app string) state.KVStore { return PrefixedStore(app, store) } +func unwrap(store state.KVStore) state.KVStore { + // unwrap one-level if wrapped + if pstore, ok := store.(prefixStore); ok { + store = pstore.store + } + return store +} + // PrefixedStore allows one to create an isolated state-space for a given // app prefix, but it cannot easily be unwrapped // diff --git a/state/merkle/state.go b/state/merkle/state.go new file mode 100644 index 000000000..423a9c6ad --- /dev/null +++ b/state/merkle/state.go @@ -0,0 +1,83 @@ +package merkle + +import ( + "github.com/tendermint/merkleeyes/iavl" + "github.com/tendermint/tmlibs/merkle" +) + +// State represents the app states, separating the commited state (for queries) +// from the working state (for CheckTx and AppendTx) +type State struct { + committed merkle.Tree + deliverTx merkle.Tree + checkTx merkle.Tree + persistent bool +} + +func NewState(tree merkle.Tree, persistent bool) State { + return State{ + committed: tree, + deliverTx: tree.Copy(), + checkTx: tree.Copy(), + persistent: persistent, + } +} + +func (s State) Committed() Bonsai { + return Bonsai{s.committed} +} + +func (s State) Append() Bonsai { + return Bonsai{s.deliverTx} +} + +func (s State) Check() Bonsai { + return Bonsai{s.checkTx} +} + +// Hash updates the tree +func (s *State) Hash() []byte { + return s.deliverTx.Hash() +} + +// BatchSet is used for some weird magic in storing the new height +func (s *State) BatchSet(key, value []byte) { + if s.persistent { + // This is in the batch with the Save, but not in the tree + tree, ok := s.deliverTx.(*iavl.IAVLTree) + if ok { + tree.BatchSet(key, value) + } + } +} + +// Commit save persistent nodes to the database and re-copies the trees +func (s *State) Commit() []byte { + var hash []byte + if s.persistent { + hash = s.deliverTx.Save() + } else { + hash = s.deliverTx.Hash() + } + + s.committed = s.deliverTx + s.deliverTx = s.committed.Copy() + s.checkTx = s.committed.Copy() + return hash +} + +// Bonsai is a deformed tree forced to fit in a small pot +type Bonsai struct { + merkle.Tree +} + +// Get matches the signature of KVStore +func (b Bonsai) Get(key []byte) []byte { + _, value, _ := b.Tree.Get(key) + return value +} + +// Set matches the signature of KVStore +func (b Bonsai) Set(key, value []byte) { + b.Tree.Set(key, value) +} diff --git a/state/merkle/store.go b/state/merkle/store.go new file mode 100644 index 000000000..1f513e989 --- /dev/null +++ b/state/merkle/store.go @@ -0,0 +1,196 @@ +package merkle + +import ( + "bytes" + "fmt" + "path" + "path/filepath" + "strings" + + abci "github.com/tendermint/abci/types" + "github.com/tendermint/go-wire" + cmn "github.com/tendermint/tmlibs/common" + dbm "github.com/tendermint/tmlibs/db" + "github.com/tendermint/tmlibs/log" + + "github.com/tendermint/merkleeyes/iavl" +) + +// Store contains the merkle tree, and all info to handle abci requests +type Store struct { + State + height uint64 + hash []byte + persisted bool + + logger log.Logger +} + +var stateKey = []byte("merkle:state") // Database key for merkle tree save value db values + +// MerkleState contains the latest Merkle root hash and the number of times `Commit` has been called +type MerkleState struct { + Hash []byte + Height uint64 +} + +// NewStore initializes an in-memory IAVLTree, or attempts to load a persistant +// tree from disk +func NewStore(dbName string, cacheSize int, logger log.Logger) *Store { + // 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 + if dbName == "" { + tree := iavl.NewIAVLTree( + 0, + nil, + ) + return &Store{ + State: NewState(tree, false), + height: initialHeight, + logger: logger, + } + } + + // Expand the path fully + dbPath, err := filepath.Abs(dbName) + if err != nil { + panic(fmt.Sprintf("Invalid Database Name: %s", dbName)) + } + + // Some external calls accidently add a ".db", which is now removed + dbPath = strings.TrimSuffix(dbPath, path.Ext(dbPath)) + + // Split the database name into it's components (dir, name) + dir := path.Dir(dbPath) + name := path.Base(dbPath) + + // Make sure the path exists + empty, _ := cmn.IsDirEmpty(dbPath + ".db") + + // 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) + + var eyesState MerkleState + if empty { + logger.Info("no existing db, creating new db") + eyesState = MerkleState{ + Hash: tree.Save(), + Height: initialHeight, + } + db.Set(stateKey, wire.BinaryBytes(eyesState)) + } else { + logger.Info("loading existing db") + eyesStateBytes := db.Get(stateKey) + err = wire.ReadBinaryBytes(eyesStateBytes, &eyesState) + if err != nil { + logger.Error("error reading MerkleEyesState", "err", err) + panic(err) + } + tree.Load(eyesState.Hash) + } + + return &Store{ + State: NewState(tree, true), + height: eyesState.Height, + hash: eyesState.Hash, + persisted: true, + logger: logger, + } +} + +// CloseDB closes the database +// func (s *Store) CloseDB() { +// if s.db != nil { +// s.db.Close() +// } +// } + +// 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)) + return abci.ResponseInfo{ + Data: cmn.Fmt("size:%v", s.State.Committed().Size()), + LastBlockHeight: s.height - 1, + LastBlockAppHash: s.hash, + } +} + +// Commit implements abci.Application +func (s *Store) Commit() abci.Result { + s.hash = s.State.Hash() + s.height++ + s.logger.Debug("Commit synced", "height", s.height, "hash", fmt.Sprintf("%X", s.hash)) + + s.State.BatchSet(stateKey, wire.BinaryBytes(MerkleState{ + Hash: s.hash, + Height: s.height, + })) + + hash := s.State.Commit() + if !bytes.Equal(hash, s.hash) { + panic("AppHash is incorrect") + } + + if s.State.Committed().Size() == 0 { + return abci.NewResultOK(nil, "Empty hash for empty tree") + } + return abci.NewResultOK(s.hash, "") +} + +// Query implements abci.Application +func (s *Store) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) { + if len(reqQuery.Data) == 0 { + return + } + tree := s.State.Committed() + + 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 + + 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, exists := tree.Proof(key) + if !exists { + resQuery.Log = "Key not found" + } + resQuery.Value = value + resQuery.Proof = proof + // TODO: return index too? + } else { + value := tree.Get(key) + resQuery.Value = value + } + + case "/index": // Get by Index + index := wire.GetInt64(reqQuery.Data) + key, value := tree.GetByIndex(int(index)) + resQuery.Key = key + resQuery.Index = int64(index) + resQuery.Value = value + + case "/size": // Get size + size := tree.Size() + sizeBytes := wire.BinaryBytes(size) + resQuery.Value = sizeBytes + + default: + resQuery.Code = abci.CodeType_UnknownRequest + resQuery.Log = cmn.Fmt("Unexpected Query path: %v", reqQuery.Path) + } + return +} diff --git a/state/state.go b/state/state.go index 398e18ca5..2751833d4 100644 --- a/state/state.go +++ b/state/state.go @@ -1,84 +1,26 @@ package state -import ( - abci "github.com/tendermint/abci/types" - eyes "github.com/tendermint/merkleeyes/client" - "github.com/tendermint/tmlibs/log" -) - -// CONTRACT: State should be quick to copy. -// See CacheWrap(). -type State struct { - chainID string - store KVStore - readCache map[string][]byte // optional, for caching writes to store - writeCache *KVCache // optional, for caching writes w/o writing to store - logger log.Logger +// ChainState maintains general information for the chain +type ChainState struct { + chainID string } -func NewState(store KVStore, l log.Logger) *State { - return &State{ - chainID: "", - store: store, - readCache: make(map[string][]byte), - writeCache: nil, - logger: l, - } +// NewChainState creates a blank state +func NewChainState() *ChainState { + return &ChainState{} } -func (s *State) SetChainID(chainID string) { +// SetChainID stores the chain id in the store +func (s *ChainState) SetChainID(store KVStore, chainID string) { s.chainID = chainID - s.store.Set([]byte("base/chain_id"), []byte(chainID)) + store.Set([]byte("base/chain_id"), []byte(chainID)) } -func (s *State) GetChainID() string { +// GetChainID gets the chain id from the cache or the store +func (s *ChainState) GetChainID(store KVStore) string { if s.chainID != "" { return s.chainID } - s.chainID = string(s.store.Get([]byte("base/chain_id"))) + s.chainID = string(store.Get([]byte("base/chain_id"))) return s.chainID } - -func (s *State) Get(key []byte) (value []byte) { - if s.readCache != nil { //if not a cachewrap - value, ok := s.readCache[string(key)] - if ok { - return value - } - } - return s.store.Get(key) -} - -func (s *State) Set(key []byte, value []byte) { - if s.readCache != nil { //if not a cachewrap - s.readCache[string(key)] = value - } - s.store.Set(key, value) -} - -func (s *State) CacheWrap() *State { - cache := NewKVCache(s) - return &State{ - chainID: s.chainID, - store: cache, - readCache: nil, - writeCache: cache, - logger: s.logger, - } -} - -// NOTE: errors if s is not from CacheWrap() -func (s *State) CacheSync() { - s.writeCache.Sync() -} - -func (s *State) Commit() abci.Result { - switch s.store.(type) { - case *eyes.Client: - s.readCache = make(map[string][]byte) - return s.store.(*eyes.Client).CommitSync() - default: - return abci.NewError(abci.CodeType_InternalError, "can only use Commit if store is merkleeyes") - } - -} diff --git a/state/state_test.go b/state/state_test.go index 8cc587b3b..096d27d19 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -1,86 +1,88 @@ package state -import ( - "bytes" - "testing" +// TODO: something similar in the merkle package... - eyes "github.com/tendermint/merkleeyes/client" - "github.com/tendermint/tmlibs/log" +// import ( +// "bytes" +// "testing" - "github.com/stretchr/testify/assert" -) +// eyes "github.com/tendermint/merkleeyes/client" +// "github.com/tendermint/tmlibs/log" -func TestState(t *testing.T) { - assert := assert.New(t) +// "github.com/stretchr/testify/assert" +// ) - //States and Stores for tests - store := NewMemKVStore() - state := NewState(store, log.TestingLogger()) - cache := state.CacheWrap() - eyesCli := eyes.NewLocalClient("", 0) +// func TestState(t *testing.T) { +// assert := assert.New(t) - //reset the store/state/cache - reset := func() { - store = NewMemKVStore() - state = NewState(store, log.TestingLogger()) - cache = state.CacheWrap() - } +// //States and Stores for tests +// store := NewMemKVStore() +// state := NewState(store, log.TestingLogger()) +// cache := state.CacheWrap() +// eyesCli := eyes.NewLocalClient("", 0) - //set the state to using the eyesCli instead of MemKVStore - useEyesCli := func() { - state = NewState(eyesCli, log.TestingLogger()) - cache = state.CacheWrap() - } +// //reset the store/state/cache +// reset := func() { +// store = NewMemKVStore() +// state = NewState(store, log.TestingLogger()) +// cache = state.CacheWrap() +// } - //key value pairs to be tested within the system - keyvalue := []struct { - key string - value string - }{ - {"foo", "snake"}, - {"bar", "mouse"}, - } +// //set the state to using the eyesCli instead of MemKVStore +// useEyesCli := func() { +// state = NewState(eyesCli, log.TestingLogger()) +// cache = state.CacheWrap() +// } - //set the kvc to have all the key value pairs - setRecords := func(kv KVStore) { - for _, n := range keyvalue { - kv.Set([]byte(n.key), []byte(n.value)) - } - } +// //key value pairs to be tested within the system +// keyvalue := []struct { +// key string +// value string +// }{ +// {"foo", "snake"}, +// {"bar", "mouse"}, +// } - //store has all the key value pairs - storeHasAll := func(kv KVStore) bool { - for _, n := range keyvalue { - if !bytes.Equal(kv.Get([]byte(n.key)), []byte(n.value)) { - return false - } - } - return true - } +// //set the kvc to have all the key value pairs +// setRecords := func(kv KVStore) { +// for _, n := range keyvalue { +// kv.Set([]byte(n.key), []byte(n.value)) +// } +// } - //test chainID - state.SetChainID("testchain") - assert.Equal(state.GetChainID(), "testchain", "ChainID is improperly stored") +// //store has all the key value pairs +// storeHasAll := func(kv KVStore) bool { +// for _, n := range keyvalue { +// if !bytes.Equal(kv.Get([]byte(n.key)), []byte(n.value)) { +// return false +// } +// } +// return true +// } - //test basic retrieve - setRecords(state) - assert.True(storeHasAll(state), "state doesn't retrieve after Set") +// //test chainID +// state.SetChainID("testchain") +// assert.Equal(state.GetChainID(), "testchain", "ChainID is improperly stored") - //Test CacheWrap with local mem store - reset() - setRecords(cache) - assert.False(storeHasAll(store), "store retrieving before CacheSync") - cache.CacheSync() - assert.True(storeHasAll(store), "store doesn't retrieve after CacheSync") +// //test basic retrieve +// setRecords(state) +// assert.True(storeHasAll(state), "state doesn't retrieve after Set") - //Test Commit on state with non-merkle store - assert.True(state.Commit().IsErr(), "Commit shouldn't work with non-merkle store") +// //Test CacheWrap with local mem store +// reset() +// setRecords(cache) +// assert.False(storeHasAll(store), "store retrieving before CacheSync") +// cache.CacheSync() +// assert.True(storeHasAll(store), "store doesn't retrieve after CacheSync") - //Test CacheWrap with merkleeyes client store - useEyesCli() - setRecords(cache) - assert.False(storeHasAll(eyesCli), "eyesCli retrieving before Commit") - cache.CacheSync() - assert.True(state.Commit().IsOK(), "Bad Commit") - assert.True(storeHasAll(eyesCli), "eyesCli doesn't retrieve after Commit") -} +// //Test Commit on state with non-merkle store +// assert.True(state.Commit().IsErr(), "Commit shouldn't work with non-merkle store") + +// //Test CacheWrap with merkleeyes client store +// useEyesCli() +// setRecords(cache) +// assert.False(storeHasAll(eyesCli), "eyesCli retrieving before Commit") +// cache.CacheSync() +// assert.True(state.Commit().IsOK(), "Bad Commit") +// assert.True(storeHasAll(eyesCli), "eyesCli doesn't retrieve after Commit") +// }