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:
committed by
Jae Kwon
parent
8d6b0929fb
commit
15b6fa0959
+47
-22
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user