From b96e4784edbdfc4a70f67a7c34ee5b57a3a4f953 Mon Sep 17 00:00:00 2001 From: Jae Kwon Date: Fri, 1 Dec 2017 09:10:17 -0800 Subject: [PATCH] Draft of new BaseApp --- app/base.go | 278 ++++++++++++++++++++++++++++++++++++++++++----- app/store.go | 264 -------------------------------------------- errors/abci.go | 59 ++++++++++ errors/errors.go | 10 +- types/tx_msg.go | 2 + 5 files changed, 315 insertions(+), 298 deletions(-) delete mode 100644 app/store.go create mode 100644 errors/abci.go diff --git a/app/base.go b/app/base.go index f3f7c391e..e589478ee 100644 --- a/app/base.go +++ b/app/base.go @@ -1,79 +1,299 @@ package app import ( + "bytes" + "fmt" + "os" + + "github.com/golang/protobuf/proto" + "github.com/pkg/errors" abci "github.com/tendermint/abci/types" + cmn "github.com/tendermint/tmlibs/common" + "github.com/tendermint/tmlibs/log" sdk "github.com/cosmos/cosmos-sdk" - "github.com/cosmos/cosmos-sdk/errors" "github.com/cosmos/cosmos-sdk/util" ) +const mainKeyHeader = "header" + // BaseApp - The ABCI application type BaseApp struct { - *StoreApp + logger log.Logger + + // App name from abci.Info + name string + + // DeliverTx (main) state + ms MultiStore + + // CheckTx state + msCheck CacheMultiStore + + // Cached validator changes from DeliverTx + pending []*abci.Validator + + // Parser for the tx. + txParser sdk.TxParser + + // Handler for CheckTx and DeliverTx. handler sdk.Handler - clock sdk.Ticker } var _ abci.Application = &BaseApp{} -// NewBaseApp extends a StoreApp with a handler and a ticker, -// which it binds to the proper abci calls -func NewBaseApp(store *StoreApp, handler sdk.Handler, clock sdk.Ticker) *BaseApp { +// CONTRACT: There exists a "main" KVStore. +func NewBaseApp(name string, ms MultiStore) (*BaseApp, error) { + + if ms.GetKVStore("main") == nil { + return nil, errors.New("BaseApp expects MultiStore with 'main' KVStore") + } + + logger := makeDefaultLogger() + lastCommitID := ms.LastCommitID() + curVersion := ms.CurrentVersion() + main := ms.GetKVStore("main") + header := (*abci.Header)(nil) + msCheck := ms.CacheMultiStore() + + // SANITY + if curVersion != lastCommitID.Version+1 { + panic("CurrentVersion != LastCommitID.Version+1") + } + + // If we've committed before, we expect ms.GetKVStore("main").Get("header") + if !lastCommitID.IsZero() { + headerBytes, ok := main.Get(mainKeyHeader) + if !ok { + return nil, errors.New(fmt.Sprintf("Version > 0 but missing key %s", mainKeyHeader)) + } + err = proto.Unmarshal(headerBytes, header) + if err != nil { + return nil, errors.Wrap(err, "Failed to parse Header") + } + + // SANITY: Validate Header + if header.Height != curVersion-1 { + errStr := fmt.Sprintf("Expected header.Height %v but got %v", version, headerHeight) + panic(errStr) + } + } + return &BaseApp{ - StoreApp: store, - handler: handler, - clock: clock, + logger: logger, + name: name, + ms: ms, + msCheck: msCheck, + pending: nil, + header: header, } } +func (app *BaseApp) SetTxParser(parser TxParser) { + app.txParser = parser +} + +func (app *BaseApp) SetHandler(handler sdk.Handler) { + app.handler = handler +} + +//---------------------------------------- + // DeliverTx - ABCI - dispatches to the handler -func (app *BaseApp) DeliverTx(txBytes []byte) abci.Result { +func (app *BaseApp) DeliverTx(txBytes []byte) abci.ResponseDeliverTx { // TODO: use real context on refactor ctx := util.MockContext( app.GetChainID(), app.WorkingHeight(), ) - // Note: first decorator must parse bytes - res, err := app.handler.DeliverTx(ctx, app.Append(), txBytes) + // Parse the transaction + tx, err := app.parseTxFn(ctx, txBytes) if err != nil { - return errors.Result(err) + err := sdk.TxParseError("").WithCause(err) + return sdk.ResponseDeliverTxFromErr(err) } + + // Make handler deal with it + data, err := app.handler.DeliverTx(ctx, app.ms, tx) + if err != nil { + return sdk.ResponseDeliverTxFromErr(err) + } + app.AddValChange(res.Diff) - return sdk.ToABCI(res) + + return abci.ResponseDeliverTx{ + Code: abci.CodeType_OK, + Data: data, + Log: "", // TODO add log from ctx.logger + } } // CheckTx - ABCI - dispatches to the handler -func (app *BaseApp) CheckTx(txBytes []byte) abci.Result { +func (app *BaseApp) CheckTx(txBytes []byte) abci.ResponseCheckTx { + // TODO: use real context on refactor ctx := util.MockContext( app.GetChainID(), app.WorkingHeight(), ) - // Note: first decorator must parse bytes - res, err := app.handler.CheckTx(ctx, app.Check(), txBytes) + // Parse the transaction + tx, err := app.parseTxFn(ctx, txBytes) if err != nil { - return errors.Result(err) + err := sdk.TxParseError("").WithCause(err) + return sdk.ResponseCheckTxFromErr(err) + } + + // Make handler deal with it + data, err := app.handler.CheckTx(ctx, app.ms, tx) + if err != nil { + return sdk.ResponseCheckTx(err) + } + + return abci.ResponseCheckTx{ + Code: abci.CodeType_OK, + Data: data, + Log: "", // TODO add log from ctx.logger } - return sdk.ToABCI(res) } -// BeginBlock - ABCI - triggers Tick actions -func (app *BaseApp) BeginBlock(req abci.RequestBeginBlock) { - // execute tick if present - if app.clock != nil { - ctx := util.MockContext( - app.GetChainID(), - app.WorkingHeight(), - ) +// Info - ABCI +func (app *BaseApp) Info(req abci.RequestInfo) abci.ResponseInfo { - diff, err := app.clock.Tick(ctx, app.Append()) + lastCommitID := app.ms.LastCommitID() + + return abci.ResponseInfo{ + Data: app.Name, + LastBlockHeight: lastCommitID.Version, + LastBlockAppHash: lastCommitID.Hash, + } +} + +// SetOption - ABCI +func (app *StoreApp) SetOption(key string, value string) string { + return "Not Implemented" +} + +// Query - ABCI +func (app *StoreApp) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) { + /* TODO + + if len(reqQuery.Data) == 0 { + resQuery.Log = "Query cannot be zero length" + resQuery.Code = abci.CodeType_EncodingError + return + } + + // set the query response height to current + tree := app.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 + + withProof := app.CommittedHeight() - 1 + if tree.Tree.VersionExists(withProof) { + height = withProof + } else { + height = app.CommittedHeight() + } + } + 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.GetVersionedWithProof(key, height) + if err != nil { + resQuery.Log = err.Error() + break + } + resQuery.Value = value + resQuery.Proof = proof.Bytes() + } else { + value := tree.Get(key) + resQuery.Value = value + } + + default: + resQuery.Code = abci.CodeType_UnknownRequest + resQuery.Log = cmn.Fmt("Unexpected Query path: %v", reqQuery.Path) + } + return + */ +} + +// Commit implements abci.Application +func (app *StoreApp) Commit() (res abci.Result) { + /* + hash, err := app.state.Commit(app.height) if err != nil { + // die if we can't commit, not to recover panic(err) } - app.AddValChange(diff) + app.logger.Debug("Commit synced", + "height", app.height, + "hash", fmt.Sprintf("%X", hash), + ) + + if app.state.Size() == 0 { + return abci.NewResultOK(nil, "Empty hash for empty tree") + } + return abci.NewResultOK(hash, "") + */ +} + +// InitChain - ABCI +func (app *StoreApp) InitChain(req abci.RequestInitChain) {} + +// BeginBlock - ABCI +func (app *StoreApp) BeginBlock(req abci.RequestBeginBlock) { + // TODO +} + +// EndBlock - ABCI +// Returns a list of all validator changes made in this block +func (app *StoreApp) EndBlock(height uint64) (res abci.ResponseEndBlock) { + // TODO: cleanup in case a validator exists multiple times in the list + res.Diffs = app.pending + app.pending = nil + return +} + +// AddValChange is meant to be called by apps on DeliverTx +// results, this is added to the cache for the endblock +// changeset +func (app *StoreApp) AddValChange(diffs []*abci.Validator) { + for _, d := range diffs { + idx := pubKeyIndex(d, app.pending) + if idx >= 0 { + app.pending[idx] = d + } else { + app.pending = append(app.pending, d) + } } } + +// return index of list with validator of same PubKey, or -1 if no match +func pubKeyIndex(val *abci.Validator, list []*abci.Validator) int { + for i, v := range list { + if bytes.Equal(val.PubKey, v.PubKey) { + return i + } + } + return -1 +} + +// Make a simple default logger +// TODO: Make log capturable for each transaction, and return it in +// ResponseDeliverTx.Log and ResponseCheckTx.Log. +func makeDefaultLogger() log.Logger { + return log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "sdk/app") +} diff --git a/app/store.go b/app/store.go deleted file mode 100644 index 58048fca1..000000000 --- a/app/store.go +++ /dev/null @@ -1,264 +0,0 @@ -package app - -import ( - "bytes" - "fmt" - "path" - "path/filepath" - "strings" - - abci "github.com/tendermint/abci/types" - "github.com/tendermint/iavl" - cmn "github.com/tendermint/tmlibs/common" - dbm "github.com/tendermint/tmlibs/db" - "github.com/tendermint/tmlibs/log" - - sdk "github.com/cosmos/cosmos-sdk" - "github.com/cosmos/cosmos-sdk/errors" - sm "github.com/cosmos/cosmos-sdk/state" -) - -// DefaultHistorySize is how many blocks of history to store for ABCI queries -const DefaultHistorySize = 10 - -// StoreApp contains a data store and all info needed -// to perform queries and handshakes. -// -// It should be embeded in another struct for CheckTx, -// DeliverTx and initializing state from the genesis. -type StoreApp struct { - // Name is what is returned from info - Name string - - // this is the database state - info *sm.ChainState - state *sm.State - - // cached validator changes from DeliverTx - pending []*abci.Validator - - // height is last committed block, DeliverTx is the next one - height uint64 - - logger log.Logger -} - -// NewStoreApp creates a data store to handle queries -func NewStoreApp(appName, dbName string, cacheSize int, logger log.Logger) (*StoreApp, error) { - state, err := loadState(dbName, cacheSize, DefaultHistorySize) - if err != nil { - return nil, err - } - app := &StoreApp{ - Name: appName, - state: state, - height: state.LatestHeight(), - info: sm.NewChainState(), - logger: logger.With("module", "app"), - } - return app, nil -} - -// MockStoreApp returns a Store app with no persistence -func MockStoreApp(appName string, logger log.Logger) (*StoreApp, error) { - return NewStoreApp(appName, "", 0, logger) -} - -// GetChainID returns the currently stored chain -func (app *StoreApp) GetChainID() string { - return app.info.GetChainID(app.state.Committed()) -} - -// Logger returns the application base logger -func (app *StoreApp) Logger() log.Logger { - return app.logger -} - -// Hash gets the last hash stored in the database -func (app *StoreApp) Hash() []byte { - return app.state.LatestHash() -} - -// Append returns the working state for DeliverTx -func (app *StoreApp) Append() sdk.SimpleDB { - return app.state.Append() -} - -// Check returns the working state for CheckTx -func (app *StoreApp) Check() sdk.SimpleDB { - return app.state.Check() -} - -// CommittedHeight gets the last block height committed -// to the db -func (app *StoreApp) CommittedHeight() uint64 { - return app.height -} - -// WorkingHeight gets the current block we are writing -func (app *StoreApp) WorkingHeight() uint64 { - return app.height + 1 -} - -// Info implements abci.Application. It returns the height and hash, -// as well as the abci name and version. -// -// The height is the block that holds the transactions, not the apphash itself. -func (app *StoreApp) Info(req abci.RequestInfo) abci.ResponseInfo { - hash := app.Hash() - - app.logger.Info("Info synced", - "height", app.CommittedHeight(), - "hash", fmt.Sprintf("%X", hash)) - - return abci.ResponseInfo{ - Data: app.Name, - LastBlockHeight: app.CommittedHeight(), - LastBlockAppHash: hash, - } -} - -// SetOption - ABCI -func (app *StoreApp) SetOption(key string, value string) string { - return "Not Implemented" -} - -// Query - ABCI -func (app *StoreApp) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) { - if len(reqQuery.Data) == 0 { - resQuery.Log = "Query cannot be zero length" - resQuery.Code = abci.CodeType_EncodingError - return - } - - // set the query response height to current - tree := app.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 - - withProof := app.CommittedHeight() - 1 - if tree.Tree.VersionExists(withProof) { - height = withProof - } else { - height = app.CommittedHeight() - } - } - 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.GetVersionedWithProof(key, height) - if err != nil { - resQuery.Log = err.Error() - break - } - resQuery.Value = value - resQuery.Proof = proof.Bytes() - } else { - value := tree.Get(key) - resQuery.Value = value - } - - default: - resQuery.Code = abci.CodeType_UnknownRequest - resQuery.Log = cmn.Fmt("Unexpected Query path: %v", reqQuery.Path) - } - return -} - -// Commit implements abci.Application -func (app *StoreApp) Commit() (res abci.Result) { - app.height++ - - hash, err := app.state.Commit(app.height) - if err != nil { - // die if we can't commit, not to recover - panic(err) - } - app.logger.Debug("Commit synced", - "height", app.height, - "hash", fmt.Sprintf("%X", hash), - ) - - if app.state.Size() == 0 { - return abci.NewResultOK(nil, "Empty hash for empty tree") - } - return abci.NewResultOK(hash, "") -} - -// InitChain - ABCI -func (app *StoreApp) InitChain(req abci.RequestInitChain) {} - -// BeginBlock - ABCI -func (app *StoreApp) BeginBlock(req abci.RequestBeginBlock) {} - -// EndBlock - ABCI -// Returns a list of all validator changes made in this block -func (app *StoreApp) EndBlock(height uint64) (res abci.ResponseEndBlock) { - // TODO: cleanup in case a validator exists multiple times in the list - res.Diffs = app.pending - app.pending = nil - return -} - -// AddValChange is meant to be called by apps on DeliverTx -// results, this is added to the cache for the endblock -// changeset -func (app *StoreApp) AddValChange(diffs []*abci.Validator) { - for _, d := range diffs { - idx := pubKeyIndex(d, app.pending) - if idx >= 0 { - app.pending[idx] = d - } else { - app.pending = append(app.pending, d) - } - } -} - -// return index of list with validator of same PubKey, or -1 if no match -func pubKeyIndex(val *abci.Validator, list []*abci.Validator) int { - for i, v := range list { - if bytes.Equal(val.PubKey, v.PubKey) { - return i - } - } - return -1 -} - -func loadState(dbName string, cacheSize int, historySize uint64) (*sm.State, error) { - // memory backed case, just for testing - if dbName == "" { - tree := iavl.NewVersionedTree(0, dbm.NewMemDB()) - return sm.NewState(tree, historySize), nil - } - - // Expand the path fully - dbPath, err := filepath.Abs(dbName) - if err != nil { - return nil, errors.ErrInternal("Invalid Database Name") - } - - // 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) - - // Open database called "dir/name.db", if it doesn't exist it will be created - db := dbm.NewDB(name, dbm.LevelDBBackendStr, dir) - tree := iavl.NewVersionedTree(cacheSize, db) - if err = tree.Load(); err != nil { - return nil, errors.ErrInternal("Loading tree: " + err.Error()) - } - - return sm.NewState(tree, historySize), nil -} diff --git a/errors/abci.go b/errors/abci.go new file mode 100644 index 000000000..6c5cebf11 --- /dev/null +++ b/errors/abci.go @@ -0,0 +1,59 @@ +package errors + +import ( + abci "github.com/tendermint/abci/types" +) + +type causer interface { + Cause() error +} + +func getABCIError(err error) (ABCIError, bool) { + if err, ok := err.(ABCIError); ok { + return err, true + } + if causer, ok := err.(causer); ok { + err := causer.Cause() + if err, ok := err.(ABCIError); ok { + return err, true + } + } + return nil, false +} + +func ResponseDeliverTxFromErr(err error) *abci.ResponseDeliverTx { + var code = CodeInternalError + var log = codeToDefaultLog(code) + + abciErr, ok := getABCIError(err) + if ok { + code = abciErr.ABCICode() + log = abciErr.ABCILog() + } + + return &abci.ResponseDeliverTx{ + Code: code, + Data: nil, + Log: log, + Tags: nil, + } +} + +func ResponseCheckTxFromErr(err error) *abci.ResponseCheckTx { + var code = CodeInternalError + var log = codeToDefaultLog(code) + + abciErr, ok := getABCIError(err) + if ok { + code = abciErr.ABCICode() + log = abciErr.ABCILog() + } + + return &abci.ResponseCheckTx{ + Code: code, + Data: nil, + Log: log, + Gas: 0, // TODO + Fee: 0, // TODO + } +} diff --git a/errors/errors.go b/errors/errors.go index 9e6e649a4..d2b9f15b2 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -5,7 +5,7 @@ import "fmt" const ( // ABCI Response Codes CodeInternalError = 1 - CodeEncodingError = 2 + CodeTxParseError = 2 CodeBadNonce = 3 CodeUnauthorized = 4 CodeInsufficientFunds = 5 @@ -17,8 +17,8 @@ func codeToDefaultLog(code uint32) string { switch code { case CodeInternalError: return "Internal error" - case CodeEncodingError: - return "Encoding error" + case CodeTxParseError: + return "Tx parse error" case CodeBadNonce: return "Bad nonce" case CodeUnauthorized: @@ -40,8 +40,8 @@ func InternalError(log string) sdkError { return newSDKError(CodeInternalError, log) } -func EncodingError(log string) sdkError { - return newSDKError(CodeEncodingError, log) +func TxParseError(log string) sdkError { + return newSDKError(CodeTxParseError, log) } func BadNonce(log string) sdkError { diff --git a/types/tx_msg.go b/types/tx_msg.go index dce71bff4..a3233192c 100644 --- a/types/tx_msg.go +++ b/types/tx_msg.go @@ -34,3 +34,5 @@ type Tx interface { // .Empty(). Signatures() []Signature } + +type TxParser func(txBytes []byte) (Tx, error)