cosmos-sdk/app.App -> cosmos-sdk/baseapp.BaseApp

This commit is contained in:
Jae Kwon
2018-01-20 20:13:46 -08:00
parent bd8bbf9d98
commit 633eaa87b3
15 changed files with 85 additions and 86 deletions
+317
View File
@@ -0,0 +1,317 @@
package baseapp
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/types"
)
var mainHeaderKey = []byte("header")
// BaseApp - The ABCI application
type BaseApp struct {
logger log.Logger
// Application name from abci.Info
name string
// Main (uncached) state
ms sdk.CommitMultiStore
// Unmarshal []byte into sdk.Tx
txDecoder sdk.TxDecoder
// Ante handler for fee and auth.
defaultAnteHandler sdk.AnteHandler
// Handle any kind of message.
router Router
//--------------------
// Volatile
// CheckTx state, a cache-wrap of `.ms`.
msCheck sdk.CacheMultiStore
// DeliverTx state, a cache-wrap of `.ms`.
msDeliver sdk.CacheMultiStore
// Current block header
header abci.Header
// Cached validator changes from DeliverTx.
valUpdates []abci.Validator
}
var _ abci.Application = &BaseApp{}
func NewBaseApp(name string, ms sdk.CommitMultiStore) *BaseApp {
return &BaseApp{
logger: makeDefaultLogger(),
name: name,
ms: ms,
router: NewRouter(),
}
}
func (app *BaseApp) Name() string {
return app.name
}
func (app *BaseApp) SetTxDecoder(txDecoder sdk.TxDecoder) {
app.txDecoder = txDecoder
}
func (app *BaseApp) SetDefaultAnteHandler(ah sdk.AnteHandler) {
app.defaultAnteHandler = ah
}
func (app *BaseApp) Router() Router {
return app.router
}
/* TODO consider:
func (app *BaseApp) SetBeginBlocker(...) {}
func (app *BaseApp) SetEndBlocker(...) {}
func (app *BaseApp) SetInitStater(...) {}
*/
func (app *BaseApp) LoadLatestVersion(mainKey sdk.SubstoreKey) error {
app.ms.LoadLatestVersion()
return app.initFromStore(mainKey)
}
func (app *BaseApp) LoadVersion(version int64, mainKey sdk.SubstoreKey) error {
app.ms.LoadVersion(version)
return app.initFromStore(mainKey)
}
// The last CommitID of the multistore.
func (app *BaseApp) LastCommitID() sdk.CommitID {
return app.ms.LastCommitID()
}
// The last commited block height.
func (app *BaseApp) LastBlockHeight() int64 {
return app.ms.LastCommitID().Version
}
// Initializes the remaining logic from app.ms.
func (app *BaseApp) initFromStore(mainKey sdk.SubstoreKey) error {
lastCommitID := app.ms.LastCommitID()
main := app.ms.GetKVStore(mainKey)
header := abci.Header{}
// Main store should exist.
if main == nil {
return errors.New("BaseApp expects MultiStore with 'main' KVStore")
}
// If we've committed before, we expect main://<mainHeaderKey>.
if !lastCommitID.IsZero() {
headerBytes := main.Get(mainHeaderKey)
if len(headerBytes) == 0 {
errStr := fmt.Sprintf("Version > 0 but missing key %s", mainHeaderKey)
return errors.New(errStr)
}
err := proto.Unmarshal(headerBytes, &header)
if err != nil {
return errors.Wrap(err, "Failed to parse Header")
}
lastVersion := lastCommitID.Version
if header.Height != lastVersion {
errStr := fmt.Sprintf("Expected main://%s.Height %v but got %v", mainHeaderKey, lastVersion, header.Height)
return errors.New(errStr)
}
}
// Set BaseApp state.
app.header = header
app.msCheck = nil
app.msDeliver = nil
app.valUpdates = nil
return nil
}
//----------------------------------------
// Implements ABCI
func (app *BaseApp) Info(req abci.RequestInfo) abci.ResponseInfo {
lastCommitID := app.ms.LastCommitID()
return abci.ResponseInfo{
Data: app.name,
LastBlockHeight: lastCommitID.Version,
LastBlockAppHash: lastCommitID.Hash,
}
}
// Implements ABCI
func (app *BaseApp) SetOption(req abci.RequestSetOption) (res abci.ResponseSetOption) {
// TODO: Implement
return
}
// Implements ABCI
func (app *BaseApp) InitChain(req abci.RequestInitChain) (res abci.ResponseInitChain) {
// TODO: Use req.Validators
return
}
// Implements ABCI
func (app *BaseApp) Query(req abci.RequestQuery) (res abci.ResponseQuery) {
// TODO: See app/query.go
return
}
// Implements ABCI
func (app *BaseApp) BeginBlock(req abci.RequestBeginBlock) (res abci.ResponseBeginBlock) {
app.header = req.Header
app.msDeliver = app.ms.CacheMultiStore()
app.msCheck = app.ms.CacheMultiStore()
return
}
// Implements ABCI
func (app *BaseApp) CheckTx(txBytes []byte) (res abci.ResponseCheckTx) {
result := app.runTx(true, txBytes)
return abci.ResponseCheckTx{
Code: result.Code,
Data: result.Data,
Log: result.Log,
GasWanted: result.GasWanted,
Fee: cmn.KI64Pair{
[]byte(result.FeeDenom),
result.FeeAmount,
},
Tags: result.Tags,
}
}
// Implements ABCI
func (app *BaseApp) DeliverTx(txBytes []byte) (res abci.ResponseDeliverTx) {
result := app.runTx(false, txBytes)
// After-handler hooks.
if result.Code == abci.CodeTypeOK {
app.valUpdates = append(app.valUpdates, result.ValidatorUpdates...)
} else {
// Even though the Code is not OK, there will be some side
// effects, like those caused by fee deductions or sequence
// incrementations.
}
// Tell the blockchain engine (i.e. Tendermint).
return abci.ResponseDeliverTx{
Code: result.Code,
Data: result.Data,
Log: result.Log,
GasWanted: result.GasWanted,
GasUsed: result.GasUsed,
Tags: result.Tags,
}
}
func (app *BaseApp) runTx(isCheckTx bool, txBytes []byte) (result sdk.Result) {
// Handle any panics.
defer func() {
if r := recover(); r != nil {
result = sdk.Result{
Code: 1, // TODO
Log: fmt.Sprintf("Recovered: %v\n", r),
}
}
}()
var store sdk.MultiStore
if isCheckTx {
store = app.msCheck
} else {
store = app.msDeliver
}
// Initialize arguments to Handler.
var ctx = sdk.NewContext(
store,
app.header,
isCheckTx,
txBytes,
)
// Decode the Tx.
tx, err := app.txDecoder(txBytes)
if err != nil {
return sdk.Result{
Code: 1, // TODO
}
}
// TODO: override default ante handler w/ custom ante handler.
// Run the ante handler.
ctx, result, abort := app.defaultAnteHandler(ctx, tx)
if isCheckTx || abort {
return result
}
// Match and run route.
msgType := tx.Type()
handler := app.router.Route(msgType)
result = handler(ctx, tx)
return result
}
// Implements ABCI
func (app *BaseApp) EndBlock(req abci.RequestEndBlock) (res abci.ResponseEndBlock) {
res.ValidatorUpdates = app.valUpdates
app.valUpdates = nil
return
}
// Implements ABCI
func (app *BaseApp) Commit() (res abci.ResponseCommit) {
app.msDeliver.Write()
commitID := app.ms.Commit()
app.logger.Debug("Commit synced",
"commit", commitID,
)
return abci.ResponseCommit{
Data: commitID.Hash,
}
}
//----------------------------------------
// Misc.
// 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")
}
+177
View File
@@ -0,0 +1,177 @@
package baseapp
import (
"bytes"
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
abci "github.com/tendermint/abci/types"
"github.com/tendermint/go-crypto"
cmn "github.com/tendermint/tmlibs/common"
dbm "github.com/tendermint/tmlibs/db"
"github.com/cosmos/cosmos-sdk/store"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// A mock transaction to update a validator's voting power.
type testTx struct {
Addr []byte
NewPower int64
}
const txType = "testTx"
func (tx testTx) Type() string { return txType }
func (tx testTx) Get(key interface{}) (value interface{}) { return nil }
func (tx testTx) GetSignBytes() []byte { return nil }
func (tx testTx) ValidateBasic() error { return nil }
func (tx testTx) GetSigners() []crypto.Address { return nil }
func (tx testTx) GetFeePayer() crypto.Address { return nil }
func (tx testTx) GetSignatures() []sdk.StdSignature { return nil }
func TestBasic(t *testing.T) {
store, storeKeys := newCommitMultiStore()
// Create app.
app := NewBaseApp(t.Name(), store)
app.SetTxDecoder(func(txBytes []byte) (sdk.Tx, error) {
var ttx testTx
fromJSON(txBytes, &ttx)
return ttx, nil
})
app.SetDefaultAnteHandler(func(ctx sdk.Context, tx sdk.Tx) (newCtx sdk.Context, res sdk.Result, abort bool) { return })
app.Router().AddRoute(txType, func(ctx sdk.Context, tx sdk.Tx) sdk.Result {
// TODO
return sdk.Result{}
})
// Load latest state, which should be empty.
err := app.LoadLatestVersion(storeKeys["main"])
assert.Nil(t, err)
assert.Equal(t, app.LastBlockHeight(), int64(0))
// Create the validators
var numVals = 3
var valSet = make([]abci.Validator, numVals)
for i := 0; i < numVals; i++ {
valSet[i] = makeVal(secret(i))
}
// Initialize the chain
app.InitChain(abci.RequestInitChain{
Validators: valSet,
})
// Simulate the start of a block.
app.BeginBlock(abci.RequestBeginBlock{})
// Add 1 to each validator's voting power.
for i, val := range valSet {
tx := testTx{
Addr: makePubKey(secret(i)).Address(),
NewPower: val.Power + 1,
}
txBytes := toJSON(tx)
res := app.DeliverTx(txBytes)
assert.True(t, res.IsOK(), "%#v", res)
}
// Simulate the end of a block.
// Get the summary of validator updates.
res := app.EndBlock(abci.RequestEndBlock{})
valUpdates := res.ValidatorUpdates
// Assert that validator updates are correct.
for _, val := range valSet {
// Sanity
assert.NotEqual(t, len(val.PubKey), 0)
// Find matching update and splice it out.
for j := 0; j < len(valUpdates); {
valUpdate := valUpdates[j]
// Matched.
if bytes.Equal(valUpdate.PubKey, val.PubKey) {
assert.Equal(t, valUpdate.Power, val.Power+1)
if j < len(valUpdates)-1 {
// Splice it out.
valUpdates = append(valUpdates[:j], valUpdates[j+1:]...)
}
break
}
// Not matched.
j += 1
}
}
assert.Equal(t, len(valUpdates), 0, "Some validator updates were unexpected")
}
//----------------------------------------
func randPower() int64 {
return cmn.RandInt64()
}
func makeVal(secret string) abci.Validator {
return abci.Validator{
PubKey: makePubKey(secret).Bytes(),
Power: randPower(),
}
}
func makePubKey(secret string) crypto.PubKey {
return makePrivKey(secret).PubKey()
}
func makePrivKey(secret string) crypto.PrivKey {
privKey := crypto.GenPrivKeyEd25519FromSecret([]byte(secret))
return privKey
}
func secret(index int) string {
return fmt.Sprintf("secret%d", index)
}
func copyVal(val abci.Validator) abci.Validator {
// val2 := *val
// return &val2
return val
}
func toJSON(o interface{}) []byte {
bz, err := json.Marshal(o)
if err != nil {
panic(err)
}
// fmt.Println(">> toJSON:", string(bz))
return bz
}
func fromJSON(bz []byte, ptr interface{}) {
// fmt.Println(">> fromJSON:", string(bz))
err := json.Unmarshal(bz, ptr)
if err != nil {
panic(err)
}
}
// Creates a sample CommitMultiStore
func newCommitMultiStore() (sdk.CommitMultiStore, map[string]sdk.SubstoreKey) {
dbMain := dbm.NewMemDB()
dbXtra := dbm.NewMemDB()
keyMain := sdk.NewKVStoreKey("main")
keyXtra := sdk.NewKVStoreKey("xtra")
ms := store.NewCommitMultiStore(dbMain) // Also store rootMultiStore metadata here (it shouldn't clash)
ms.SetSubstoreLoader(keyMain, store.NewIAVLStoreLoader(dbMain, 0, 0))
ms.SetSubstoreLoader(keyXtra, store.NewIAVLStoreLoader(dbXtra, 0, 0))
return ms, map[string]sdk.SubstoreKey{
"main": keyMain,
"xtra": keyXtra,
}
}
+10
View File
@@ -0,0 +1,10 @@
/*
Package baseapp contains data structures that provide basic data storage
functionality and act as a bridge between the ABCI interface and the SDK
abstractions.
BaseApp has no state except the CommitMultiStore you provide upon init.
See examples/basecoin/app/* for usage.
*/
package baseapp
+54
View File
@@ -0,0 +1,54 @@
package baseapp
/*
XXX Make this work with MultiStore.
XXX It will require some interfaces updates in store/types.go.
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
*/
+46
View File
@@ -0,0 +1,46 @@
package baseapp
import (
"regexp"
sdk "github.com/cosmos/cosmos-sdk/types"
)
type Router interface {
AddRoute(r string, h sdk.Handler)
Route(path string) (h sdk.Handler)
}
type route struct {
r string
h sdk.Handler
}
type router struct {
routes []route
}
func NewRouter() *router {
return &router{
routes: make([]route, 0),
}
}
var isAlpha = regexp.MustCompile(`^[a-zA-Z]+$`).MatchString
func (rtr *router) AddRoute(r string, h sdk.Handler) {
if !isAlpha(r) {
panic("route expressions can only contain alphanumeric characters")
}
rtr.routes = append(rtr.routes, route{r, h})
}
// TODO handle expressive matches.
func (rtr *router) Route(path string) (h sdk.Handler) {
for _, route := range rtr.routes {
if route.r == path {
return route.h
}
}
return nil
}
+22
View File
@@ -0,0 +1,22 @@
{
"chain_id": "foo_bar_chain",
"app_options": {
"accounts": [{
"pub_key": {
"type": "ed25519",
"data": "6880db93598e283a67c4d88fc67a8858aa2de70f713fe94a5109e29c137100c2"
},
"coins": [
{
"denom": "blank",
"amount": 12345
},
{
"denom": "ETH",
"amount": 654321
}
]
}],
"plugin_options": ["plugin1/key1", "value1", "plugin1/key2", "value2"]
}
}
+39
View File
@@ -0,0 +1,39 @@
{
"chain_id": "addr_accounts_chain",
"app_options": {
"accounts": [{
"name": "alice",
"pub_key": {
"type": "ed25519",
"data": "DBD9A46C45868F0A37C92B53113C09B048FBD87B5FBC2F8B199052973B8FAA36"
},
"coins": [
{
"denom": "one",
"amount": 111
}
]
}, {
"name": "bob",
"address": "C471FB670E44D219EE6DF2FC284BE38793ACBCE1",
"coins": [
{
"denom": "two",
"amount": 222
}
]
}, {
"name": "sam",
"pub_key": {
"type": "secp256k1",
"data": "02AA8342F63CCCCE6DDB128525BA048CE0B2993DA3B4308746E1F216361A87651E"
},
"coins": [
{
"denom": "four",
"amount": 444
}
]
}]
}
}
+52
View File
@@ -0,0 +1,52 @@
{
"chain_id": "addr_accounts_chain",
"app_options": {
"accounts": [{
"name": "alice",
"pub_key": {
"type": "ed25519",
"data": "DBD9A46C45868F0A37C92B53113C09B048FBD87B5FBC2F8B199052973B8FAA36"
},
"coins": [
{
"denom": "one",
"amount": 111
}
]
}, {
"name": "bob",
"address": "C471FB670E44D219EE6DF2FC284BE38793ACBCE1",
"coins": [
{
"denom": "two",
"amount": 222
}
]
}, {
"name": "carl",
"address": "1234ABCDD18E8EFE3FFC4B0506BF9BF8E5B0D9E9",
"pub_key": {
"type": "ed25519",
"data": "177C0AC45E86257F0708DC085D592AB22AAEECD1D26381B757F7C96135921858"
},
"coins": [
{
"denom": "three",
"amount": 333
}
]
}, {
"name": "sam",
"pub_key": {
"type": "secp256k1",
"data": "02AA8342F63CCCCE6DDB128525BA048CE0B2993DA3B4308746E1F216361A87651E"
},
"coins": [
{
"denom": "four",
"amount": 444
}
]
}]
}
}
+3
View File
@@ -0,0 +1,3 @@
{
"chain_id": "foo_bar_chain"
}