Cache-wrap context during ante handler exec (#2781)

* Use cache-wrapped multi-store in ante
* Implement TestBaseAppAnteHandler
* Add reference documentation for BaseApp/CheckTx/DeliverTx
This commit is contained in:
Alexander Bezobchuk
2018-11-16 10:33:47 -08:00
committed by Jae Kwon
parent 8d6b0929fb
commit 15b6fa0959
5 changed files with 214 additions and 43 deletions
+47 -22
View File
@@ -10,7 +10,6 @@ import (
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto/tmhash"
cmn "github.com/tendermint/tendermint/libs/common"
dbm "github.com/tendermint/tendermint/libs/db"
"github.com/tendermint/tendermint/libs/log"
@@ -505,11 +504,11 @@ func validateBasicTxMsgs(msgs []sdk.Msg) sdk.Error {
// retrieve the context for the ante handler and store the tx bytes; store
// the vote infos if the tx runs within the deliverTx() state.
func (app *BaseApp) getContextForAnte(mode runTxMode, txBytes []byte) (ctx sdk.Context) {
// Get the context
ctx = getState(app, mode).ctx.WithTxBytes(txBytes)
ctx = app.getState(mode).ctx.WithTxBytes(txBytes)
if mode == runTxModeDeliver {
ctx = ctx.WithVoteInfos(app.voteInfos)
}
return
}
@@ -571,7 +570,7 @@ func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, mode runTxMode) (re
// Returns the applicantion's deliverState if app is in runTxModeDeliver,
// otherwise it returns the application's checkstate.
func getState(app *BaseApp, mode runTxMode) *state {
func (app *BaseApp) getState(mode runTxMode) *state {
if mode == runTxModeCheck || mode == runTxModeSimulate {
return app.checkState
}
@@ -581,20 +580,42 @@ func getState(app *BaseApp, mode runTxMode) *state {
func (app *BaseApp) initializeContext(ctx sdk.Context, mode runTxMode) sdk.Context {
if mode == runTxModeSimulate {
ctx = ctx.WithMultiStore(getState(app, runTxModeSimulate).CacheMultiStore())
ctx = ctx.WithMultiStore(app.getState(runTxModeSimulate).CacheMultiStore())
}
return ctx
}
// cacheTxContext returns a new context based off of the provided context with a
// cache wrapped multi-store and the store itself to allow the caller to write
// changes from the cached multi-store.
func (app *BaseApp) cacheTxContext(
ctx sdk.Context, txBytes []byte, mode runTxMode,
) (sdk.Context, sdk.CacheMultiStore) {
msCache := app.getState(mode).CacheMultiStore()
if msCache.TracingEnabled() {
msCache = msCache.WithTracingContext(
sdk.TraceContext(
map[string]interface{}{
"txHash": fmt.Sprintf("%X", tmhash.Sum(txBytes)),
},
),
).(sdk.CacheMultiStore)
}
return ctx.WithMultiStore(msCache), msCache
}
// runTx processes a transaction. The transactions is proccessed via an
// anteHandler. txBytes may be nil in some cases, eg. in tests. Also, in the
// future we may support "internal" transactions.
// anteHandler. The provided txBytes may be nil in some cases, eg. in tests. For
// further details on transaction execution, reference the BaseApp SDK
// documentation.
func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk.Result) {
// NOTE: GasWanted should be returned by the AnteHandler. GasUsed is
// determined by the GasMeter. We need access to the context to get the gas
// meter so we initialize upfront.
var gasWanted int64
var msCache sdk.CacheMultiStore
ctx := app.getContextForAnte(mode, txBytes)
ctx = app.initializeContext(ctx, mode)
@@ -619,16 +640,27 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk
return err.Result()
}
// run the ante handler
// Execute the ante handler if one is defined.
if app.anteHandler != nil {
newCtx, result, abort := app.anteHandler(ctx, tx, (mode == runTxModeSimulate))
var anteCtx sdk.Context
var msCache sdk.CacheMultiStore
// Cache wrap context before anteHandler call in case it aborts.
// This is required for both CheckTx and DeliverTx.
// https://github.com/cosmos/cosmos-sdk/issues/2772
// NOTE: Alternatively, we could require that anteHandler ensures that
// writes do not happen if aborted/failed. This may have some
// performance benefits, but it'll be more difficult to get right.
anteCtx, msCache = app.cacheTxContext(ctx, txBytes, mode)
newCtx, result, abort := app.anteHandler(anteCtx, tx, (mode == runTxModeSimulate))
if abort {
return result
}
if !newCtx.IsZero() {
ctx = newCtx
}
msCache.Write()
gasWanted = result.GasWanted
}
@@ -638,17 +670,10 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk
return
}
// Keep the state in a transient CacheWrap in case processing the messages
// fails.
msCache = getState(app, mode).CacheMultiStore()
if msCache.TracingEnabled() {
msCache = msCache.WithTracingContext(sdk.TraceContext(
map[string]interface{}{"txHash": cmn.HexBytes(tmhash.Sum(txBytes)).String()},
)).(sdk.CacheMultiStore)
}
ctx = ctx.WithMultiStore(msCache)
result = app.runMsgs(ctx, msgs, mode)
// Create a new context based off of the existing context with a cache wrapped
// multi-store in case message processing fails.
runMsgCtx, msCache := app.cacheTxContext(ctx, txBytes, mode)
result = app.runMsgs(runMsgCtx, msgs, mode)
result.GasWanted = gasWanted
// only update state if all messages pass
+100 -9
View File
@@ -282,8 +282,19 @@ func TestInitChainer(t *testing.T) {
// Simple tx with a list of Msgs.
type txTest struct {
Msgs []sdk.Msg
Counter int64
Msgs []sdk.Msg
Counter int64
FailOnAnte bool
}
func (tx *txTest) setFailOnAnte(fail bool) {
tx.FailOnAnte = fail
}
func (tx *txTest) setFailOnHandler(fail bool) {
for i, msg := range tx.Msgs {
tx.Msgs[i] = msgCounter{msg.(msgCounter).Counter, fail}
}
}
// Implements Tx
@@ -297,7 +308,8 @@ const (
// ValidateBasic() fails on negative counters.
// Otherwise it's up to the handlers
type msgCounter struct {
Counter int64
Counter int64
FailOnHandler bool
}
// Implements Msg
@@ -315,9 +327,9 @@ func (msg msgCounter) ValidateBasic() sdk.Error {
func newTxCounter(txInt int64, msgInts ...int64) *txTest {
var msgs []sdk.Msg
for _, msgInt := range msgInts {
msgs = append(msgs, msgCounter{msgInt})
msgs = append(msgs, msgCounter{msgInt, false})
}
return &txTest{msgs, txInt}
return &txTest{msgs, txInt, false}
}
// a msg we dont know how to route
@@ -369,8 +381,13 @@ func testTxDecoder(cdc *codec.Codec) sdk.TxDecoder {
func anteHandlerTxTest(t *testing.T, capKey *sdk.KVStoreKey, storeKey []byte) sdk.AnteHandler {
return func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, res sdk.Result, abort bool) {
store := ctx.KVStore(capKey)
msgCounter := tx.(txTest).Counter
res = incrementingCounter(t, store, storeKey, msgCounter)
txTest := tx.(txTest)
if txTest.FailOnAnte {
return newCtx, sdk.ErrInternal("ante handler failure").Result(), true
}
res = incrementingCounter(t, store, storeKey, txTest.Counter)
return
}
}
@@ -381,10 +398,15 @@ func handlerMsgCounter(t *testing.T, capKey *sdk.KVStoreKey, deliverKey []byte)
var msgCount int64
switch m := msg.(type) {
case *msgCounter:
if m.FailOnHandler {
return sdk.ErrInternal("message handler failure").Result()
}
msgCount = m.Counter
case *msgCounter2:
msgCount = m.Counter
}
return incrementingCounter(t, store, deliverKey, msgCount)
}
}
@@ -712,12 +734,12 @@ func TestRunInvalidTransaction(t *testing.T) {
// Transaction with no known route
{
unknownRouteTx := txTest{[]sdk.Msg{msgNoRoute{}}, 0}
unknownRouteTx := txTest{[]sdk.Msg{msgNoRoute{}}, 0, false}
err := app.Deliver(unknownRouteTx)
require.EqualValues(t, sdk.CodeUnknownRequest, err.Code)
require.EqualValues(t, sdk.CodespaceRoot, err.Codespace)
unknownRouteTx = txTest{[]sdk.Msg{msgCounter{}, msgNoRoute{}}, 0}
unknownRouteTx = txTest{[]sdk.Msg{msgCounter{}, msgNoRoute{}}, 0, false}
err = app.Deliver(unknownRouteTx)
require.EqualValues(t, sdk.CodeUnknownRequest, err.Code)
require.EqualValues(t, sdk.CodespaceRoot, err.Codespace)
@@ -829,3 +851,72 @@ func TestTxGasLimits(t *testing.T) {
}
}
}
func TestBaseAppAnteHandler(t *testing.T) {
anteKey := []byte("ante-key")
anteOpt := func(bapp *BaseApp) {
bapp.SetAnteHandler(anteHandlerTxTest(t, capKey1, anteKey))
}
deliverKey := []byte("deliver-key")
routerOpt := func(bapp *BaseApp) {
bapp.Router().AddRoute(routeMsgCounter, handlerMsgCounter(t, capKey1, deliverKey))
}
cdc := codec.New()
app := setupBaseApp(t, anteOpt, routerOpt)
app.InitChain(abci.RequestInitChain{})
registerTestCodec(cdc)
app.BeginBlock(abci.RequestBeginBlock{})
// execute a tx that will fail ante handler execution
//
// NOTE: State should not be mutated here. This will be implicitly checked by
// the next txs ante handler execution (anteHandlerTxTest).
tx := newTxCounter(0, 0)
tx.setFailOnAnte(true)
txBytes, err := cdc.MarshalBinaryLengthPrefixed(tx)
require.NoError(t, err)
res := app.DeliverTx(txBytes)
require.False(t, res.IsOK(), fmt.Sprintf("%v", res))
ctx := app.getState(runTxModeDeliver).ctx
store := ctx.KVStore(capKey1)
require.Equal(t, int64(0), getIntFromStore(store, anteKey))
// execute at tx that will pass the ante handler (the checkTx state should
// mutate) but will fail the message handler
tx = newTxCounter(0, 0)
tx.setFailOnHandler(true)
txBytes, err = cdc.MarshalBinaryLengthPrefixed(tx)
require.NoError(t, err)
res = app.DeliverTx(txBytes)
require.False(t, res.IsOK(), fmt.Sprintf("%v", res))
ctx = app.getState(runTxModeDeliver).ctx
store = ctx.KVStore(capKey1)
require.Equal(t, int64(1), getIntFromStore(store, anteKey))
require.Equal(t, int64(0), getIntFromStore(store, deliverKey))
// execute a successful ante handler and message execution where state is
// implicitly checked by previous tx executions
tx = newTxCounter(1, 0)
txBytes, err = cdc.MarshalBinaryLengthPrefixed(tx)
require.NoError(t, err)
res = app.DeliverTx(txBytes)
require.True(t, res.IsOK(), fmt.Sprintf("%v", res))
ctx = app.getState(runTxModeDeliver).ctx
store = ctx.KVStore(capKey1)
require.Equal(t, int64(2), getIntFromStore(store, anteKey))
require.Equal(t, int64(1), getIntFromStore(store, deliverKey))
// commit
app.EndBlock(abci.RequestEndBlock{})
app.Commit()
}