Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

baseapp: ctxCheck and ctxDeliver, begin/endBlocker #476

Merged
merged 4 commits into from
Feb 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 63 additions & 53 deletions baseapp/baseapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we will not use it for the hub, everything is updated through endblock... I'd vote to remove, but maybe there are other use cases to leave it in (but then again, maybe we should just enforce val updates only in endblock)


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
}

Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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)
}

//----------------------------------------
Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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.

Expand All @@ -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.
Expand All @@ -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,
)
return abci.ResponseCommit{
Data: commitID.Hash,
}
}

//----------------------------------------
// Helpers
// 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)

func (app *BaseApp) getMultiStore(isCheckTx bool) sdk.MultiStore {
if isCheckTx {
return app.msCheck
return abci.ResponseCommit{
Data: commitID.Hash,
}
return app.msDeliver
}
126 changes: 123 additions & 3 deletions baseapp/baseapp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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{
Expand All @@ -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 {
Expand All @@ -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())
Expand Down
Loading