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

[R4R] Cache-wrap context during ante handler exec #2781

Merged
merged 21 commits into from
Nov 16, 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
7 changes: 4 additions & 3 deletions PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ IMPROVEMENTS
* [\#2749](https://github.com/cosmos/cosmos-sdk/pull/2749) Add --chain-id flag to gaiad testnet

* Gaia
- #2773 Require moniker to be provided on `gaiad init`.
- #2672 [Makefile] Updated for better Windows compatibility and ledger support logic, get_tools was rewritten as a cross-compatible Makefile.
- [#110](https://github.com/tendermint/devops/issues/110) Updated CircleCI job to trigger website build when cosmos docs are updated.
- #2772 Update BaseApp to not persist state when the ante handler fails on DeliverTx.
- #2773 Require moniker to be provided on `gaiad init`.
- #2672 [Makefile] Updated for better Windows compatibility and ledger support logic, get_tools was rewritten as a cross-compatible Makefile.
- [#110](https://github.com/tendermint/devops/issues/110) Updated CircleCI job to trigger website build when cosmos docs are updated.

* SDK
- [x/mock/simulation] [\#2720] major cleanup, introduction of helper objects, reorganization
Expand Down
69 changes: 47 additions & 22 deletions baseapp/baseapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

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

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

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

Expand All @@ -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
Expand Down
109 changes: 100 additions & 9 deletions baseapp/baseapp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

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

Ok so I guess the first arg, 1, means that the ante handler for one of the two above succeeded? But it's not clear which one.

The second 0 means that none of the msgs succeeded, but that wasn't clear either...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added explicit store checks via getting the app's state. Lmk if this still isn't clear. This logic is baked into the testing ante handler and message handler, so I'm trying to work with what I already have without making further drastic changes.


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()
}
4 changes: 4 additions & 0 deletions cmd/gaia/cli_test/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -537,16 +537,20 @@ func TestGaiaCLISendGenerateSignAndBroadcast(t *testing.T) {
success, stdout, _ = executeWriteRetStdStreams(t, fmt.Sprintf(
"gaiacli tx broadcast %v --json %v", flags, signedTxFile.Name()))
require.True(t, success)

var result struct {
Response abci.ResponseDeliverTx
}

require.Nil(t, app.MakeCodec().UnmarshalJSON([]byte(stdout), &result))
require.Equal(t, msg.Fee.Gas, result.Response.GasUsed)
require.Equal(t, msg.Fee.Gas, result.Response.GasWanted)

tests.WaitForNextNBlocksTM(2, port)

barAcc := executeGetAccount(t, fmt.Sprintf("gaiacli query account %s %v", barAddr, flags))
require.Equal(t, int64(10), barAcc.GetCoins().AmountOf(stakeTypes.DefaultBondDenom).Int64())

fooAcc = executeGetAccount(t, fmt.Sprintf("gaiacli query account %s %v", fooAddr, flags))
require.Equal(t, int64(40), fooAcc.GetCoins().AmountOf(stakeTypes.DefaultBondDenom).Int64())
}
Expand Down
Loading