feat: [ENG-792] Implement E2E Testing Framework (#107)
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
dbm "github.com/cometbft/cometbft-db"
|
||||
"github.com/cometbft/cometbft/libs/log"
|
||||
cometrand "github.com/cometbft/cometbft/libs/rand"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
|
||||
"github.com/skip-mev/pob/tests/app"
|
||||
"github.com/skip-mev/pob/tests/app/params"
|
||||
)
|
||||
|
||||
const (
|
||||
keyringPassphrase = "testpassphrase"
|
||||
keyringAppName = "testnet"
|
||||
)
|
||||
|
||||
var (
|
||||
encodingConfig params.EncodingConfig
|
||||
cdc codec.Codec
|
||||
)
|
||||
|
||||
func init() {
|
||||
testApp := app.New(log.NewNopLogger(), dbm.NewMemDB(), nil, true, simtestutil.NewAppOptionsWithFlagHome(app.DefaultNodeHome))
|
||||
encodingConfig = params.EncodingConfig{
|
||||
InterfaceRegistry: testApp.InterfaceRegistry(),
|
||||
Codec: testApp.AppCodec(),
|
||||
TxConfig: testApp.TxConfig(),
|
||||
Amino: testApp.LegacyAmino(),
|
||||
}
|
||||
cdc = encodingConfig.Codec
|
||||
}
|
||||
|
||||
type chain struct {
|
||||
dataDir string
|
||||
id string
|
||||
validators []*validator
|
||||
}
|
||||
|
||||
func newChain() (*chain, error) {
|
||||
tmpDir, err := os.MkdirTemp("", "pob-e2e-testnet-")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &chain{
|
||||
id: "chain-" + cometrand.NewRand().Str(6),
|
||||
dataDir: tmpDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *chain) configDir() string {
|
||||
return fmt.Sprintf("%s/%s", c.dataDir, c.id)
|
||||
}
|
||||
|
||||
func (c *chain) createAndInitValidators(count int) error {
|
||||
for i := 0; i < count; i++ {
|
||||
node := c.createValidator(i)
|
||||
|
||||
// generate genesis files
|
||||
if err := node.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.validators = append(c.validators, node)
|
||||
|
||||
// create keys
|
||||
if err := node.createKey("val"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := node.createNodeKey(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := node.createConsensusKey(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *chain) createValidator(index int) *validator {
|
||||
return &validator{
|
||||
chain: c,
|
||||
index: index,
|
||||
moniker: "testapp",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cometcfg "github.com/cometbft/cometbft/config"
|
||||
cometjson "github.com/cometbft/cometbft/libs/json"
|
||||
rpchttp "github.com/cometbft/cometbft/rpc/client/http"
|
||||
"github.com/cosmos/cosmos-sdk/server"
|
||||
srvconfig "github.com/cosmos/cosmos-sdk/server/config"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
|
||||
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
|
||||
govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
"github.com/skip-mev/pob/tests/app"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
var (
|
||||
numValidators = 3
|
||||
minGasPrice = sdk.NewDecCoinFromDec(app.BondDenom, sdk.MustNewDecFromStr("0.02")).String()
|
||||
initBalanceStr = sdk.NewInt64Coin(app.BondDenom, 510000000000).String()
|
||||
stakeAmount, _ = sdk.NewIntFromString("100000000000")
|
||||
stakeAmountCoin = sdk.NewCoin(app.BondDenom, stakeAmount)
|
||||
)
|
||||
|
||||
type IntegrationTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
tmpDirs []string
|
||||
chain *chain
|
||||
dkrPool *dockertest.Pool
|
||||
dkrNet *dockertest.Network
|
||||
valResources []*dockertest.Resource
|
||||
}
|
||||
|
||||
func TestIntegrationTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(IntegrationTestSuite))
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) SetupSuite() {
|
||||
s.T().Log("setting up e2e integration test suite...")
|
||||
|
||||
var err error
|
||||
s.chain, err = newChain()
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.T().Logf("starting e2e infrastructure; chain-id: %s; datadir: %s", s.chain.id, s.chain.dataDir)
|
||||
|
||||
s.dkrPool, err = dockertest.NewPool("")
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.dkrNet, err = s.dkrPool.CreateNetwork(fmt.Sprintf("%s-testnet", s.chain.id))
|
||||
s.Require().NoError(err)
|
||||
|
||||
// The bootstrapping phase is as follows:
|
||||
//
|
||||
// 1. Initialize TestApp validator nodes.
|
||||
// 2. Create and initialize TestApp validator genesis files, i.e. setting
|
||||
// delegate keys for validators.
|
||||
// 3. Start TestApp network.
|
||||
s.initNodes()
|
||||
s.initGenesis()
|
||||
s.initValidatorConfigs()
|
||||
s.runValidators()
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) TearDownSuite() {
|
||||
if str := os.Getenv("POB_E2E_SKIP_CLEANUP"); len(str) > 0 {
|
||||
skipCleanup, err := strconv.ParseBool(str)
|
||||
s.Require().NoError(err)
|
||||
|
||||
if skipCleanup {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s.T().Log("tearing down e2e integration test suite...")
|
||||
|
||||
for _, vc := range s.valResources {
|
||||
s.Require().NoError(s.dkrPool.Purge(vc))
|
||||
}
|
||||
|
||||
s.Require().NoError(s.dkrPool.RemoveNetwork(s.dkrNet))
|
||||
|
||||
os.RemoveAll(s.chain.dataDir)
|
||||
for _, td := range s.tmpDirs {
|
||||
os.RemoveAll(td)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) initNodes() {
|
||||
s.Require().NoError(s.chain.createAndInitValidators(numValidators))
|
||||
|
||||
// initialize a genesis file for the first validator
|
||||
val0ConfigDir := s.chain.validators[0].configDir()
|
||||
for _, val := range s.chain.validators {
|
||||
valAddr, err := val.keyInfo.GetAddress()
|
||||
s.Require().NoError(err)
|
||||
s.Require().NoError(addGenesisAccount(val0ConfigDir, "", initBalanceStr, valAddr))
|
||||
}
|
||||
|
||||
// copy the genesis file to the remaining validators
|
||||
for _, val := range s.chain.validators[1:] {
|
||||
_, err := copyFile(
|
||||
filepath.Join(val0ConfigDir, "config", "genesis.json"),
|
||||
filepath.Join(val.configDir(), "config", "genesis.json"),
|
||||
)
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) initGenesis() {
|
||||
serverCtx := server.NewDefaultContext()
|
||||
config := serverCtx.Config
|
||||
|
||||
config.SetRoot(s.chain.validators[0].configDir())
|
||||
config.Moniker = s.chain.validators[0].moniker
|
||||
|
||||
genFilePath := config.GenesisFile()
|
||||
appGenState, genDoc, err := genutiltypes.GenesisStateFromGenFile(genFilePath)
|
||||
s.T().Log("starting e2e infrastructure; validator_0 config:", genFilePath)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// x/gov
|
||||
var govGenState govtypesv1.GenesisState
|
||||
s.Require().NoError(cdc.UnmarshalJSON(appGenState[govtypes.ModuleName], &govGenState))
|
||||
|
||||
votingPeriod := 5 * time.Second
|
||||
govGenState.Params.VotingPeriod = &votingPeriod
|
||||
govGenState.Params.MinDeposit = sdk.NewCoins(sdk.NewCoin(app.BondDenom, sdk.NewInt(100)))
|
||||
|
||||
bz, err := cdc.MarshalJSON(&govGenState)
|
||||
s.Require().NoError(err)
|
||||
appGenState[govtypes.ModuleName] = bz
|
||||
|
||||
var genUtilGenState genutiltypes.GenesisState
|
||||
s.Require().NoError(cdc.UnmarshalJSON(appGenState[genutiltypes.ModuleName], &genUtilGenState))
|
||||
|
||||
// x/genutil genesis txs
|
||||
genTxs := make([]json.RawMessage, len(s.chain.validators))
|
||||
for i, val := range s.chain.validators {
|
||||
createValMsg, err := val.buildCreateValidatorMsg(stakeAmountCoin)
|
||||
s.Require().NoError(err)
|
||||
|
||||
signedTx, err := val.signMsg(createValMsg)
|
||||
s.Require().NoError(err)
|
||||
|
||||
txRaw, err := cdc.MarshalJSON(signedTx)
|
||||
s.Require().NoError(err)
|
||||
|
||||
genTxs[i] = txRaw
|
||||
}
|
||||
|
||||
genUtilGenState.GenTxs = genTxs
|
||||
|
||||
bz, err = cdc.MarshalJSON(&genUtilGenState)
|
||||
s.Require().NoError(err)
|
||||
appGenState[genutiltypes.ModuleName] = bz
|
||||
|
||||
bz, err = json.MarshalIndent(appGenState, "", " ")
|
||||
s.Require().NoError(err)
|
||||
|
||||
genDoc.AppState = bz
|
||||
|
||||
bz, err = cometjson.MarshalIndent(genDoc, "", " ")
|
||||
s.Require().NoError(err)
|
||||
|
||||
// write the updated genesis file to each validator
|
||||
for _, val := range s.chain.validators {
|
||||
writeFile(filepath.Join(val.configDir(), "config", "genesis.json"), bz)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) initValidatorConfigs() {
|
||||
for i, val := range s.chain.validators {
|
||||
tmCfgPath := filepath.Join(val.configDir(), "config", "config.toml")
|
||||
|
||||
vpr := viper.New()
|
||||
vpr.SetConfigFile(tmCfgPath)
|
||||
s.Require().NoError(vpr.ReadInConfig())
|
||||
|
||||
valConfig := cometcfg.DefaultConfig()
|
||||
s.Require().NoError(vpr.Unmarshal(valConfig))
|
||||
|
||||
valConfig.P2P.ListenAddress = "tcp://0.0.0.0:26656"
|
||||
valConfig.P2P.AddrBookStrict = false
|
||||
valConfig.P2P.ExternalAddress = fmt.Sprintf("%s:%d", val.instanceName(), 26656)
|
||||
valConfig.RPC.ListenAddress = "tcp://0.0.0.0:26657"
|
||||
valConfig.StateSync.Enable = false
|
||||
valConfig.LogLevel = "info"
|
||||
|
||||
var peers []string
|
||||
|
||||
for j := 0; j < len(s.chain.validators); j++ {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
|
||||
peer := s.chain.validators[j]
|
||||
peerID := fmt.Sprintf("%s@%s%d:26656", peer.nodeKey.ID(), peer.moniker, j)
|
||||
peers = append(peers, peerID)
|
||||
}
|
||||
|
||||
valConfig.P2P.PersistentPeers = strings.Join(peers, ",")
|
||||
cometcfg.WriteConfigFile(tmCfgPath, valConfig)
|
||||
|
||||
// set application configuration
|
||||
appCfgPath := filepath.Join(val.configDir(), "config", "app.toml")
|
||||
appConfig := srvconfig.DefaultConfig()
|
||||
appConfig.API.Enable = true
|
||||
appConfig.MinGasPrices = minGasPrice
|
||||
|
||||
srvconfig.WriteConfigFile(appCfgPath, appConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *IntegrationTestSuite) runValidators() {
|
||||
s.T().Log("starting POB TestApp validator containers...")
|
||||
|
||||
s.valResources = make([]*dockertest.Resource, len(s.chain.validators))
|
||||
for i, val := range s.chain.validators {
|
||||
runOpts := &dockertest.RunOptions{
|
||||
Name: val.instanceName(),
|
||||
NetworkID: s.dkrNet.Network.ID,
|
||||
Mounts: []string{
|
||||
fmt.Sprintf("%s/:/root/.testapp", val.configDir()),
|
||||
},
|
||||
Repository: "docker.io/skip-mev/pob-e2e",
|
||||
}
|
||||
|
||||
// expose the first validator for debugging and communication
|
||||
if val.index == 0 {
|
||||
runOpts.PortBindings = map[docker.Port][]docker.PortBinding{
|
||||
"1317/tcp": {{HostIP: "", HostPort: "1317"}},
|
||||
"6060/tcp": {{HostIP: "", HostPort: "6060"}},
|
||||
"6061/tcp": {{HostIP: "", HostPort: "6061"}},
|
||||
"6062/tcp": {{HostIP: "", HostPort: "6062"}},
|
||||
"6063/tcp": {{HostIP: "", HostPort: "6063"}},
|
||||
"6064/tcp": {{HostIP: "", HostPort: "6064"}},
|
||||
"6065/tcp": {{HostIP: "", HostPort: "6065"}},
|
||||
"9090/tcp": {{HostIP: "", HostPort: "9090"}},
|
||||
"26656/tcp": {{HostIP: "", HostPort: "26656"}},
|
||||
"26657/tcp": {{HostIP: "", HostPort: "26657"}},
|
||||
}
|
||||
}
|
||||
|
||||
resource, err := s.dkrPool.RunWithOptions(runOpts, noRestart)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.valResources[i] = resource
|
||||
s.T().Logf("started POB TestApp validator container: %s", resource.Container.ID)
|
||||
}
|
||||
|
||||
rpcClient, err := rpchttp.New("tcp://localhost:26657", "/websocket")
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().Eventually(
|
||||
func() bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
status, err := rpcClient.Status(ctx)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// let the node produce a few blocks
|
||||
if status.SyncInfo.CatchingUp || status.SyncInfo.LatestBlockHeight < 3 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
5*time.Minute,
|
||||
time.Second,
|
||||
"POB TestApp node failed to produce blocks",
|
||||
)
|
||||
}
|
||||
|
||||
func noRestart(config *docker.HostConfig) {
|
||||
// in this case we don't want the nodes to restart on failure
|
||||
config.RestartPolicy = docker.RestartPolicy{
|
||||
Name: "no",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
func (s *IntegrationTestSuite) TestTmp() {
|
||||
s.Require().True(true)
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
comettypes "github.com/cometbft/cometbft/types"
|
||||
"github.com/cosmos/cosmos-sdk/server"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/genutil"
|
||||
genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
|
||||
)
|
||||
|
||||
func getGenDoc(path string) (*comettypes.GenesisDoc, error) {
|
||||
serverCtx := server.NewDefaultContext()
|
||||
config := serverCtx.Config
|
||||
config.SetRoot(path)
|
||||
|
||||
genFile := config.GenesisFile()
|
||||
doc := &comettypes.GenesisDoc{}
|
||||
|
||||
if _, err := os.Stat(genFile); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
|
||||
doc, err = comettypes.GenesisDocFromFile(genFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read genesis doc from file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
func addGenesisAccount(path, moniker, amountStr string, accAddr sdk.AccAddress) error {
|
||||
serverCtx := server.NewDefaultContext()
|
||||
config := serverCtx.Config
|
||||
|
||||
config.SetRoot(path)
|
||||
config.Moniker = moniker
|
||||
|
||||
coins, err := sdk.ParseCoinsNormalized(amountStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse coins: %w", err)
|
||||
}
|
||||
|
||||
balances := banktypes.Balance{Address: accAddr.String(), Coins: coins.Sort()}
|
||||
genAccount := authtypes.NewBaseAccount(accAddr, nil, 0, 0)
|
||||
|
||||
genFile := config.GenesisFile()
|
||||
appState, genDoc, err := genutiltypes.GenesisStateFromGenFile(genFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal genesis state: %w", err)
|
||||
}
|
||||
|
||||
authGenState := authtypes.GetGenesisStateFromAppState(cdc, appState)
|
||||
|
||||
accs, err := authtypes.UnpackAccounts(authGenState.Accounts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get accounts from any: %w", err)
|
||||
}
|
||||
|
||||
if accs.Contains(accAddr) {
|
||||
return fmt.Errorf("failed to add account to genesis state; account already exists: %s", accAddr)
|
||||
}
|
||||
|
||||
// Add the new account to the set of genesis accounts and sanitize the
|
||||
// accounts afterwards.
|
||||
accs = append(accs, genAccount)
|
||||
accs = authtypes.SanitizeGenesisAccounts(accs)
|
||||
|
||||
genAccs, err := authtypes.PackAccounts(accs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert accounts into any's: %w", err)
|
||||
}
|
||||
|
||||
authGenState.Accounts = genAccs
|
||||
|
||||
authGenStateBz, err := cdc.MarshalJSON(&authGenState)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal auth genesis state: %w", err)
|
||||
}
|
||||
|
||||
appState[authtypes.ModuleName] = authGenStateBz
|
||||
|
||||
bankGenState := banktypes.GetGenesisStateFromAppState(cdc, appState)
|
||||
bankGenState.Balances = append(bankGenState.Balances, balances)
|
||||
bankGenState.Balances = banktypes.SanitizeGenesisBalances(bankGenState.Balances)
|
||||
|
||||
bankGenStateBz, err := cdc.MarshalJSON(bankGenState)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal bank genesis state: %w", err)
|
||||
}
|
||||
|
||||
appState[banktypes.ModuleName] = bankGenStateBz
|
||||
|
||||
appStateJSON, err := json.Marshal(appState)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal application genesis state: %w", err)
|
||||
}
|
||||
|
||||
genDoc.AppState = appStateJSON
|
||||
return genutil.ExportGenesisFile(genDoc, genFile)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func copyFile(src, dst string) (int64, error) {
|
||||
sourceFileStat, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if !sourceFileStat.Mode().IsRegular() {
|
||||
return 0, fmt.Errorf("%s is not a regular file", src)
|
||||
}
|
||||
|
||||
source, err := os.Open(src)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
destination, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer destination.Close()
|
||||
|
||||
nBytes, err := io.Copy(destination, source)
|
||||
return nBytes, err
|
||||
}
|
||||
|
||||
func writeFile(path string, body []byte) error {
|
||||
_, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(path, body, 0o600)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"github.com/cosmos/go-bip39"
|
||||
)
|
||||
|
||||
func createMnemonic() (string, error) {
|
||||
entropySeed, err := bip39.NewEntropy(256)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mnemonic, err := bip39.NewMnemonic(entropySeed)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return mnemonic, nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/codec/unknownproto"
|
||||
sdktx "github.com/cosmos/cosmos-sdk/types/tx"
|
||||
)
|
||||
|
||||
func decodeTx(txBytes []byte) (*sdktx.Tx, error) {
|
||||
var raw sdktx.TxRaw
|
||||
|
||||
// reject all unknown proto fields in the root TxRaw
|
||||
err := unknownproto.RejectUnknownFieldsStrict(txBytes, &raw, encodingConfig.InterfaceRegistry)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to reject unknown fields: %w", err)
|
||||
}
|
||||
|
||||
if err := cdc.Unmarshal(txBytes, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var body sdktx.TxBody
|
||||
if err := cdc.Unmarshal(raw.BodyBytes, &body); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode tx: %w", err)
|
||||
}
|
||||
|
||||
var authInfo sdktx.AuthInfo
|
||||
|
||||
// reject all unknown proto fields in AuthInfo
|
||||
err = unknownproto.RejectUnknownFieldsStrict(raw.AuthInfoBytes, &authInfo, encodingConfig.InterfaceRegistry)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to reject unknown fields: %w", err)
|
||||
}
|
||||
|
||||
if err := cdc.Unmarshal(raw.AuthInfoBytes, &authInfo); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode auth info: %w", err)
|
||||
}
|
||||
|
||||
return &sdktx.Tx{
|
||||
Body: &body,
|
||||
AuthInfo: &authInfo,
|
||||
Signatures: raw.Signatures,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
cometcfg "github.com/cometbft/cometbft/config"
|
||||
"github.com/cometbft/cometbft/p2p"
|
||||
"github.com/cometbft/cometbft/privval"
|
||||
sdkcrypto "github.com/cosmos/cosmos-sdk/crypto"
|
||||
cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
|
||||
"github.com/cosmos/cosmos-sdk/crypto/hd"
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keyring"
|
||||
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
|
||||
"github.com/cosmos/cosmos-sdk/server"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdktx "github.com/cosmos/cosmos-sdk/types/tx"
|
||||
txsigning "github.com/cosmos/cosmos-sdk/types/tx/signing"
|
||||
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
|
||||
"github.com/cosmos/cosmos-sdk/x/genutil"
|
||||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||
"github.com/skip-mev/pob/tests/app"
|
||||
)
|
||||
|
||||
type validator struct {
|
||||
chain *chain
|
||||
index int
|
||||
moniker string
|
||||
mnemonic string
|
||||
keyInfo keyring.Record
|
||||
privateKey cryptotypes.PrivKey
|
||||
consensusKey privval.FilePVKey
|
||||
nodeKey p2p.NodeKey
|
||||
}
|
||||
|
||||
func (v *validator) instanceName() string {
|
||||
return fmt.Sprintf("%s%d", v.moniker, v.index)
|
||||
}
|
||||
|
||||
func (v *validator) configDir() string {
|
||||
return fmt.Sprintf("%s/%s", v.chain.configDir(), v.instanceName())
|
||||
}
|
||||
|
||||
func (v *validator) createConfig() error {
|
||||
p := path.Join(v.configDir(), "config")
|
||||
return os.MkdirAll(p, 0o755)
|
||||
}
|
||||
|
||||
func (v *validator) init() error {
|
||||
if err := v.createConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serverCtx := server.NewDefaultContext()
|
||||
config := serverCtx.Config
|
||||
|
||||
config.SetRoot(v.configDir())
|
||||
config.Moniker = v.moniker
|
||||
|
||||
genDoc, err := getGenDoc(v.configDir())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
appState, err := json.MarshalIndent(app.ModuleBasics.DefaultGenesis(cdc), "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to JSON encode app genesis state: %w", err)
|
||||
}
|
||||
|
||||
genDoc.ChainID = v.chain.id
|
||||
genDoc.Validators = nil
|
||||
genDoc.AppState = appState
|
||||
|
||||
if err = genutil.ExportGenesisFile(genDoc, config.GenesisFile()); err != nil {
|
||||
return fmt.Errorf("failed to export app genesis state: %w", err)
|
||||
}
|
||||
|
||||
cometcfg.WriteConfigFile(filepath.Join(config.RootDir, "config", "config.toml"), config)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *validator) createNodeKey() error {
|
||||
serverCtx := server.NewDefaultContext()
|
||||
config := serverCtx.Config
|
||||
|
||||
config.SetRoot(v.configDir())
|
||||
config.Moniker = v.moniker
|
||||
|
||||
nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.nodeKey = *nodeKey
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *validator) createConsensusKey() error {
|
||||
serverCtx := server.NewDefaultContext()
|
||||
config := serverCtx.Config
|
||||
|
||||
config.SetRoot(v.configDir())
|
||||
config.Moniker = v.moniker
|
||||
|
||||
pvKeyFile := config.PrivValidatorKeyFile()
|
||||
if err := os.MkdirAll(filepath.Dir(pvKeyFile), 0o777); err != nil {
|
||||
return fmt.Errorf("could not create directory %q: %w", filepath.Dir(pvKeyFile), err)
|
||||
}
|
||||
|
||||
pvStateFile := config.PrivValidatorStateFile()
|
||||
if err := os.MkdirAll(filepath.Dir(pvStateFile), 0o777); err != nil {
|
||||
return fmt.Errorf("could not create directory %q: %w", filepath.Dir(pvStateFile), err)
|
||||
}
|
||||
|
||||
filePV := privval.LoadOrGenFilePV(pvKeyFile, pvStateFile)
|
||||
v.consensusKey = filePV.Key
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *validator) createKeyFromMnemonic(name, mnemonic string) error {
|
||||
kb, err := keyring.New(keyringAppName, keyring.BackendTest, v.configDir(), nil, cdc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyringAlgos, _ := kb.SupportedAlgorithms()
|
||||
algo, err := keyring.NewSigningAlgoFromString(string(hd.Secp256k1Type), keyringAlgos)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info, err := kb.NewAccount(name, mnemonic, "", sdk.FullFundraiserPath, algo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
privKeyArmor, err := kb.ExportPrivKeyArmor(name, keyringPassphrase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
privKey, _, err := sdkcrypto.UnarmorDecryptPrivKey(privKeyArmor, keyringPassphrase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.keyInfo = *info
|
||||
v.mnemonic = mnemonic
|
||||
v.privateKey = privKey
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *validator) createKey(name string) error {
|
||||
mnemonic, err := createMnemonic()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return v.createKeyFromMnemonic(name, mnemonic)
|
||||
}
|
||||
|
||||
func (v *validator) buildCreateValidatorMsg(amount sdk.Coin) (sdk.Msg, error) {
|
||||
description := stakingtypes.NewDescription(v.moniker, "", "", "", "")
|
||||
commissionRates := stakingtypes.CommissionRates{
|
||||
Rate: sdk.MustNewDecFromStr("0.1"),
|
||||
MaxRate: sdk.MustNewDecFromStr("0.2"),
|
||||
MaxChangeRate: sdk.MustNewDecFromStr("0.01"),
|
||||
}
|
||||
|
||||
// get the initial validator min self delegation
|
||||
minSelfDelegation, _ := sdk.NewIntFromString("1")
|
||||
|
||||
valPubKey, err := cryptocodec.FromTmPubKeyInterface(v.consensusKey.PubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
valAddr, err := v.keyInfo.GetAddress()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stakingtypes.NewMsgCreateValidator(
|
||||
sdk.ValAddress(valAddr),
|
||||
valPubKey,
|
||||
amount,
|
||||
description,
|
||||
commissionRates,
|
||||
minSelfDelegation,
|
||||
)
|
||||
}
|
||||
|
||||
func (v *validator) signMsg(msgs ...sdk.Msg) (*sdktx.Tx, error) {
|
||||
txBuilder := encodingConfig.TxConfig.NewTxBuilder()
|
||||
|
||||
if err := txBuilder.SetMsgs(msgs...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txBuilder.SetMemo(fmt.Sprintf("%s@%s:26656", v.nodeKey.ID(), v.instanceName()))
|
||||
txBuilder.SetFeeAmount(sdk.NewCoins())
|
||||
txBuilder.SetGasLimit(200_000)
|
||||
|
||||
signerData := authsigning.SignerData{
|
||||
ChainID: v.chain.id,
|
||||
AccountNumber: 0,
|
||||
Sequence: 0,
|
||||
}
|
||||
|
||||
// For SIGN_MODE_DIRECT, calling SetSignatures calls setSignerInfos on
|
||||
// TxBuilder under the hood, and SignerInfos is needed to generate the sign
|
||||
// bytes. This is the reason for setting SetSignatures here, with a nil
|
||||
// signature.
|
||||
//
|
||||
// Note: This line is not needed for SIGN_MODE_LEGACY_AMINO, but putting it
|
||||
// also doesn't affect its generated sign bytes, so for code's simplicity
|
||||
// sake, we put it here.
|
||||
pubKey, err := v.keyInfo.GetPubKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sig := txsigning.SignatureV2{
|
||||
PubKey: pubKey,
|
||||
Data: &txsigning.SingleSignatureData{
|
||||
SignMode: txsigning.SignMode_SIGN_MODE_DIRECT,
|
||||
Signature: nil,
|
||||
},
|
||||
Sequence: 0,
|
||||
}
|
||||
|
||||
if err := txBuilder.SetSignatures(sig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bytesToSign, err := encodingConfig.TxConfig.SignModeHandler().GetSignBytes(
|
||||
txsigning.SignMode_SIGN_MODE_DIRECT,
|
||||
signerData,
|
||||
txBuilder.GetTx(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sigBytes, err := v.privateKey.Sign(bytesToSign)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sig = txsigning.SignatureV2{
|
||||
PubKey: pubKey,
|
||||
Data: &txsigning.SingleSignatureData{
|
||||
SignMode: txsigning.SignMode_SIGN_MODE_DIRECT,
|
||||
Signature: sigBytes,
|
||||
},
|
||||
Sequence: 0,
|
||||
}
|
||||
if err := txBuilder.SetSignatures(sig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signedTx := txBuilder.GetTx()
|
||||
bz, err := encodingConfig.TxConfig.TxEncoder()(signedTx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return decodeTx(bz)
|
||||
}
|
||||
Reference in New Issue
Block a user