Merge pull request #476 from cosmos/begin-end-block

baseapp: ctxCheck and ctxDeliver, begin/endBlocker
This commit is contained in:
Ethan Buchman 2018-02-18 13:52:50 -05:00 committed by GitHub
commit c9dd62dd89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 221 additions and 87 deletions

View File

@ -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
}

View File

@ -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())

View File

@ -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{}
}

View File

@ -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)

View File

@ -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