cosmos-sdk/tests/integration/v2/app.go
Matt Kocubinski 43e28b43ad
refactor(store/v2): simplify genesis flow (#22435)
Co-authored-by: Marko <marko@baricevic.me>
2024-11-08 14:05:25 +00:00

412 lines
12 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"
"cosmossdk.io/core/comet"
corecontext "cosmossdk.io/core/context"
"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"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
"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
// 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
}
func DefaultStartUpConfig(t *testing.T) StartupConfig {
t.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 := t.TempDir()
t.Logf("generated integration test app config; HomeDir=%s", homedir)
return StartupConfig{
ValidatorSet: CreateRandomValidatorSet,
GenesisBehavior: Genesis_COMMIT,
GenesisAccounts: []GenesisAccount{ga},
HomeDir: homedir,
}
}
// 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",
},
},
services.NewGenesisHeaderService(stf.HeaderService{}),
cometService,
kvFactory,
&eventService{},
storeBuilder,
),
depinject.Invoke(
std.RegisterInterfaces,
),
),
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
}
// 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++
return resp, state
}
// StateLatestContext creates returns a new context from context.Background() with the latest state.
func (a *App) StateLatestContext(t *testing.T) context.Context {
t.Helper()
_, state, err := a.Store.StateLatest()
require.NoError(t, 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))
signMode, err := authsign.APISignModeToInternal(a.txConfig.SignModeHandler().DefaultMode())
require.NoError(t, err)
// 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: signMode,
},
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 {
signerData := authsign.SignerData{
Address: sdk.AccAddress(p.PubKey().Address()).String(),
ChainID: chainID,
AccountNumber: accNums[i],
Sequence: accSeqs[i],
PubKey: p.PubKey(),
}
signBytes, err := authsign.GetSignBytesAdapter(
ctx, a.txConfig.SignModeHandler(), signMode, 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
}
// 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()
}