492 lines
14 KiB
Go
492 lines
14 KiB
Go
package integration
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"testing"
|
|
"time"
|
|
|
|
cmtproto "github.com/cometbft/cometbft/api/cometbft/types/v1"
|
|
cmtjson "github.com/cometbft/cometbft/libs/json"
|
|
cmttypes "github.com/cometbft/cometbft/types"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/types/known/anypb"
|
|
|
|
corebranch "cosmossdk.io/core/branch"
|
|
"cosmossdk.io/core/comet"
|
|
corecontext "cosmossdk.io/core/context"
|
|
"cosmossdk.io/core/gas"
|
|
"cosmossdk.io/core/header"
|
|
"cosmossdk.io/core/server"
|
|
corestore "cosmossdk.io/core/store"
|
|
"cosmossdk.io/core/transaction"
|
|
"cosmossdk.io/depinject"
|
|
sdkmath "cosmossdk.io/math"
|
|
"cosmossdk.io/runtime/v2"
|
|
"cosmossdk.io/runtime/v2/services"
|
|
"cosmossdk.io/server/v2/stf"
|
|
"cosmossdk.io/server/v2/stf/branch"
|
|
"cosmossdk.io/store/v2"
|
|
"cosmossdk.io/store/v2/root"
|
|
bankkeeper "cosmossdk.io/x/bank/keeper"
|
|
banktypes "cosmossdk.io/x/bank/types"
|
|
consensustypes "cosmossdk.io/x/consensus/types"
|
|
txsigning "cosmossdk.io/x/tx/signing"
|
|
|
|
"github.com/cosmos/cosmos-sdk/client"
|
|
"github.com/cosmos/cosmos-sdk/codec"
|
|
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
|
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
|
|
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
|
|
"github.com/cosmos/cosmos-sdk/std"
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
"github.com/cosmos/cosmos-sdk/types/simulation"
|
|
"github.com/cosmos/cosmos-sdk/types/tx/signing"
|
|
authsign "github.com/cosmos/cosmos-sdk/x/auth/signing"
|
|
"github.com/cosmos/cosmos-sdk/x/auth/tx"
|
|
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
|
)
|
|
|
|
const DefaultGenTxGas = 10000000
|
|
const (
|
|
Genesis_COMMIT = iota
|
|
Genesis_NOCOMMIT
|
|
Genesis_SKIP
|
|
)
|
|
|
|
type stateMachineTx = transaction.Tx
|
|
|
|
type handler = func(ctx context.Context) (transaction.Msg, error)
|
|
|
|
// DefaultConsensusParams defines the default CometBFT consensus params used in
|
|
// SimApp testing.
|
|
var DefaultConsensusParams = &cmtproto.ConsensusParams{
|
|
Version: &cmtproto.VersionParams{
|
|
App: 1,
|
|
},
|
|
Block: &cmtproto.BlockParams{
|
|
MaxBytes: 200000,
|
|
MaxGas: 100_000_000,
|
|
},
|
|
Evidence: &cmtproto.EvidenceParams{
|
|
MaxAgeNumBlocks: 302400,
|
|
MaxAgeDuration: 504 * time.Hour, // 3 weeks is the max duration
|
|
MaxBytes: 10000,
|
|
},
|
|
Validator: &cmtproto.ValidatorParams{
|
|
PubKeyTypes: []string{
|
|
cmttypes.ABCIPubKeyTypeEd25519,
|
|
cmttypes.ABCIPubKeyTypeSecp256k1,
|
|
},
|
|
},
|
|
}
|
|
|
|
// StartupConfig defines the startup configuration of a new test app.
|
|
type StartupConfig struct {
|
|
// ValidatorSet defines a custom validator set to be validating the app.
|
|
ValidatorSet func() (*cmttypes.ValidatorSet, error)
|
|
// AppOption defines the additional operations that will be run in the app builder phase.
|
|
AppOption runtime.AppBuilderOption[stateMachineTx]
|
|
// GenesisBehavior defines the behavior of the app at genesis.
|
|
GenesisBehavior int
|
|
// GenesisAccounts defines the genesis accounts to be used in the app.
|
|
GenesisAccounts []GenesisAccount
|
|
// HomeDir defines the home directory of the app where config and data will be stored.
|
|
HomeDir string
|
|
// BranchService defines the custom branch service to be used in the app.
|
|
BranchService corebranch.Service
|
|
// RouterServiceBuilder defines the custom builder
|
|
// for msg router and query router service to be used in the app.
|
|
RouterServiceBuilder runtime.RouterServiceBuilder
|
|
// HeaderService defines the custom header service to be used in the app.
|
|
HeaderService header.Service
|
|
|
|
GasService gas.Service
|
|
}
|
|
|
|
func DefaultStartUpConfig(tb testing.TB) StartupConfig {
|
|
tb.Helper()
|
|
|
|
priv := secp256k1.GenPrivKey()
|
|
ba := authtypes.NewBaseAccount(
|
|
priv.PubKey().Address().Bytes(),
|
|
priv.PubKey(),
|
|
0,
|
|
0,
|
|
)
|
|
ga := GenesisAccount{
|
|
ba,
|
|
sdk.NewCoins(
|
|
sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100000000000000)),
|
|
),
|
|
}
|
|
homedir := tb.TempDir()
|
|
tb.Logf("generated integration test app config; HomeDir=%s", homedir)
|
|
return StartupConfig{
|
|
ValidatorSet: CreateRandomValidatorSet,
|
|
GenesisBehavior: Genesis_COMMIT,
|
|
GenesisAccounts: []GenesisAccount{ga},
|
|
HomeDir: homedir,
|
|
BranchService: stf.BranchService{},
|
|
RouterServiceBuilder: runtime.NewRouterBuilder(
|
|
stf.NewMsgRouterService, stf.NewQueryRouterService(),
|
|
),
|
|
HeaderService: services.NewGenesisHeaderService(stf.HeaderService{}),
|
|
GasService: stf.NewGasMeterService(),
|
|
}
|
|
}
|
|
|
|
// RunMsgConfig defines the run message configuration.
|
|
type RunMsgConfig struct {
|
|
Commit bool
|
|
}
|
|
|
|
// Option is a function that can be used to configure the integration app.
|
|
type Option func(*RunMsgConfig)
|
|
|
|
// WithAutomaticCommit enables automatic commit.
|
|
// This means that the integration app will automatically commit the state after each msg.
|
|
func WithAutomaticCommit() Option {
|
|
return func(cfg *RunMsgConfig) {
|
|
cfg.Commit = true
|
|
}
|
|
}
|
|
|
|
// NewApp initializes a new runtime.App. A Nop logger is set in runtime.App.
|
|
// appConfig defines the application configuration (f.e. app_config.go).
|
|
// extraOutputs defines the extra outputs to be assigned by the dependency injector (depinject).
|
|
func NewApp(
|
|
appConfig depinject.Config,
|
|
startupConfig StartupConfig,
|
|
extraOutputs ...interface{},
|
|
) (*App, error) {
|
|
// create the app with depinject
|
|
var (
|
|
storeBuilder = root.NewBuilder()
|
|
app *runtime.App[stateMachineTx]
|
|
appBuilder *runtime.AppBuilder[stateMachineTx]
|
|
txConfig client.TxConfig
|
|
txConfigOptions tx.ConfigOptions
|
|
cometService comet.Service = &cometServiceImpl{}
|
|
kvFactory corestore.KVStoreServiceFactory = func(actor []byte) corestore.KVStoreService {
|
|
return services.NewGenesisKVService(actor, &storeService{actor, stf.NewKVStoreService(actor)})
|
|
}
|
|
cdc codec.Codec
|
|
err error
|
|
)
|
|
|
|
if err := depinject.Inject(
|
|
depinject.Configs(
|
|
appConfig,
|
|
codec.DefaultProviders,
|
|
depinject.Supply(
|
|
&root.Config{
|
|
Home: startupConfig.HomeDir,
|
|
AppDBBackend: "goleveldb",
|
|
Options: root.DefaultStoreOptions(),
|
|
},
|
|
runtime.GlobalConfig{
|
|
"server": server.ConfigMap{
|
|
"minimum-gas-prices": "0stake",
|
|
},
|
|
},
|
|
cometService,
|
|
kvFactory,
|
|
&eventService{},
|
|
storeBuilder,
|
|
startupConfig.BranchService,
|
|
startupConfig.RouterServiceBuilder,
|
|
startupConfig.HeaderService,
|
|
startupConfig.GasService,
|
|
),
|
|
depinject.Invoke(
|
|
std.RegisterInterfaces,
|
|
std.RegisterLegacyAminoCodec,
|
|
),
|
|
),
|
|
append(extraOutputs, &appBuilder, &cdc, &txConfigOptions, &txConfig, &storeBuilder)...); err != nil {
|
|
return nil, fmt.Errorf("failed to inject dependencies: %w", err)
|
|
}
|
|
|
|
app, err = appBuilder.Build()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to build app: %w", err)
|
|
}
|
|
if err := app.LoadLatest(); err != nil {
|
|
return nil, fmt.Errorf("failed to load app: %w", err)
|
|
}
|
|
|
|
store := storeBuilder.Get()
|
|
if store == nil {
|
|
return nil, fmt.Errorf("failed to build store: %w", err)
|
|
}
|
|
err = store.SetInitialVersion(0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to set initial version: %w", err)
|
|
}
|
|
|
|
integrationApp := &App{App: app, Store: store, txConfig: txConfig, lastHeight: 0}
|
|
if startupConfig.GenesisBehavior == Genesis_SKIP {
|
|
return integrationApp, nil
|
|
}
|
|
|
|
// create validator set
|
|
valSet, err := startupConfig.ValidatorSet()
|
|
if err != nil {
|
|
return nil, errors.New("failed to create validator set")
|
|
}
|
|
|
|
var (
|
|
balances []banktypes.Balance
|
|
genAccounts []authtypes.GenesisAccount
|
|
)
|
|
for _, ga := range startupConfig.GenesisAccounts {
|
|
genAccounts = append(genAccounts, ga.GenesisAccount)
|
|
balances = append(
|
|
balances,
|
|
banktypes.Balance{
|
|
Address: ga.GenesisAccount.GetAddress().String(),
|
|
Coins: ga.Coins,
|
|
},
|
|
)
|
|
}
|
|
|
|
genesisJSON, err := genesisStateWithValSet(
|
|
cdc,
|
|
app.DefaultGenesis(),
|
|
valSet,
|
|
genAccounts,
|
|
balances...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create genesis state: %w", err)
|
|
}
|
|
|
|
// init chain must be called to stop deliverState from being nil
|
|
genesisJSONBytes, err := cmtjson.MarshalIndent(genesisJSON, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"failed to marshal default genesis state: %w",
|
|
err,
|
|
)
|
|
}
|
|
|
|
ctx := context.WithValue(
|
|
context.Background(),
|
|
corecontext.CometParamsInitInfoKey,
|
|
&consensustypes.MsgUpdateParams{
|
|
Authority: "consensus",
|
|
Block: DefaultConsensusParams.Block,
|
|
Evidence: DefaultConsensusParams.Evidence,
|
|
Validator: DefaultConsensusParams.Validator,
|
|
Abci: DefaultConsensusParams.Abci,
|
|
Synchrony: DefaultConsensusParams.Synchrony,
|
|
Feature: DefaultConsensusParams.Feature,
|
|
},
|
|
)
|
|
|
|
emptyHash := sha256.Sum256(nil)
|
|
_, genesisState, err := app.InitGenesis(
|
|
ctx,
|
|
&server.BlockRequest[stateMachineTx]{
|
|
Height: 1,
|
|
Time: time.Now(),
|
|
Hash: emptyHash[:],
|
|
ChainId: "test-chain",
|
|
AppHash: emptyHash[:],
|
|
IsGenesis: true,
|
|
},
|
|
genesisJSONBytes,
|
|
&genesisTxCodec{txConfigOptions},
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed init genesis: %w", err)
|
|
}
|
|
|
|
if startupConfig.GenesisBehavior == Genesis_NOCOMMIT {
|
|
integrationApp.lastHeight = 0
|
|
return integrationApp, nil
|
|
}
|
|
|
|
_, err = integrationApp.Commit(genesisState)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to commit initial version: %w", err)
|
|
}
|
|
|
|
return integrationApp, nil
|
|
}
|
|
|
|
// App is a wrapper around runtime.App that provides additional testing utilities.
|
|
type App struct {
|
|
*runtime.App[stateMachineTx]
|
|
lastHeight uint64
|
|
Store store.RootStore
|
|
txConfig client.TxConfig
|
|
}
|
|
|
|
func (a App) LastBlockHeight() uint64 {
|
|
return a.lastHeight
|
|
}
|
|
|
|
// Deliver delivers a block with the given transactions and returns the resulting state.
|
|
func (a *App) Deliver(
|
|
t *testing.T, ctx context.Context, txs []stateMachineTx,
|
|
) (*server.BlockResponse, corestore.WriterMap) {
|
|
t.Helper()
|
|
req := &server.BlockRequest[stateMachineTx]{
|
|
Height: a.lastHeight + 1,
|
|
Txs: txs,
|
|
Hash: make([]byte, 32),
|
|
AppHash: make([]byte, 32),
|
|
}
|
|
resp, state, err := a.DeliverBlock(ctx, req)
|
|
require.NoError(t, err)
|
|
a.lastHeight++
|
|
|
|
// update block height and block time if integration context is present
|
|
iCtx, ok := ctx.Value(contextKey).(*integrationContext)
|
|
if ok {
|
|
iCtx.header.Height = int64(a.lastHeight)
|
|
}
|
|
return resp, state
|
|
}
|
|
|
|
// StateLatestContext creates returns a new context from context.Background() with the latest state.
|
|
func (a *App) StateLatestContext(tb testing.TB) context.Context {
|
|
tb.Helper()
|
|
_, state, err := a.Store.StateLatest()
|
|
require.NoError(tb, err)
|
|
writeableState := branch.DefaultNewWriterMap(state)
|
|
iCtx := &integrationContext{state: writeableState}
|
|
return context.WithValue(context.Background(), contextKey, iCtx)
|
|
}
|
|
|
|
// Commit commits the given state and returns the new state hash.
|
|
func (a *App) Commit(state corestore.WriterMap) ([]byte, error) {
|
|
changes, err := state.GetStateChanges()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get state changes: %w", err)
|
|
}
|
|
cs := &corestore.Changeset{Version: a.lastHeight, Changes: changes}
|
|
return a.Store.Commit(cs)
|
|
}
|
|
|
|
// SignCheckDeliver signs and checks the given messages and delivers them.
|
|
func (a *App) SignCheckDeliver(
|
|
t *testing.T, ctx context.Context, msgs []sdk.Msg,
|
|
chainID string, accNums, accSeqs []uint64, privateKeys []cryptotypes.PrivKey,
|
|
txErrString string,
|
|
) server.TxResult {
|
|
t.Helper()
|
|
|
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
sigs := make([]signing.SignatureV2, len(privateKeys))
|
|
|
|
// create a random length memo
|
|
memo := simulation.RandStringOfLength(r, simulation.RandIntBetween(r, 0, 100))
|
|
|
|
// 1st round: set SignatureV2 with empty signatures, to set correct
|
|
// signer infos.
|
|
for i, p := range privateKeys {
|
|
sigs[i] = signing.SignatureV2{
|
|
PubKey: p.PubKey(),
|
|
Data: &signing.SingleSignatureData{
|
|
SignMode: a.txConfig.SignModeHandler().DefaultMode(),
|
|
},
|
|
Sequence: accSeqs[i],
|
|
}
|
|
}
|
|
|
|
txBuilder := a.txConfig.NewTxBuilder()
|
|
err := txBuilder.SetMsgs(msgs...)
|
|
require.NoError(t, err)
|
|
err = txBuilder.SetSignatures(sigs...)
|
|
require.NoError(t, err)
|
|
txBuilder.SetMemo(memo)
|
|
txBuilder.SetFeeAmount(sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)})
|
|
txBuilder.SetGasLimit(DefaultGenTxGas)
|
|
|
|
// 2nd round: once all signer infos are set, every signer can sign.
|
|
for i, p := range privateKeys {
|
|
anyPk, err := codectypes.NewAnyWithValue(p.PubKey())
|
|
require.NoError(t, err)
|
|
|
|
signerData := txsigning.SignerData{
|
|
Address: sdk.AccAddress(p.PubKey().Address()).String(),
|
|
ChainID: chainID,
|
|
AccountNumber: accNums[i],
|
|
Sequence: accSeqs[i],
|
|
PubKey: &anypb.Any{TypeUrl: anyPk.TypeUrl, Value: anyPk.Value},
|
|
}
|
|
|
|
signBytes, err := authsign.GetSignBytesAdapter(
|
|
ctx, a.txConfig.SignModeHandler(), a.txConfig.SignModeHandler().DefaultMode(), signerData,
|
|
// todo why fetch twice?
|
|
txBuilder.GetTx())
|
|
require.NoError(t, err)
|
|
sig, err := p.Sign(signBytes)
|
|
require.NoError(t, err)
|
|
sigs[i].Data.(*signing.SingleSignatureData).Signature = sig
|
|
}
|
|
err = txBuilder.SetSignatures(sigs...)
|
|
require.NoError(t, err)
|
|
|
|
builtTx := txBuilder.GetTx()
|
|
blockResponse, blockState := a.Deliver(t, ctx, []stateMachineTx{builtTx})
|
|
|
|
require.Equal(t, 1, len(blockResponse.TxResults))
|
|
txResult := blockResponse.TxResults[0]
|
|
if txErrString != "" {
|
|
require.ErrorContains(t, txResult.Error, txErrString)
|
|
} else {
|
|
require.NoError(t, txResult.Error)
|
|
}
|
|
|
|
_, err = a.Commit(blockState)
|
|
require.NoError(t, err)
|
|
|
|
return txResult
|
|
}
|
|
|
|
// RunMsg runs the handler for a transaction message.
|
|
// It required the context to have the integration context.
|
|
// a new state is committed if the option WithAutomaticCommit is set in options.
|
|
func (app *App) RunMsg(t *testing.T, ctx context.Context, handler handler, option ...Option) (resp transaction.Msg, err error) {
|
|
t.Helper()
|
|
|
|
// set options
|
|
cfg := &RunMsgConfig{}
|
|
for _, opt := range option {
|
|
opt(cfg)
|
|
}
|
|
|
|
// need to have integration context
|
|
integrationCtx, ok := ctx.Value(contextKey).(*integrationContext)
|
|
require.True(t, ok)
|
|
|
|
resp, err = handler(ctx)
|
|
|
|
if cfg.Commit {
|
|
app.lastHeight++
|
|
_, err := app.Commit(integrationCtx.state)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
return resp, err
|
|
}
|
|
|
|
// CheckBalance checks the balance of the given address.
|
|
func (a *App) CheckBalance(
|
|
t *testing.T, ctx context.Context, addr sdk.AccAddress, expected sdk.Coins, keeper bankkeeper.Keeper,
|
|
) {
|
|
t.Helper()
|
|
balances := keeper.GetAllBalances(ctx, addr)
|
|
require.Equal(t, expected, balances)
|
|
}
|
|
|
|
func (a *App) Close() error {
|
|
return a.Store.Close()
|
|
}
|