269 lines
8.3 KiB
Go
269 lines
8.3 KiB
Go
package stf
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/cosmos/gogoproto/proto"
|
|
gogotypes "github.com/cosmos/gogoproto/types"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
appmanager "cosmossdk.io/core/app"
|
|
appmodulev2 "cosmossdk.io/core/appmodule/v2"
|
|
coregas "cosmossdk.io/core/gas"
|
|
"cosmossdk.io/core/store"
|
|
"cosmossdk.io/server/v2/stf/branch"
|
|
"cosmossdk.io/server/v2/stf/gas"
|
|
"cosmossdk.io/server/v2/stf/mock"
|
|
)
|
|
|
|
func addMsgHandlerToSTF[T any, PT interface {
|
|
*T
|
|
proto.Message
|
|
},
|
|
U any, UT interface {
|
|
*U
|
|
proto.Message
|
|
}](
|
|
t *testing.T,
|
|
stf *STF[mock.Tx],
|
|
handler func(ctx context.Context, msg PT) (UT, error),
|
|
) {
|
|
t.Helper()
|
|
msgRouterBuilder := NewMsgRouterBuilder()
|
|
err := msgRouterBuilder.RegisterHandler(
|
|
msgTypeURL(PT(new(T))),
|
|
func(ctx context.Context, msg appmodulev2.Message) (msgResp appmodulev2.Message, err error) {
|
|
typedReq := msg.(PT)
|
|
typedResp, err := handler(ctx, typedReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return typedResp, nil
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
msgRouter, err := msgRouterBuilder.Build()
|
|
require.NoError(t, err)
|
|
stf.msgRouter = msgRouter
|
|
}
|
|
|
|
func TestSTF(t *testing.T) {
|
|
state := mock.DB()
|
|
mockTx := mock.Tx{
|
|
Sender: []byte("sender"),
|
|
Msg: &gogotypes.BoolValue{Value: true},
|
|
GasLimit: 100_000,
|
|
}
|
|
|
|
sum := sha256.Sum256([]byte("test-hash"))
|
|
|
|
s := &STF[mock.Tx]{
|
|
doPreBlock: func(ctx context.Context, txs []mock.Tx) error { return nil },
|
|
doBeginBlock: func(ctx context.Context) error {
|
|
kvSet(t, ctx, "begin-block")
|
|
return nil
|
|
},
|
|
doEndBlock: func(ctx context.Context) error {
|
|
kvSet(t, ctx, "end-block")
|
|
return nil
|
|
},
|
|
doValidatorUpdate: func(ctx context.Context) ([]appmodulev2.ValidatorUpdate, error) { return nil, nil },
|
|
doTxValidation: func(ctx context.Context, tx mock.Tx) error {
|
|
kvSet(t, ctx, "validate")
|
|
return nil
|
|
},
|
|
postTxExec: func(ctx context.Context, tx mock.Tx, success bool) error {
|
|
kvSet(t, ctx, "post-tx-exec")
|
|
return nil
|
|
},
|
|
branchFn: branch.DefaultNewWriterMap,
|
|
makeGasMeter: gas.DefaultGasMeter,
|
|
makeGasMeteredState: gas.DefaultWrapWithGasMeter,
|
|
}
|
|
|
|
addMsgHandlerToSTF(t, s, func(ctx context.Context, msg *gogotypes.BoolValue) (*gogotypes.BoolValue, error) {
|
|
kvSet(t, ctx, "exec")
|
|
return nil, nil
|
|
})
|
|
|
|
t.Run("begin and end block", func(t *testing.T) {
|
|
_, newState, err := s.DeliverBlock(context.Background(), &appmanager.BlockRequest[mock.Tx]{
|
|
Height: uint64(1),
|
|
Time: time.Date(2024, 2, 3, 18, 23, 0, 0, time.UTC),
|
|
AppHash: sum[:],
|
|
Hash: sum[:],
|
|
}, state)
|
|
require.NoError(t, err)
|
|
stateHas(t, newState, "begin-block")
|
|
stateHas(t, newState, "end-block")
|
|
})
|
|
|
|
t.Run("basic tx", func(t *testing.T) {
|
|
result, newState, err := s.DeliverBlock(context.Background(), &appmanager.BlockRequest[mock.Tx]{
|
|
Height: uint64(1),
|
|
Time: time.Date(2024, 2, 3, 18, 23, 0, 0, time.UTC),
|
|
AppHash: sum[:],
|
|
Hash: sum[:],
|
|
Txs: []mock.Tx{mockTx},
|
|
}, state)
|
|
require.NoError(t, err)
|
|
stateHas(t, newState, "validate")
|
|
stateHas(t, newState, "exec")
|
|
stateHas(t, newState, "post-tx-exec")
|
|
|
|
require.Len(t, result.TxResults, 1)
|
|
txResult := result.TxResults[0]
|
|
require.NotZero(t, txResult.GasUsed)
|
|
require.Equal(t, mockTx.GasLimit, txResult.GasWanted)
|
|
})
|
|
|
|
t.Run("exec tx out of gas", func(t *testing.T) {
|
|
s := s.clone()
|
|
|
|
mockTx := mock.Tx{
|
|
Sender: []byte("sender"),
|
|
Msg: &gogotypes.BoolValue{Value: true}, // msg does not matter at all because our handler does nothing.
|
|
GasLimit: 0, // NO GAS!
|
|
}
|
|
|
|
// this handler will propagate the storage error back, we expect
|
|
// out of gas immediately at tx validation level.
|
|
s.doTxValidation = func(ctx context.Context, tx mock.Tx) error {
|
|
w, err := ctx.(*executionContext).state.GetWriter(actorName)
|
|
require.NoError(t, err)
|
|
err = w.Set([]byte("gas_failure"), []byte{})
|
|
require.Error(t, err)
|
|
return err
|
|
}
|
|
|
|
result, newState, err := s.DeliverBlock(context.Background(), &appmanager.BlockRequest[mock.Tx]{
|
|
Height: uint64(1),
|
|
Time: time.Date(2024, 2, 3, 18, 23, 0, 0, time.UTC),
|
|
AppHash: sum[:],
|
|
Hash: sum[:],
|
|
Txs: []mock.Tx{mockTx},
|
|
}, state)
|
|
require.NoError(t, err)
|
|
stateNotHas(t, newState, "gas_failure") // assert during out of gas no state changes leaked.
|
|
require.ErrorIs(t, result.TxResults[0].Error, coregas.ErrOutOfGas, result.TxResults[0].Error)
|
|
})
|
|
|
|
t.Run("fail exec tx", func(t *testing.T) {
|
|
// update the stf to fail on the handler
|
|
s := s.clone()
|
|
addMsgHandlerToSTF(t, &s, func(ctx context.Context, msg *gogotypes.BoolValue) (*gogotypes.BoolValue, error) {
|
|
return nil, errors.New("failure")
|
|
})
|
|
|
|
blockResult, newState, err := s.DeliverBlock(context.Background(), &appmanager.BlockRequest[mock.Tx]{
|
|
Height: uint64(1),
|
|
Time: time.Date(2024, 2, 3, 18, 23, 0, 0, time.UTC),
|
|
AppHash: sum[:],
|
|
Hash: sum[:],
|
|
Txs: []mock.Tx{mockTx},
|
|
}, state)
|
|
require.NoError(t, err)
|
|
require.ErrorContains(t, blockResult.TxResults[0].Error, "failure")
|
|
stateHas(t, newState, "begin-block")
|
|
stateHas(t, newState, "end-block")
|
|
stateHas(t, newState, "validate")
|
|
stateNotHas(t, newState, "exec")
|
|
stateHas(t, newState, "post-tx-exec")
|
|
})
|
|
|
|
t.Run("tx is success but post tx failed", func(t *testing.T) {
|
|
s := s.clone()
|
|
s.postTxExec = func(ctx context.Context, tx mock.Tx, success bool) error {
|
|
return errors.New("post tx failure")
|
|
}
|
|
blockResult, newState, err := s.DeliverBlock(context.Background(), &appmanager.BlockRequest[mock.Tx]{
|
|
Height: uint64(1),
|
|
Time: time.Date(2024, 2, 3, 18, 23, 0, 0, time.UTC),
|
|
AppHash: sum[:],
|
|
Hash: sum[:],
|
|
Txs: []mock.Tx{mockTx},
|
|
}, state)
|
|
require.NoError(t, err)
|
|
require.ErrorContains(t, blockResult.TxResults[0].Error, "post tx failure")
|
|
stateHas(t, newState, "begin-block")
|
|
stateHas(t, newState, "end-block")
|
|
stateHas(t, newState, "validate")
|
|
stateNotHas(t, newState, "exec")
|
|
stateNotHas(t, newState, "post-tx-exec")
|
|
})
|
|
|
|
t.Run("tx failed and post tx failed", func(t *testing.T) {
|
|
s := s.clone()
|
|
addMsgHandlerToSTF(t, &s, func(ctx context.Context, msg *gogotypes.BoolValue) (*gogotypes.BoolValue, error) {
|
|
return nil, errors.New("exec failure")
|
|
})
|
|
s.postTxExec = func(ctx context.Context, tx mock.Tx, success bool) error { return errors.New("post tx failure") }
|
|
blockResult, newState, err := s.DeliverBlock(context.Background(), &appmanager.BlockRequest[mock.Tx]{
|
|
Height: uint64(1),
|
|
Time: time.Date(2024, 2, 3, 18, 23, 0, 0, time.UTC),
|
|
AppHash: sum[:],
|
|
Hash: sum[:],
|
|
Txs: []mock.Tx{mockTx},
|
|
}, state)
|
|
require.NoError(t, err)
|
|
require.ErrorContains(t, blockResult.TxResults[0].Error, "exec failure\npost tx failure")
|
|
stateHas(t, newState, "begin-block")
|
|
stateHas(t, newState, "end-block")
|
|
stateHas(t, newState, "validate")
|
|
stateNotHas(t, newState, "exec")
|
|
stateNotHas(t, newState, "post-tx-exec")
|
|
})
|
|
|
|
t.Run("fail validate tx", func(t *testing.T) {
|
|
// update stf to fail on the validation step
|
|
s := s.clone()
|
|
s.doTxValidation = func(ctx context.Context, tx mock.Tx) error { return errors.New("failure") }
|
|
blockResult, newState, err := s.DeliverBlock(context.Background(), &appmanager.BlockRequest[mock.Tx]{
|
|
Height: uint64(1),
|
|
Time: time.Date(2024, 2, 3, 18, 23, 0, 0, time.UTC),
|
|
AppHash: sum[:],
|
|
Hash: sum[:],
|
|
Txs: []mock.Tx{mockTx},
|
|
}, state)
|
|
require.NoError(t, err)
|
|
require.ErrorContains(t, blockResult.TxResults[0].Error, "failure")
|
|
stateHas(t, newState, "begin-block")
|
|
stateHas(t, newState, "end-block")
|
|
stateNotHas(t, newState, "validate")
|
|
stateNotHas(t, newState, "exec")
|
|
})
|
|
}
|
|
|
|
var actorName = []byte("cookies")
|
|
|
|
func kvSet(t *testing.T, ctx context.Context, v string) {
|
|
t.Helper()
|
|
state, err := ctx.(*executionContext).state.GetWriter(actorName)
|
|
require.NoError(t, err)
|
|
require.NoError(t, state.Set([]byte(v), []byte(v)))
|
|
}
|
|
|
|
func stateHas(t *testing.T, accountState store.ReaderMap, key string) {
|
|
t.Helper()
|
|
state, err := accountState.GetReader(actorName)
|
|
require.NoError(t, err)
|
|
has, err := state.Has([]byte(key))
|
|
require.NoError(t, err)
|
|
require.Truef(t, has, "state did not have key: %s", key)
|
|
}
|
|
|
|
func stateNotHas(t *testing.T, accountState store.ReaderMap, key string) {
|
|
t.Helper()
|
|
state, err := accountState.GetReader(actorName)
|
|
require.NoError(t, err)
|
|
has, err := state.Has([]byte(key))
|
|
require.NoError(t, err)
|
|
require.Falsef(t, has, "state was not supposed to have key: %s", key)
|
|
}
|