diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index f455b31843..cf86e3f7ac 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -20,25 +20,33 @@ var mainHeaderKey = []byte("header") // The ABCI application type BaseApp struct { - logger log.Logger - name string // application name from abci.Info - db dbm.DB // common DB backend - cms sdk.CommitMultiStore // Main (uncached) state - txDecoder sdk.TxDecoder // unmarshal []byte into sdk.Tx - initChainer sdk.InitChainer // - anteHandler sdk.AnteHandler // ante handler for fee and auth - router Router // handle any kind of message + // initialized on creation + logger log.Logger + name string // application name from abci.Info + db dbm.DB // common DB backend + cms sdk.CommitMultiStore // Main (uncached) state + router Router // handle any kind of message + + // must be set + txDecoder sdk.TxDecoder // unmarshal []byte into sdk.Tx + anteHandler sdk.AnteHandler // ante handler for fee and auth + + // may be nil + initChainer sdk.InitChainer // initialize state with validators and state blob + beginBlocker sdk.BeginBlocker // logic to run before any txs + endBlocker sdk.EndBlocker // logic to run after all txs, and to determine valset changes //-------------------- // Volatile - // .msCheck and .header are set on initialization. - // .msDeliver is only set (and reset) in BeginBlock. - // .header and .valUpdates are also reset in BeginBlock. - // .msCheck is only reset in Commit. + // .msCheck and .ctxCheck are set on initialization and reset on Commit. + // .msDeliver and .ctxDeliver are (re-)set on BeginBlock. + // .valUpdates accumulate in DeliverTx and reset in BeginBlock. + // QUESTION: should we put valUpdates in the ctxDeliver? - header abci.Header // current block header msCheck sdk.CacheMultiStore // CheckTx state, a cache-wrap of `.cms` msDeliver sdk.CacheMultiStore // DeliverTx state, a cache-wrap of `.cms` + ctxCheck sdk.Context // CheckTx context + ctxDeliver sdk.Context // DeliverTx context valUpdates []abci.Validator // cached validator changes from DeliverTx } @@ -79,6 +87,12 @@ func (app *BaseApp) SetTxDecoder(txDecoder sdk.TxDecoder) { func (app *BaseApp) SetInitChainer(initChainer sdk.InitChainer) { app.initChainer = initChainer } +func (app *BaseApp) SetBeginBlocker(beginBlocker sdk.BeginBlocker) { + app.beginBlocker = beginBlocker +} +func (app *BaseApp) SetEndBlocker(endBlocker sdk.EndBlocker) { + app.endBlocker = endBlocker +} func (app *BaseApp) SetAnteHandler(ah sdk.AnteHandler) { // deducts fee from payer, verifies signatures and nonces, sets Signers to ctx. app.anteHandler = ah @@ -87,11 +101,6 @@ func (app *BaseApp) SetAnteHandler(ah sdk.AnteHandler) { // nolint - Get functions func (app *BaseApp) Router() Router { return app.router } -/* TODO consider: -func (app *BaseApp) SetBeginBlocker(...) {} -func (app *BaseApp) SetEndBlocker(...) {} -*/ - // load latest application version func (app *BaseApp) LoadLatestVersion(mainKey sdk.StoreKey) error { app.cms.LoadLatestVersion() @@ -143,23 +152,19 @@ func (app *BaseApp) initFromStore(mainKey sdk.StoreKey) error { } } - // set BaseApp state - app.header = header + // initialize Check state app.msCheck = app.cms.CacheMultiStore() - app.msDeliver = nil - app.valUpdates = nil + app.ctxCheck = app.NewContext(true, abci.Header{}) return nil } -// NewContext returns a new Context suitable for AnteHandler and Handler processing. -// NOTE: header is empty for checkTx -// NOTE: txBytes may be nil, for instance in tests (using app.Check or app.Deliver directly). -func (app *BaseApp) NewContext(isCheckTx bool, txBytes []byte) sdk.Context { - store := app.getMultiStore(isCheckTx) - // XXX CheckTx can't safely get the header - header := abci.Header{} - return sdk.NewContext(store, header, isCheckTx, txBytes) +// NewContext returns a new Context with the correct store, the given header, and nil txBytes. +func (app *BaseApp) NewContext(isCheckTx bool, header abci.Header) sdk.Context { + if isCheckTx { + return sdk.NewContext(app.msCheck, header, true, nil) + } + return sdk.NewContext(app.msDeliver, header, false, nil) } //---------------------------------------- @@ -195,11 +200,8 @@ func (app *BaseApp) InitChain(req abci.RequestInitChain) (res abci.ResponseInitC // NOTE: we're writing to the cms directly, without a CacheWrap ctx := sdk.NewContext(app.cms, abci.Header{}, false, nil) - err := app.initChainer(ctx, req) - if err != nil { - // TODO: something better https://github.com/cosmos/cosmos-sdk/issues/468 - cmn.Exit(fmt.Sprintf("error initializing application genesis state: %v", err)) - } + res = app.initChainer(ctx, req) + // TODO: handle error https://github.com/cosmos/cosmos-sdk/issues/468 // XXX this commits everything and bumps the version. // https://github.com/cosmos/cosmos-sdk/issues/442#issuecomment-366470148 @@ -221,10 +223,12 @@ func (app *BaseApp) Query(req abci.RequestQuery) (res abci.ResponseQuery) { // Implements ABCI func (app *BaseApp) BeginBlock(req abci.RequestBeginBlock) (res abci.ResponseBeginBlock) { - // NOTE: For consistency we should unset these upon EndBlock. - app.header = req.Header app.msDeliver = app.cms.CacheMultiStore() + app.ctxDeliver = app.NewContext(false, req.Header) app.valUpdates = nil + if app.beginBlocker != nil { + res = app.beginBlocker(app.ctxDeliver, req) + } return } @@ -317,8 +321,13 @@ func (app *BaseApp) runTx(isCheckTx bool, txBytes []byte, tx sdk.Tx) (result sdk return err.Result() } - // Construct a Context. - var ctx = app.NewContext(isCheckTx, txBytes) + // Get the context + var ctx sdk.Context + if isCheckTx { + ctx = app.ctxCheck.WithTxBytes(txBytes) + } else { + ctx = app.ctxDeliver.WithTxBytes(txBytes) + } // TODO: override default ante handler w/ custom ante handler. @@ -332,7 +341,7 @@ func (app *BaseApp) runTx(isCheckTx bool, txBytes []byte, tx sdk.Tx) (result sdk } // CacheWrap app.msDeliver in case it fails. - msCache := app.getMultiStore(false).CacheMultiStore() + msCache := app.msDeliver.CacheMultiStore() ctx = ctx.WithMultiStore(msCache) // Match and run route. @@ -350,30 +359,31 @@ func (app *BaseApp) runTx(isCheckTx bool, txBytes []byte, tx sdk.Tx) (result sdk // Implements ABCI func (app *BaseApp) EndBlock(req abci.RequestEndBlock) (res abci.ResponseEndBlock) { - res.ValidatorUpdates = app.valUpdates - app.valUpdates = nil - app.msDeliver = nil + if app.endBlocker != nil { + res = app.endBlocker(app.ctxDeliver, req) + } else { + res.ValidatorUpdates = app.valUpdates + } return } // Implements ABCI func (app *BaseApp) Commit() (res abci.ResponseCommit) { + // Write the Deliver state and commit the MultiStore app.msDeliver.Write() commitID := app.cms.Commit() app.logger.Debug("Commit synced", "commit", commitID, ) + + // Reset the Check state + // NOTE: safe because Tendermint holds a lock on the mempool for Commit. + // Use the header from this latest block. + header := app.ctxDeliver.BlockHeader() + app.msCheck = app.cms.CacheMultiStore() + app.ctxCheck = app.NewContext(true, header) + return abci.ResponseCommit{ Data: commitID.Hash, } } - -//---------------------------------------- -// Helpers - -func (app *BaseApp) getMultiStore(isCheckTx bool) sdk.MultiStore { - if isCheckTx { - return app.msCheck - } - return app.msDeliver -} diff --git a/baseapp/baseapp_test.go b/baseapp/baseapp_test.go index fb136904cc..8eac7ad102 100644 --- a/baseapp/baseapp_test.go +++ b/baseapp/baseapp_test.go @@ -51,6 +51,19 @@ func TestMountStores(t *testing.T) { func TestLoadVersion(t *testing.T) { // TODO + // Test that we can make commits and then reload old versions. + // Test that LoadLatestVersion actually does. +} + +func TestTxDecoder(t *testing.T) { + // TODO + // Test that txs can be unmarshalled and read and that + // correct error codes are returned when not +} + +func TestInfo(t *testing.T) { + // TODO + // Test that Info returns the latest committed state. } func TestInitChainer(t *testing.T) { @@ -65,10 +78,10 @@ func TestInitChainer(t *testing.T) { key, value := []byte("hello"), []byte("goodbye") // initChainer sets a value in the store - var initChainer sdk.InitChainer = func(ctx sdk.Context, req abci.RequestInitChain) sdk.Error { + var initChainer sdk.InitChainer = func(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { store := ctx.KVStore(capKey) store.Set(key, value) - return nil + return abci.ResponseInitChain{} } query := abci.RequestQuery{ @@ -88,7 +101,114 @@ func TestInitChainer(t *testing.T) { assert.Equal(t, value, res.Value) } +// Test that successive CheckTx can see eachothers effects +// on the store within a block, and that the CheckTx state +// gets reset to the latest Committed state during Commit +func TestCheckTx(t *testing.T) { + // TODO +} + +// Test that successive DeliverTx can see eachothers effects +// on the store, both within and across blocks. +func TestDeliverTx(t *testing.T) { + app := newBaseApp(t.Name()) + + // make a cap key and mount the store + capKey := sdk.NewKVStoreKey("main") + app.MountStoresIAVL(capKey) + err := app.LoadLatestVersion(capKey) // needed to make stores non-nil + assert.Nil(t, err) + + counter := 0 + txPerHeight := 2 + app.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx) (newCtx sdk.Context, res sdk.Result, abort bool) { return }) + app.Router().AddRoute(msgType, func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + store := ctx.KVStore(capKey) + if counter > 0 { + // check previous value in store + counterBytes := []byte{byte(counter - 1)} + prevBytes := store.Get(counterBytes) + assert.Equal(t, prevBytes, counterBytes) + } + + // set the current counter in the store + counterBytes := []byte{byte(counter)} + store.Set(counterBytes, counterBytes) + + // check we can see the current header + thisHeader := ctx.BlockHeader() + height := int64((counter / txPerHeight) + 1) + assert.Equal(t, height, thisHeader.Height) + + counter += 1 + return sdk.Result{} + }) + + tx := testUpdatePowerTx{} // doesn't matter + header := abci.Header{AppHash: []byte("apphash")} + + nBlocks := 3 + for blockN := 0; blockN < nBlocks; blockN++ { + // block1 + header.Height = int64(blockN + 1) + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + for i := 0; i < txPerHeight; i++ { + app.Deliver(tx) + } + app.EndBlock(abci.RequestEndBlock{}) + app.Commit() + } +} + +// Test that we can only query from the latest committed state. +func TestQuery(t *testing.T) { + app := newBaseApp(t.Name()) + + // make a cap key and mount the store + capKey := sdk.NewKVStoreKey("main") + app.MountStoresIAVL(capKey) + err := app.LoadLatestVersion(capKey) // needed to make stores non-nil + assert.Nil(t, err) + + key, value := []byte("hello"), []byte("goodbye") + + app.SetAnteHandler(func(ctx sdk.Context, tx sdk.Tx) (newCtx sdk.Context, res sdk.Result, abort bool) { return }) + app.Router().AddRoute(msgType, func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + store := ctx.KVStore(capKey) + store.Set(key, value) + return sdk.Result{} + }) + + query := abci.RequestQuery{ + Path: "/main/key", + Data: key, + } + + // query is empty before we do anything + res := app.Query(query) + assert.Equal(t, 0, len(res.Value)) + + tx := testUpdatePowerTx{} // doesn't matter + + // query is still empty after a CheckTx + app.Check(tx) + res = app.Query(query) + assert.Equal(t, 0, len(res.Value)) + + // query is still empty after a DeliverTx before we commit + app.BeginBlock(abci.RequestBeginBlock{}) + app.Deliver(tx) + res = app.Query(query) + assert.Equal(t, 0, len(res.Value)) + + // query returns correct value after Commit + app.Commit() + res = app.Query(query) + assert.Equal(t, value, res.Value) +} + //---------------------- +// TODO: clean this up // A mock transaction to update a validator's voting power. type testUpdatePowerTx struct { @@ -107,7 +227,7 @@ func (tx testUpdatePowerTx) GetSigners() []crypto.Address { return ni func (tx testUpdatePowerTx) GetFeePayer() crypto.Address { return nil } func (tx testUpdatePowerTx) GetSignatures() []sdk.StdSignature { return nil } -func TestExecution(t *testing.T) { +func TestValidatorChange(t *testing.T) { // Create app. app := newBaseApp(t.Name()) diff --git a/examples/basecoin/app/app.go b/examples/basecoin/app/app.go index b1f66009e4..58c25dda80 100644 --- a/examples/basecoin/app/app.go +++ b/examples/basecoin/app/app.go @@ -57,8 +57,8 @@ func NewBasecoinApp(logger log.Logger, db dbm.DB) *BasecoinApp { app.Router().AddRoute("sketchy", sketchy.NewHandler()) // initialize BaseApp - app.SetTxDecoder() - app.SetInitChainer() + app.SetTxDecoder(app.txDecoder) + app.SetInitChainer(app.initChainer) app.MountStoresIAVL(app.capKeyMainStore, app.capKeyIBCStore) app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper)) err := app.LoadLatestVersion(app.capKeyMainStore) @@ -78,37 +78,35 @@ func MakeTxCodec() *wire.Codec { } // custom logic for transaction decoding -func (app *BasecoinApp) SetTxDecoder() { - app.BaseApp.SetTxDecoder(func(txBytes []byte) (sdk.Tx, sdk.Error) { - var tx = sdk.StdTx{} - // StdTx.Msg is an interface whose concrete - // types are registered in app/msgs.go. - err := app.cdc.UnmarshalBinary(txBytes, &tx) - if err != nil { - return nil, sdk.ErrTxParse("").TraceCause(err, "") - } - return tx, nil - }) +func (app *BasecoinApp) txDecoder(txBytes []byte) (sdk.Tx, sdk.Error) { + var tx = sdk.StdTx{} + // StdTx.Msg is an interface. The concrete types + // are registered by MakeTxCodec in bank.RegisterWire. + err := app.cdc.UnmarshalBinary(txBytes, &tx) + if err != nil { + return nil, sdk.ErrTxParse("").TraceCause(err, "") + } + return tx, nil } // custom logic for basecoin initialization -func (app *BasecoinApp) SetInitChainer() { - app.BaseApp.SetInitChainer(func(ctx sdk.Context, req abci.RequestInitChain) sdk.Error { - stateJSON := req.AppStateBytes +func (app *BasecoinApp) initChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { + stateJSON := req.AppStateBytes - genesisState := new(types.GenesisState) - err := json.Unmarshal(stateJSON, genesisState) + genesisState := new(types.GenesisState) + err := json.Unmarshal(stateJSON, genesisState) + if err != nil { + panic(err) // TODO https://github.com/cosmos/cosmos-sdk/issues/468 + // return sdk.ErrGenesisParse("").TraceCause(err, "") + } + + for _, gacc := range genesisState.Accounts { + acc, err := gacc.ToAppAccount() if err != nil { - return sdk.ErrGenesisParse("").TraceCause(err, "") + panic(err) // TODO https://github.com/cosmos/cosmos-sdk/issues/468 + // return sdk.ErrGenesisParse("").TraceCause(err, "") } - - for _, gacc := range genesisState.Accounts { - acc, err := gacc.ToAppAccount() - if err != nil { - return sdk.ErrGenesisParse("").TraceCause(err, "") - } - app.accountMapper.SetAccount(ctx, acc) - } - return nil - }) + app.accountMapper.SetAccount(ctx, acc) + } + return abci.ResponseInitChain{} } diff --git a/examples/basecoin/app/app_test.go b/examples/basecoin/app/app_test.go index 5ef1195c8f..9bcd1e7748 100644 --- a/examples/basecoin/app/app_test.go +++ b/examples/basecoin/app/app_test.go @@ -87,7 +87,7 @@ func TestGenesis(t *testing.T) { bapp.InitChain(abci.RequestInitChain{vals, stateBytes}) // a checkTx context - ctx := bapp.BaseApp.NewContext(true, nil) + ctx := bapp.BaseApp.NewContext(true, abci.Header{}) res1 := bapp.accountMapper.GetAccount(ctx, baseAcc.Address) assert.Equal(t, acc, res1) diff --git a/types/abci.go b/types/abci.go index 58f40dd673..40651163c4 100644 --- a/types/abci.go +++ b/types/abci.go @@ -3,4 +3,10 @@ package types import abci "github.com/tendermint/abci/types" // initialize application state at genesis -type InitChainer func(ctx Context, req abci.RequestInitChain) Error +type InitChainer func(ctx Context, req abci.RequestInitChain) abci.ResponseInitChain + +// run code before the transactions in a block +type BeginBlocker func(ctx Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock + +// run code after the transactions in a block and return updates to the validator set +type EndBlocker func(ctx Context, req abci.RequestEndBlock) abci.ResponseEndBlock