feat: add testutil/testnet package (#15655)

Co-authored-by: Marko <marbar3778@yahoo.com>
This commit is contained in:
Mark Rushakoff 2023-04-05 15:16:45 -04:00 committed by GitHub
parent e2a8f7ca32
commit 68af247459
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1728 additions and 0 deletions

View File

@ -0,0 +1,144 @@
package testnet_test
import (
"fmt"
"math/rand"
"net"
"testing"
"time"
"cosmossdk.io/log"
"cosmossdk.io/simapp"
cmtcfg "github.com/cometbft/cometbft/config"
dbm "github.com/cosmos/cosmos-db"
"github.com/cosmos/cosmos-sdk/baseapp"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
"github.com/cosmos/cosmos-sdk/testutil/testnet"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
)
// Use a limited set of available ports to ensure that
// retries eventually land on a free port.
func TestCometStarter_PortContention(t *testing.T) {
if testing.Short() {
t.Skip("skipping long test in short mode")
}
const nVals = 4
// Find n+1 addresses that should be free.
// Ephemeral port range should start at about 49k+
// according to `sysctl net.inet.ip.portrange` on macOS,
// and at about 32k+ on Linux
// according to `sysctl net.ipv4.ip_local_port_range`.
//
// Because we attempt to find free addresses outside that range,
// it is unlikely that another process will claim a port
// we discover to be free, during the time this test runs.
const portSeekStart = 19000
reuseAddrs := make([]string, 0, nVals+1)
for i := portSeekStart; i < portSeekStart+1000; i++ {
addr := fmt.Sprintf("127.0.0.1:%d", i)
ln, err := net.Listen("tcp", addr)
if err != nil {
// No need to log the failure.
continue
}
// If the port was free, append it to our reusable addresses.
reuseAddrs = append(reuseAddrs, "tcp://"+addr)
_ = ln.Close()
if len(reuseAddrs) == nVals+1 {
break
}
}
if len(reuseAddrs) != nVals+1 {
t.Fatalf("needed %d free ports but only found %d", nVals+1, len(reuseAddrs))
}
// Now that we have one more port than the number of validators,
// there is a good chance that picking a random port will conflict with a previously chosen one.
// But since CometStarter retries several times,
// it should eventually land on a free port.
valPKs := testnet.NewValidatorPrivKeys(nVals)
cmtVals := valPKs.CometGenesisValidators()
stakingVals := cmtVals.StakingValidators()
const chainID = "simapp-cometstarter"
b := testnet.DefaultGenesisBuilderOnlyValidators(
chainID,
stakingVals,
sdk.NewCoin(sdk.DefaultBondDenom, sdk.DefaultPowerReduction),
)
jGenesis := b.Encode()
// Use an info-level logger, because the debug logs in comet are noisy
// and there is a data race in comet debug logs,
// due to be fixed in v0.37.1 which is not yet released:
// https://github.com/cometbft/cometbft/pull/532
logger := log.NewTestLoggerInfo(t)
const nRuns = 4
for i := 0; i < nRuns; i++ {
t.Run(fmt.Sprintf("attempt %d", i), func(t *testing.T) {
nodes, err := testnet.NewNetwork(nVals, func(idx int) *testnet.CometStarter {
rootDir := t.TempDir()
app := simapp.NewSimApp(
logger.With("instance", idx),
dbm.NewMemDB(),
nil,
true,
simtestutil.NewAppOptionsWithFlagHome(rootDir),
baseapp.SetChainID(chainID),
)
cfg := cmtcfg.DefaultConfig()
// memdb is sufficient for this test.
cfg.BaseConfig.DBBackend = "memdb"
return testnet.NewCometStarter(
app,
cfg,
valPKs[idx].Val,
jGenesis,
rootDir,
).
Logger(logger.With("rootmodule", fmt.Sprintf("comet_node-%d", idx))).
TCPAddrChooser(func() string {
// This chooser function is the key of this test,
// where there is only one more available address than there are nodes.
// Therefore it is likely that an address will already be in use,
// thereby exercising the address-in-use retry.
return reuseAddrs[rand.Intn(len(reuseAddrs))]
})
})
defer nodes.StopAndWait()
require.NoError(t, err)
heightAdvanced := false
for j := 0; j < 40; j++ {
cs := nodes[0].ConsensusState()
if cs.GetLastHeight() < 2 {
time.Sleep(250 * time.Millisecond)
continue
}
// Saw height advance.
heightAdvanced = true
break
}
if !heightAdvanced {
t.Fatalf("consensus height did not advance in approximately 10 seconds")
}
})
}
}

View File

@ -0,0 +1,10 @@
// Package testnet contains tests for
// [github.com/cosmos/cosmos-sdk/testutil/testnet].
//
// Eventually all of these tests will move into that package,
// but that is currently blocked on having a minimal app defined
// in the root cosmos-sdk Go module.
// Once that app is available, the contents of this package
// will be moved to testutil/testnet,
// and references to SimApp will be replaced by the minimal app.
package testnet

View File

@ -0,0 +1,112 @@
package testnet_test
import (
"fmt"
"os"
"path/filepath"
"cosmossdk.io/log"
"cosmossdk.io/simapp"
cmtcfg "github.com/cometbft/cometbft/config"
dbm "github.com/cosmos/cosmos-db"
"github.com/cosmos/cosmos-sdk/baseapp"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
"github.com/cosmos/cosmos-sdk/testutil/testnet"
sdk "github.com/cosmos/cosmos-sdk/types"
)
func Example_basicUsage() {
const nVals = 2
// Set up new private keys for the set of validators.
valPKs := testnet.NewValidatorPrivKeys(nVals)
// Comet-style validators.
cmtVals := valPKs.CometGenesisValidators()
// Cosmos SDK staking validators for genesis.
stakingVals := cmtVals.StakingValidators()
const chainID = "example-basic"
// Create a genesis builder that only requires validators,
// without any separate delegator accounts.
//
// If you need further customization, start with testnet.NewGenesisBuilder().
b := testnet.DefaultGenesisBuilderOnlyValidators(
chainID,
stakingVals,
// The amount to use in each validator's account during gentx.
sdk.NewCoin(sdk.DefaultBondDenom, sdk.DefaultPowerReduction),
)
// JSON-formatted genesis.
jGenesis := b.Encode()
// In this example, we have an outer root directory for the validators.
// Use t.TempDir() in tests.
rootDir, err := os.MkdirTemp("", "testnet-example-")
if err != nil {
panic(err)
}
defer os.RemoveAll(rootDir)
// In tests, you probably want to use log.NewTestLoggerInfo(t).
logger := log.NewNopLogger()
// The NewNetwork function creates a network of validators.
// We have to provide a callback to return CometStarter instances.
// NewNetwork will start all the comet instances concurrently
// and join the nodes together.
nodes, err := testnet.NewNetwork(nVals, func(idx int) *testnet.CometStarter {
// Make a new directory for the validator being created.
// In tests, this would be a simpler call to t.TempDir().
dir := filepath.Join(rootDir, fmt.Sprintf("val-%d", idx))
if err := os.Mkdir(dir, 0o755); err != nil {
panic(err)
}
// TODO: use a different minimal app for this.
app := simapp.NewSimApp(
logger.With("instance", idx),
dbm.NewMemDB(),
nil,
true,
simtestutil.NewAppOptionsWithFlagHome(rootDir),
baseapp.SetChainID(chainID),
)
// Each CometStarter instance must be associated with
// a distinct comet Config object,
// as the CometStarter will automatically modify some fields,
// including P2P.ListenAddress.
cfg := cmtcfg.DefaultConfig()
// No need to persist comet's DB to disk in this example.
cfg.BaseConfig.DBBackend = "memdb"
return testnet.NewCometStarter(
app,
cfg,
valPKs[idx].Val, // Validator private key for this comet instance.
jGenesis, // Raw bytes of genesis file.
dir, // Where to put files on disk.
).Logger(logger.With("root_module", fmt.Sprintf("comet_%d", idx)))
})
// StopAndWait must be deferred before the error check,
// as the nodes value may contain some successfully started instances.
defer nodes.StopAndWait()
if err != nil {
panic(err)
}
// Now you can begin interacting with the nodes.
// For the sake of this example, we'll just check
// a couple simple properties of one node.
fmt.Println(nodes[0].IsListening())
fmt.Println(nodes[0].GenesisDoc().ChainID)
// Output:
// true
// example-basic
}

View File

@ -0,0 +1,213 @@
package testnet
import (
"errors"
"fmt"
"net"
"os"
"path/filepath"
"syscall"
"cosmossdk.io/log"
abcitypes "github.com/cometbft/cometbft/abci/types"
cmtcfg "github.com/cometbft/cometbft/config"
cmted25519 "github.com/cometbft/cometbft/crypto/ed25519"
"github.com/cometbft/cometbft/node"
"github.com/cometbft/cometbft/p2p"
"github.com/cometbft/cometbft/privval"
"github.com/cometbft/cometbft/proxy"
cmttypes "github.com/cometbft/cometbft/types"
servercmtlog "github.com/cosmos/cosmos-sdk/server/log"
genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
)
// CometStarter offers a builder-pattern interface to
// starting a Comet instance with an ABCI application running alongside.
//
// As CometStart is more broadly used in the codebase,
// the number of available methods on CometStarter will grow.
type CometStarter struct {
logger log.Logger
app abcitypes.Application
cfg *cmtcfg.Config
valPrivKey cmted25519.PrivKey
genesis []byte
rootDir string
tcpAddrChooser func() string
startTries int
}
// NewCometStarter accepts a minimal set of arguments to start comet with an ABCI app.
// For further configuration, chain other CometStarter methods before calling Start:
//
// NewCometStarter(...).Logger(...).Start()
func NewCometStarter(
app abcitypes.Application,
cfg *cmtcfg.Config,
valPrivKey cmted25519.PrivKey,
genesis []byte,
rootDir string,
) *CometStarter {
cfg.SetRoot(rootDir)
// CometStarter won't work without these settings,
// so set them unconditionally.
cfg.P2P.AllowDuplicateIP = true
cfg.P2P.AddrBookStrict = false
// For now, we disallow RPC listening.
// Comet v0.37 uses a global value such that multiple comet nodes in one process
// end up contending over one "rpc environment" and only the last-started validator
// will control the RPC service.
//
// The "rpc environment" was removed as a global in
// https://github.com/cometbft/cometbft/commit/3324f49fb7e7b40189726746493e83b82a61b558
// which is due to land in v0.38.
//
// At that point, we should keep the default as RPC off,
// but we should add a RPCListen method to opt in to enabling it.
cfg.RPC.ListenAddress = ""
// defaultStartTries is somewhat arbitrary.
// Occasionally TestCometStarter_PortContention would fail with 10 tries,
// and bumping it up to 12 makes it almost never fail.
const defaultStartTries = 12
return &CometStarter{
logger: log.NewNopLogger(),
app: app,
cfg: cfg,
genesis: genesis,
valPrivKey: valPrivKey,
rootDir: rootDir,
startTries: defaultStartTries,
}
}
// Logger sets the logger for s and for the eventual started comet instance.
func (s *CometStarter) Logger(logger log.Logger) *CometStarter {
s.logger = logger
return s
}
// Start returns a started Comet node.
func (s *CometStarter) Start() (*node.Node, error) {
fpv, nodeKey, err := s.initDisk()
if err != nil {
return nil, err
}
appGenesisProvider := func() (*cmttypes.GenesisDoc, error) {
appGenesis, err := genutiltypes.AppGenesisFromFile(s.cfg.GenesisFile())
if err != nil {
return nil, err
}
return appGenesis.ToGenesisDoc()
}
for i := 0; i < s.startTries; i++ {
s.cfg.P2P.ListenAddress = s.likelyAvailableAddress()
n, err := node.NewNode(
s.cfg,
fpv,
nodeKey,
proxy.NewLocalClientCreator(s.app),
appGenesisProvider,
node.DefaultDBProvider,
node.DefaultMetricsProvider(s.cfg.Instrumentation),
servercmtlog.CometZeroLogWrapper{Logger: s.logger},
)
if err != nil {
return nil, fmt.Errorf("failed to create comet node: %w", err)
}
err = n.Start()
if err == nil {
return n, nil
}
// Error isn't nil -- if it is EADDRINUSE then we can try again.
if errors.Is(err, syscall.EADDRINUSE) {
continue
}
// Non-nil error that isn't EADDRINUSE, just return the error.
return nil, err
}
// If we didn't return a node from inside the loop,
// then we must have exhausted our try limit.
return nil, fmt.Errorf("failed to start a comet node within %d tries", s.startTries)
}
// initDisk creates the config and data directories on disk,
// and other required files, so that comet and the validator work correctly.
// It also generates a node key for validators.
func (s *CometStarter) initDisk() (cmttypes.PrivValidator, *p2p.NodeKey, error) {
if err := os.MkdirAll(filepath.Join(s.rootDir, "config"), 0o750); err != nil {
return nil, nil, fmt.Errorf("failed to make config directory: %w", err)
}
if err := os.MkdirAll(filepath.Join(s.rootDir, "data"), 0o750); err != nil {
return nil, nil, fmt.Errorf("failed to make data directory: %w", err)
}
fpv := privval.NewFilePV(s.valPrivKey, s.cfg.PrivValidatorKeyFile(), s.cfg.PrivValidatorStateFile())
fpv.Save()
if err := os.WriteFile(s.cfg.GenesisFile(), s.genesis, 0600); err != nil {
return nil, nil, fmt.Errorf("failed to write genesis file: %w", err)
}
nodeKey, err := p2p.LoadOrGenNodeKey(s.cfg.NodeKeyFile())
if err != nil {
return nil, nil, err
}
return fpv, nodeKey, nil
}
// TCPAddrChooser sets the function to use when selecting a (likely to be free)
// TCP address for comet's P2P port.
//
// This should only be used when testing CometStarter.
//
// It must return a string in format "tcp://IP:PORT".
func (s *CometStarter) TCPAddrChooser(fn func() string) *CometStarter {
s.tcpAddrChooser = fn
return s
}
// likelyAvailableAddress provides a TCP address that is likely to be available
// for comet or other processes to listen on.
//
// Generally, it is better to directly provide a net.Listener that is already bound to an address,
// but unfortunately comet does not offer that as part of its API.
// Instead, we locally bind to :0 and then report that as a "likely available" port.
// If another process steals that port before our comet instance can bind to it,
// the Start method handles retries.
func (s *CometStarter) likelyAvailableAddress() string {
// If s.TCPAddrChooser was called, use that implementation.
if s.tcpAddrChooser != nil {
return s.tcpAddrChooser()
}
// Fall back to attempting a random port.
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
panic(fmt.Errorf("failed to bind to random port: %w", err))
}
defer ln.Close()
return "tcp://" + ln.Addr().String()
}

View File

@ -0,0 +1,59 @@
package testnet
import (
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
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"
)
// DelegatorPrivKeys is a slice of secp256k1.PrivKey.
type DelegatorPrivKeys []*secp256k1.PrivKey
// NewDelegatorPrivKeysreturns a DelegatorPrivKeys of length n,
// where each set of keys is dynamically generated.
func NewDelegatorPrivKeys(n int) DelegatorPrivKeys {
dpk := make(DelegatorPrivKeys, n)
for i := range dpk {
dpk[i] = secp256k1.GenPrivKey()
}
return dpk
}
// BaseAccounts returns the base accounts corresponding to the delegators' public keys.
func (dpk DelegatorPrivKeys) BaseAccounts() BaseAccounts {
ba := make(BaseAccounts, len(dpk))
for i, pk := range dpk {
pubKey := pk.PubKey()
const accountNumber = 0
const sequenceNumber = 0
ba[i] = authtypes.NewBaseAccount(
pubKey.Address().Bytes(), pubKey, accountNumber, sequenceNumber,
)
}
return ba
}
// BaseAccounts is a slice of [*authtypes.BaseAccount].
type BaseAccounts []*authtypes.BaseAccount
// Balances creates a slice of [banktypes.Balance] for each account in ba,
// where each balance has an identical Coins value of the singleBalance argument.
func (ba BaseAccounts) Balances(singleBalance sdk.Coins) []banktypes.Balance {
balances := make([]banktypes.Balance, len(ba))
for i, b := range ba {
balances[i] = banktypes.Balance{
Address: b.GetAddress().String(),
Coins: singleBalance,
}
}
return balances
}

4
testutil/testnet/doc.go Normal file
View File

@ -0,0 +1,4 @@
// Package testnet provides APIs for easily create and configure
// validators, genesis files, and comet instances,
// to support testing app chain instances in-process.
package testnet

528
testutil/testnet/genesis.go Normal file
View File

@ -0,0 +1,528 @@
package testnet
import (
"encoding/json"
"fmt"
"strconv"
"time"
cmttypes "github.com/cometbft/cometbft/types"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
"github.com/cosmos/cosmos-sdk/x/auth/tx"
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
consensusparamtypes "github.com/cosmos/cosmos-sdk/x/consensus/types"
distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
)
// GenesisBuilder enables constructing a genesis file,
// following a builder pattern.
//
// None of the methods on GenesisBuilder return an error,
// choosing instead to panic.
// GenesisBuilder is only intended for use in tests,
// where inputs are predetermined and expected to succeed.
type GenesisBuilder struct {
amino *codec.LegacyAmino
codec *codec.ProtoCodec
// The value used in ChainID.
// Some other require this value,
// so store it as a field instead of re-parsing it from JSON.
chainID string
// The outer JSON object.
// Most data goes into app_state, but there are some top-level fields.
outer map[string]json.RawMessage
// Many of GenesisBuilder's methods operate on the app_state JSON object,
// so we track that separately and nest it inside outer upon a call to JSON().
appState map[string]json.RawMessage
gentxs []sdk.Tx
}
// NewGenesisBuilder returns an initialized GenesisBuilder.
//
// The returned GenesisBuilder has an initial height of 1
// and a genesis_time of the current time when the function was called.
func NewGenesisBuilder() *GenesisBuilder {
ir := codectypes.NewInterfaceRegistry()
cryptocodec.RegisterInterfaces(ir)
stakingtypes.RegisterInterfaces(ir)
banktypes.RegisterInterfaces(ir)
authtypes.RegisterInterfaces(ir)
pCodec := codec.NewProtoCodec(ir)
return &GenesisBuilder{
amino: codec.NewLegacyAmino(),
codec: pCodec,
outer: map[string]json.RawMessage{
"initial_height": json.RawMessage(`"1"`),
"genesis_time": json.RawMessage(
strconv.AppendQuote(nil, time.Now().UTC().Format(time.RFC3339Nano)),
),
},
appState: map[string]json.RawMessage{},
}
}
// GenTx emulates the gentx CLI, creating a message to create a validator
// represented by val, with "amount" self delegation,
// and signed by privVal.
func (b *GenesisBuilder) GenTx(privVal secp256k1.PrivKey, val cmttypes.GenesisValidator, amount sdk.Coin) *GenesisBuilder {
if b.chainID == "" {
panic(fmt.Errorf("(*GenesisBuilder).GenTx must not be called before (*GenesisBuilder).ChainID"))
}
pubKey, err := cryptocodec.FromCmtPubKeyInterface(val.PubKey)
if err != nil {
panic(err)
}
// Produce the create validator message.
msg, err := stakingtypes.NewMsgCreateValidator(
privVal.PubKey().Address().Bytes(),
pubKey,
amount,
stakingtypes.Description{
Moniker: "TODO",
},
stakingtypes.CommissionRates{
Rate: sdk.MustNewDecFromStr("0.1"),
MaxRate: sdk.MustNewDecFromStr("0.2"),
MaxChangeRate: sdk.MustNewDecFromStr("0.01"),
},
sdk.OneInt(),
)
if err != nil {
panic(err)
}
valAddr, err := sdk.ValAddressFromBech32(msg.ValidatorAddress)
if err != nil {
panic(err)
}
msg.DelegatorAddress = sdk.AccAddress(valAddr).String()
if err := msg.ValidateBasic(); err != nil {
panic(err)
}
txConf := authtx.NewTxConfig(b.codec, tx.DefaultSignModes)
txb := txConf.NewTxBuilder()
if err := txb.SetMsgs(msg); err != nil {
panic(err)
}
const signMode = signing.SignMode_SIGN_MODE_DIRECT
// Need to set the signature object on the tx builder first,
// otherwise we end up signing a different total message
// compared to what gets eventually verified.
if err := txb.SetSignatures(
signing.SignatureV2{
PubKey: privVal.PubKey(),
Data: &signing.SingleSignatureData{
SignMode: signMode,
},
},
); err != nil {
panic(err)
}
// Generate bytes to be signed.
bytesToSign, err := txConf.SignModeHandler().GetSignBytes(
signing.SignMode_SIGN_MODE_DIRECT,
authsigning.SignerData{
ChainID: b.chainID,
PubKey: privVal.PubKey(),
Address: sdk.MustBech32ifyAddressBytes("cosmos", privVal.PubKey().Address()), // TODO: don't hardcode cosmos1!
// No account or sequence number for gentx.
},
txb.GetTx(),
)
if err != nil {
panic(err)
}
// Produce the signature.
signed, err := privVal.Sign(bytesToSign)
if err != nil {
panic(err)
}
// Set the signature on the builder.
if err := txb.SetSignatures(
signing.SignatureV2{
PubKey: privVal.PubKey(),
Data: &signing.SingleSignatureData{
SignMode: signMode,
Signature: signed,
},
},
); err != nil {
panic(err)
}
b.gentxs = append(b.gentxs, txb.GetTx())
return b
}
// ChainID sets the genesis's "chain_id" field.
func (b *GenesisBuilder) ChainID(id string) *GenesisBuilder {
b.chainID = id
var err error
b.outer["chain_id"], err = json.Marshal(id)
if err != nil {
panic(err)
}
return b
}
// GenesisTime sets the genesis's "genesis_time" field.
// Note that [NewGenesisBuilder] sets the genesis time to the current time by default.
func (b *GenesisBuilder) GenesisTime(t time.Time) *GenesisBuilder {
var err error
b.outer["genesis_time"], err = json.Marshal(t.Format(time.RFC3339Nano))
if err != nil {
panic(err)
}
return b
}
// InitialHeight sets the genesis's "initial_height" field to h.
// Note that [NewGenesisBuilder] sets the initial height to 1 by default.
func (b *GenesisBuilder) InitialHeight(h int64) *GenesisBuilder {
var err error
b.outer["initial_height"], err = json.Marshal(strconv.FormatInt(h, 10))
if err != nil {
panic(err)
}
return b
}
// AuthParams sets the auth params on the genesis.
func (b *GenesisBuilder) AuthParams(params authtypes.Params) *GenesisBuilder {
var err error
b.appState[authtypes.ModuleName], err = json.Marshal(map[string]any{
"params": params,
})
if err != nil {
panic(err)
}
return b
}
// DefaultAuthParams calls b.AuthParams with [authtypes.DefaultParams],
// as a convenience so that callers do not have to import the authtypes package.
func (b *GenesisBuilder) DefaultAuthParams() *GenesisBuilder {
return b.AuthParams(authtypes.DefaultParams())
}
// Consensus sets the consensus parameters and initial validators.
//
// If params is nil, [cmttypes.DefaultConsensusParams] is used.
func (b *GenesisBuilder) Consensus(params *cmttypes.ConsensusParams, vals CometGenesisValidators) *GenesisBuilder {
if params == nil {
params = cmttypes.DefaultConsensusParams()
}
var err error
b.outer[consensusparamtypes.ModuleName], err = (&genutiltypes.ConsensusGenesis{
Params: params,
Validators: vals.ToComet(),
}).MarshalJSON()
if err != nil {
panic(err)
}
return b
}
// Staking sets the staking parameters, validators, and delegations on the genesis.
//
// This also modifies the bank state's balances to include the bonded pool balance.
func (b *GenesisBuilder) Staking(
params stakingtypes.Params,
vals StakingValidators,
delegations []stakingtypes.Delegation,
) *GenesisBuilder {
var err error
b.appState[stakingtypes.ModuleName], err = b.codec.MarshalJSON(
stakingtypes.NewGenesisState(params, vals.ToStakingType(), delegations),
)
if err != nil {
panic(err)
}
// Modify bank state for bonded pool.
var coins sdk.Coins
for _, v := range vals {
coins = coins.Add(sdk.NewCoin(sdk.DefaultBondDenom, v.V.Tokens))
}
bondedPoolBalance := banktypes.Balance{
Address: authtypes.NewModuleAddress(stakingtypes.BondedPoolName).String(),
Coins: coins,
}
// get bank types genesis, add account
bankGenesis := banktypes.GetGenesisStateFromAppState(b.codec, b.appState)
bankGenesis.Balances = append(bankGenesis.Balances, bondedPoolBalance)
b.appState[banktypes.ModuleName], err = b.codec.MarshalJSON(bankGenesis)
if err != nil {
panic(err)
}
return b
}
// StakingWithDefaultParams calls b.Staking, providing [stakingtypes.DefaultParams]
// so that callers don't necessarily have to import [stakingtypes].
func (b *GenesisBuilder) StakingWithDefaultParams(vals StakingValidators, delegations []stakingtypes.Delegation) *GenesisBuilder {
return b.Staking(stakingtypes.DefaultParams(), vals, delegations)
}
// DefaultStaking is shorthand for b.StakingWithDefaultParams with nil validators and delegations.
func (b *GenesisBuilder) DefaultStaking() *GenesisBuilder {
return b.StakingWithDefaultParams(nil, nil)
}
// Banking sets the banking genesis state.
func (b *GenesisBuilder) Banking(
params banktypes.Params,
balances []banktypes.Balance,
totalSupply sdk.Coins,
denomMetadata []banktypes.Metadata,
sendEnabled []banktypes.SendEnabled,
) *GenesisBuilder {
var err error
b.appState[banktypes.ModuleName], err = b.codec.MarshalJSON(
banktypes.NewGenesisState(
params,
balances,
totalSupply,
denomMetadata,
sendEnabled,
),
)
if err != nil {
panic(err)
}
return b
}
// BankingWithDefaultParams calls b.Banking with [banktypes.DefaultParams],
// so that callers don't necessarily have to import [banktypes].
func (b *GenesisBuilder) BankingWithDefaultParams(
balances []banktypes.Balance,
totalSupply sdk.Coins,
denomMetadata []banktypes.Metadata,
sendEnabled []banktypes.SendEnabled,
) *GenesisBuilder {
return b.Banking(
banktypes.DefaultParams(),
balances,
totalSupply,
denomMetadata,
sendEnabled,
)
}
// Mint sets the mint genesis state.
func (b *GenesisBuilder) Mint(m minttypes.Minter, p minttypes.Params) *GenesisBuilder {
var err error
b.appState[minttypes.ModuleName], err = b.codec.MarshalJSON(
minttypes.NewGenesisState(m, p),
)
if err != nil {
panic(err)
}
return b
}
// DefaultMint calls b.Mint with [minttypes.DefaultInitialMinter] and [minttypes.DefaultParams].
func (b *GenesisBuilder) DefaultMint() *GenesisBuilder {
return b.Mint(minttypes.DefaultInitialMinter(), minttypes.DefaultParams())
}
// Slashing sets the slashing genesis state.
func (b *GenesisBuilder) Slashing(
params slashingtypes.Params,
si []slashingtypes.SigningInfo,
mb []slashingtypes.ValidatorMissedBlocks,
) *GenesisBuilder {
var err error
b.appState[slashingtypes.ModuleName], err = b.codec.MarshalJSON(
slashingtypes.NewGenesisState(params, si, mb),
)
if err != nil {
panic(err)
}
return b
}
// SlashingWithDefaultParams calls b.Slashing with [slashingtypes.DefaultParams],
// so that callers don't necessarily have to import [slashingtypes].
func (b *GenesisBuilder) SlashingWithDefaultParams(
si []slashingtypes.SigningInfo,
mb []slashingtypes.ValidatorMissedBlocks,
) *GenesisBuilder {
return b.Slashing(slashingtypes.DefaultParams(), si, mb)
}
// DefaultSlashing is shorthand for b.SlashingWithDefaultParams
// with nil signing info and validator missed blocks.
func (b *GenesisBuilder) DefaultSlashing() *GenesisBuilder {
return b.SlashingWithDefaultParams(nil, nil)
}
// BaseAccounts sets the initial base accounts and balances.
func (b *GenesisBuilder) BaseAccounts(ba BaseAccounts, balances []banktypes.Balance) *GenesisBuilder {
// Logic mostly copied from AddGenesisAccount.
authGenState := authtypes.GetGenesisStateFromAppState(b.codec, b.appState)
bankGenState := banktypes.GetGenesisStateFromAppState(b.codec, b.appState)
accs, err := authtypes.UnpackAccounts(authGenState.Accounts)
if err != nil {
panic(err)
}
for _, a := range ba {
accs = append(accs, a)
}
accs = authtypes.SanitizeGenesisAccounts(accs)
genAccs, err := authtypes.PackAccounts(accs)
if err != nil {
panic(err)
}
authGenState.Accounts = genAccs
jAuthGenState, err := b.codec.MarshalJSON(&authGenState)
if err != nil {
panic(err)
}
b.appState[authtypes.ModuleName] = jAuthGenState
bankGenState.Balances = append(bankGenState.Balances, balances...)
bankGenState.Balances = banktypes.SanitizeGenesisBalances(bankGenState.Balances)
jBankState, err := b.codec.MarshalJSON(bankGenState)
if err != nil {
panic(err)
}
b.appState[banktypes.ModuleName] = jBankState
return b
}
func (b *GenesisBuilder) Distribution(g *distributiontypes.GenesisState) *GenesisBuilder {
j, err := b.codec.MarshalJSON(g)
if err != nil {
panic(err)
}
b.appState[distributiontypes.ModuleName] = j
return b
}
func (b *GenesisBuilder) DefaultDistribution() *GenesisBuilder {
return b.Distribution(distributiontypes.DefaultGenesisState())
}
// JSON returns the map of the genesis after applying some final transformations.
func (b *GenesisBuilder) JSON() map[string]json.RawMessage {
gentxGenesisState := genutiltypes.NewGenesisStateFromTx(
authtx.NewTxConfig(b.codec, tx.DefaultSignModes).TxJSONEncoder(),
b.gentxs,
)
if err := genutiltypes.ValidateGenesis(
gentxGenesisState,
authtx.NewTxConfig(b.codec, tx.DefaultSignModes).TxJSONDecoder(),
genutiltypes.DefaultMessageValidator,
); err != nil {
panic(err)
}
b.appState = genutiltypes.SetGenesisStateInAppState(
b.codec, b.appState, gentxGenesisState,
)
appState, err := b.amino.MarshalJSON(b.appState)
if err != nil {
panic(err)
}
b.outer["app_state"] = appState
return b.outer
}
// Encode returns the JSON-encoded, finalized genesis.
func (b *GenesisBuilder) Encode() []byte {
j, err := b.amino.MarshalJSON(b.JSON())
if err != nil {
panic(err)
}
return j
}
// DefaultGenesisBuilderOnlyValidators returns a GenesisBuilder configured only with the given StakingValidators,
// with default parameters everywhere else.
// validatorAmount is the amount to give each validator during gentx.
//
// This is a convenience for the common case of nothing special in the genesis.
// For anything outside of the defaults,
// the longhand form of NewGenesisBuilder().ChainID(chainID)... should be used.
func DefaultGenesisBuilderOnlyValidators(
chainID string,
sv StakingValidators,
validatorAmount sdk.Coin,
) *GenesisBuilder {
cmtVals := make(CometGenesisValidators, len(sv))
for i := range sv {
cmtVals[i] = sv[i].C
}
b := NewGenesisBuilder().
ChainID(chainID).
DefaultAuthParams().
Consensus(nil, cmtVals).
BaseAccounts(sv.BaseAccounts(), nil).
StakingWithDefaultParams(nil, nil).
BankingWithDefaultParams(sv.Balances(), nil, nil, nil).
DefaultDistribution().
DefaultMint().
SlashingWithDefaultParams(nil, nil)
for _, v := range sv {
b.GenTx(*v.PK.Del, v.C.V, sdk.NewCoin(sdk.DefaultBondDenom, sdk.DefaultPowerReduction))
}
return b
}

View File

@ -0,0 +1,256 @@
package testnet_test
import (
"encoding/base64"
"encoding/json"
"fmt"
"testing"
"time"
cmted25519 "github.com/cometbft/cometbft/crypto/ed25519"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
"github.com/cosmos/cosmos-sdk/testutil/testnet"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/bech32"
"github.com/stretchr/testify/require"
)
func TestGenesisBuilder_GenesisTime(t *testing.T) {
t.Run("defaults to current time", func(t *testing.T) {
before := time.Now()
time.Sleep(time.Millisecond) // So that the genesis time will be strictly after "before".
gb := testnet.NewGenesisBuilder()
time.Sleep(time.Millisecond) // So that the genesis time will be strictly before "after".
after := time.Now()
var gts string
require.NoError(t, json.Unmarshal(gb.JSON()["genesis_time"], &gts))
gt, err := time.Parse(time.RFC3339Nano, gts)
require.NoError(t, err)
require.True(t, gt.After(before))
require.True(t, after.After(gt))
})
t.Run("can be set to arbitrary time", func(t *testing.T) {
want := time.Date(2023, 3, 27, 9, 50, 23, 0, time.UTC)
gb := testnet.NewGenesisBuilder().GenesisTime(want)
var gts string
require.NoError(t, json.Unmarshal(gb.JSON()["genesis_time"], &gts))
gt, err := time.Parse(time.RFC3339Nano, gts)
require.NoError(t, err)
require.True(t, gt.Equal(want))
})
}
func TestGenesisBuilder_InitialHeight(t *testing.T) {
t.Run("defaults to 1", func(t *testing.T) {
var ih string
require.NoError(
t,
json.Unmarshal(
testnet.NewGenesisBuilder().JSON()["initial_height"],
&ih,
),
)
require.Equal(t, ih, "1")
})
t.Run("can be set to arbitrary value", func(t *testing.T) {
var ih string
require.NoError(
t,
json.Unmarshal(
testnet.NewGenesisBuilder().InitialHeight(12345).JSON()["initial_height"],
&ih,
),
)
require.Equal(t, ih, "12345")
})
}
func TestGenesisBuilder_ChainID(t *testing.T) {
// No default.
gb := testnet.NewGenesisBuilder()
m := gb.JSON()
_, ok := m["chain_id"]
require.False(t, ok)
m = gb.ChainID("my-chain").JSON()
var id string
require.NoError(
t,
json.Unmarshal(
gb.ChainID("my-chain").JSON()["chain_id"],
&id,
),
)
require.Equal(t, id, "my-chain")
}
// Use known keys and addresses to assert that correct validator and delegator keys
// occur in the expected locations (i.e. we didn't mistakenly swap the keys anywhere).
func TestGenesisBuilder_GentxAddresses(t *testing.T) {
const chainID = "simapp-chain"
const valSecret0 = "val-secret-0"
const valAddr0 = "3F3B076353767F046477A6E0982F808C24D1870A"
const valPubKey0 = "ZhVhrOUHnUwYw/GlBSBrw/0X6A261gchCRYkAxGF2jk="
valKey0 := cmted25519.GenPrivKeyFromSecret([]byte(valSecret0))
if addr := valKey0.PubKey().Address().String(); addr != valAddr0 {
t.Fatalf("unexpected address %q for validator key 0 (expected %q)", addr, valAddr0)
}
if pub := base64.StdEncoding.EncodeToString(valKey0.PubKey().Bytes()); pub != valPubKey0 {
t.Fatalf("unexpected public key %q for validator key 0 (expected %q)", pub, valAddr0)
}
const delSecret0 = "del-secret-0"
const delAddr0 = "30D7E04DA313C31B59A46408494B4272F0A9A256"
const delPubKey0 = "Aol+ZF9xBuZmYJrT1QFLpZBvSfr/zEKifWyg0Xi1tsFV"
const delAccAddr0 = "cosmos1xrt7qndrz0p3kkdyvsyyjj6zwtc2ngjky8dcpe"
delKey0 := secp256k1.GenPrivKeyFromSecret([]byte(delSecret0))
if addr := delKey0.PubKey().Address().String(); addr != delAddr0 {
t.Fatalf("unexpected address %q for delegator key 0 (expected %q)", addr, delAddr0)
}
if pub := base64.StdEncoding.EncodeToString(delKey0.PubKey().Bytes()); pub != delPubKey0 {
t.Fatalf("unexpected public key %q for delegator key 0 (expected %q)", pub, delAddr0)
}
da, err := bech32.ConvertAndEncode("cosmos", delKey0.PubKey().Address().Bytes())
require.NoError(t, err)
if da != delAccAddr0 {
t.Fatalf("unexpected account address %q for delegator key 0 (expected %q)", da, delAccAddr0)
}
valPKs := testnet.ValidatorPrivKeys{
&testnet.ValidatorPrivKey{
Val: valKey0,
Del: delKey0,
},
}
cmtVals := valPKs.CometGenesisValidators()
stakingVals := cmtVals.StakingValidators()
valBaseAccounts := stakingVals.BaseAccounts()
b := testnet.NewGenesisBuilder().
ChainID("my-test-chain").
DefaultAuthParams().
Consensus(nil, cmtVals).
BaseAccounts(valBaseAccounts, nil).
StakingWithDefaultParams(stakingVals, nil)
for i, v := range valPKs {
b.GenTx(*v.Del, cmtVals[i].V, sdk.NewCoin(sdk.DefaultBondDenom, sdk.DefaultPowerReduction))
}
var g struct {
Consensus struct {
Validators []struct {
Address string `json:"address"`
PubKey struct {
Value string `json:"value"`
} `json:"pub_key"`
} `json:"validators"`
} `json:"consensus"`
AppState struct {
Genutil struct {
GenTxs []struct {
Body struct {
Messages []struct {
Type string `json:"@type"`
DelegatorAddress string `json:"delegator_address"`
ValidatorAddress string `json:"validator_address"`
PubKey struct {
Key string `json:"key"`
} `json:"pubkey"`
} `json:"messages"`
} `json:"body"`
AuthInfo struct {
SignerInfos []struct {
PublicKey struct {
Key string `json:"key"`
} `json:"public_key"`
} `json:"signer_infos"`
} `json:"auth_info"`
} `json:"gen_txs"`
} `json:"genutil"`
Auth struct {
Accounts []struct {
Address string `json:"address"`
PubKey struct {
Key string `json:"key"`
} `json:"pub_key"`
} `json:"accounts"`
} `json:"auth"`
} `json:"app_state"`
}
if err := json.Unmarshal(b.Encode(), &g); err != nil {
t.Fatal(err)
}
// Validator encoded as expected.
vals := g.Consensus.Validators
require.Equal(t, vals[0].Address, valAddr0)
require.Equal(t, vals[0].PubKey.Value, valPubKey0)
// Public keys on gentx message match correct keys (no ed25519/secp256k1 mismatch).
gentxs := g.AppState.Genutil.GenTxs
require.Equal(t, gentxs[0].Body.Messages[0].PubKey.Key, valPubKey0)
require.Equal(t, gentxs[0].AuthInfo.SignerInfos[0].PublicKey.Key, delPubKey0)
// Delegator is derived from the secp256k1 key, not the ed25519 key.
require.Equal(t, gentxs[0].Body.Messages[0].DelegatorAddress, delAccAddr0)
// The validator address must match the delegator address.
_, parsedValAddr, err := bech32.DecodeAndConvert(gentxs[0].Body.Messages[0].DelegatorAddress)
require.NoError(t, err)
require.Equal(t, fmt.Sprintf("%X", parsedValAddr), delAddr0)
// The only base account in this genesis, matches the secp256k1 key.
acct := g.AppState.Auth.Accounts[0]
require.Equal(t, acct.Address, delAccAddr0)
require.Equal(t, acct.PubKey.Key, delPubKey0)
}
func ExampleGenesisBuilder() {
const nVals = 4
// Generate random private keys for validators.
valPKs := testnet.NewValidatorPrivKeys(nVals)
// Produce the comet representation of those validators.
cmtVals := valPKs.CometGenesisValidators()
stakingVals := cmtVals.StakingValidators()
// Configure a new genesis builder
// with a fairly thorough set of defaults.
//
// If you only ever need defaults, you can use DefaultGenesisBuilderOnlyValidators().
b := testnet.NewGenesisBuilder().
ChainID("my-chain-id").
DefaultAuthParams().
Consensus(nil, cmtVals).
BaseAccounts(stakingVals.BaseAccounts(), nil).
DefaultStaking().
BankingWithDefaultParams(stakingVals.Balances(), nil, nil, nil).
DefaultDistribution().
DefaultMint().
DefaultSlashing()
for i := range stakingVals {
b.GenTx(*valPKs[i].Del, cmtVals[i].V, sdk.NewCoin(sdk.DefaultBondDenom, sdk.DefaultPowerReduction))
}
// Now, you can access b.JSON() if you need to make further modifications
// not (yet) supported by the GenesisBuilder API,
// or you can use b.Encode() for the serialzed JSON of the genesis.
}

116
testutil/testnet/network.go Normal file
View File

@ -0,0 +1,116 @@
package testnet
import (
"errors"
"fmt"
"sync"
"github.com/cometbft/cometbft/p2p"
)
// NewNetwork concurrently calls createCometStarter, nVals times;
// then it returns a slice of started comet nodes,
// in order corresponding with the number passed to createCometStarter.
// The returned nodes will all be peered together,
// by dialing each node's [github.com/cometbft/cometbft/p2p/pex.Reactor] to each other.
//
// Every node is attempted to be started,
// and any errors collected are joined together and returned.
//
// In the event of errors, a non-nil Nodes slice may still be returned
// and some elements may be nil.
// Callers should call [Nodes.Stop] and [Nodes.Wait] to perform cleanup,
// regardless of the returned error.
func NewNetwork(nVals int, createCometStarter func(int) *CometStarter) (Nodes, error) {
// The ordered slice of nodes; correct indexing is important.
// The creator goroutines will write directly into this slice.
nodes := make(Nodes, nVals)
// Every node will be started in its own goroutine.
// We collect the switches so that each node can dial every other node.
switchCh := make(chan *p2p.Switch, nVals)
errCh := make(chan error, nVals)
var wg sync.WaitGroup
// Start goroutines to populate nodes slice and notify as each node is available.
for i := 0; i < nVals; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
n, err := createCometStarter(i).Start()
if err != nil {
errCh <- fmt.Errorf("failed to start node %d: %w", i, err)
return
}
// Notify that the new node's switch is available,
// so this node can be peered with the other nodes.
switchCh <- n.PEXReactor().Switch
// And assign the node into its correct index in the ordered slice.
nodes[i] = n
}(i)
}
// Once all the creation goroutines are complete, close the channels,
// to signal to the collection goroutines.
go func() {
wg.Wait()
close(errCh)
close(switchCh)
}()
joinPeersDone := make(chan struct{})
go joinPeers(switchCh, joinPeersDone)
finalErrCh := make(chan error, 1)
go collectErrors(errCh, finalErrCh)
// If there were any errors, return them.
// And return any set nodes, so that they can be cleaned up properly.
if err := <-finalErrCh; err != nil {
return nodes, err
}
// No errors, so wait for peer joining to complete
// before returning the ordered nodes.
<-joinPeersDone
return nodes, nil
}
// collectErrors collects all errors that arrive on the in channel,
// joins them, then sends the joined final error on the out channel.
func collectErrors(in <-chan error, out chan<- error) {
var errs []error
for err := range in {
errs = append(errs, err)
}
var res error
if len(errs) > 0 {
res = errors.Join(errs...)
}
out <- res
}
// joinPeers collects each switch arriving on newSwitches;
// each time a new switch arrives, it dials every previously seen switch.
//
// This allows each node to be started independently and concurrently
// without predetermined p2p ports.
func joinPeers(newSwitches <-chan *p2p.Switch, done chan<- struct{}) {
defer close(done)
var readySwitches []*p2p.Switch
for newSwitch := range newSwitches {
newNetAddr := newSwitch.NetAddress()
for _, s := range readySwitches {
// For every new switch, connect with all the previously seen switches.
// It might not be necessary to dial in both directions, but it shouldn't hurt.
_ = s.DialPeerWithAddress(newNetAddr)
_ = newSwitch.DialPeerWithAddress(s.NetAddress())
}
readySwitches = append(readySwitches, newSwitch)
}
}

70
testutil/testnet/nodes.go Normal file
View File

@ -0,0 +1,70 @@
package testnet
import (
"errors"
"fmt"
"time"
"github.com/cometbft/cometbft/node"
)
// Nodes is a slice of comet nodes,
// with some additional convenience methods.
//
// Nodes may contain nil elements,
// so that a partially failed call to NewNetwork
// can still be properly cleaned up.
type Nodes []*node.Node
// Stop stops each node sequentially.
// All errors occurring during stop are returned as a joined error.
//
// Nil elements in ns are skipped.
func (ns Nodes) Stop() error {
var errs []error
for i, n := range ns {
if n == nil {
continue
}
if err := n.Stop(); err != nil {
errs = append(errs, fmt.Errorf("failed to stop node %d: %w", i, err))
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
// Wait blocks until every node in ns has completely stopped.
//
// Nil elements in ns are skipped.
func (ns Nodes) Wait() {
for _, n := range ns {
if n == nil {
continue
}
n.Wait()
}
}
// StopAndWait is shorthand for calling both Stop() and Wait(),
// useful as a deferred call in tests.
func (ns Nodes) StopAndWait() error {
err := ns.Stop()
ns.Wait()
// TODO(mr): remove this sleep call after we are using a version of Comet
// that includes a fix for https://github.com/cometbft/cometbft/issues/646.
//
// On my machine, this sleep appears to completely eliminate the late file write.
// It also almost always works around https://github.com/cometbft/cometbft/issues/652.
//
// Presumably the fix for those two issues will be included in a v0.37.1 release.
// If not, I assume they will be part of the first v0.38 series release.
time.Sleep(250 * time.Millisecond)
return err
}

View File

@ -0,0 +1,29 @@
package testnet_test
import (
"testing"
"github.com/cometbft/cometbft/node"
"github.com/cosmos/cosmos-sdk/testutil/testnet"
"github.com/stretchr/testify/require"
)
// Nil entries in a Nodes slice don't fail Stop or Wait.
func TestNodes_StopWaitNil(t *testing.T) {
for _, tc := range []struct {
Name string
Nodes []*node.Node
}{
{Name: "nil slice", Nodes: nil},
{Name: "slice with nil elements", Nodes: []*node.Node{nil}},
} {
ns := testnet.Nodes(tc.Nodes)
t.Run(tc.Name, func(t *testing.T) {
require.NoError(t, ns.Stop())
// Nothing special to assert about Wait().
// It should return immediately, without panicking.
ns.Wait()
})
}
}

View File

@ -0,0 +1,187 @@
package testnet
import (
"fmt"
sdkmath "cosmossdk.io/math"
cmted25519 "github.com/cometbft/cometbft/crypto/ed25519"
cmttypes "github.com/cometbft/cometbft/types"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/bech32"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
)
// ValidatorPrivKeys is a slice of [*ValidatorPrivKey].
type ValidatorPrivKeys []*ValidatorPrivKey
// ValidatorPrivKey holds a validator key (a comet ed25519 key)
// and the validator's delegator or account key (a Cosmos SDK secp256k1 key).
type ValidatorPrivKey struct {
Val cmted25519.PrivKey
Del *secp256k1.PrivKey
}
// NewValidatorPrivKeys returns a ValidatorPrivKeys of length n,
// where each set of keys is dynamically generated.
//
// If writing a test where deterministic keys are required,
// the caller should manually construct a slice and assign each key as needed.
func NewValidatorPrivKeys(n int) ValidatorPrivKeys {
vpk := make(ValidatorPrivKeys, n)
for i := range vpk {
vpk[i] = &ValidatorPrivKey{
Val: cmted25519.GenPrivKey(),
Del: secp256k1.GenPrivKey(),
}
}
return vpk
}
// CometGenesisValidators derives the CometGenesisValidators belonging to vpk.
func (vpk ValidatorPrivKeys) CometGenesisValidators() CometGenesisValidators {
cgv := make(CometGenesisValidators, len(vpk))
for i, pk := range vpk {
pubKey := pk.Val.PubKey()
const votingPower = 1
cmtVal := cmttypes.NewValidator(pubKey, votingPower)
cgv[i] = &CometGenesisValidator{
V: cmttypes.GenesisValidator{
Address: cmtVal.Address,
PubKey: cmtVal.PubKey,
Power: cmtVal.VotingPower,
Name: fmt.Sprintf("val-%d", i),
},
PK: pk,
}
}
return cgv
}
// CometGenesisValidators is a slice of [*CometGenesisValidator].
type CometGenesisValidators []*CometGenesisValidator
// CometGenesisValidator holds a comet GenesisValidator
// and a reference to the ValidatorPrivKey from which the CometGenesisValidator was derived.
type CometGenesisValidator struct {
V cmttypes.GenesisValidator
PK *ValidatorPrivKey
}
// ToComet returns a new slice of [cmttypes.GenesisValidator],
// useful for some interactions.
func (cgv CometGenesisValidators) ToComet() []cmttypes.GenesisValidator {
vs := make([]cmttypes.GenesisValidator, len(cgv))
for i, v := range cgv {
vs[i] = v.V
}
return vs
}
// StakingValidators derives the StakingValidators belonging to cgv.
func (cgv CometGenesisValidators) StakingValidators() StakingValidators {
vals := make(StakingValidators, len(cgv))
for i, v := range cgv {
pk, err := cryptocodec.FromCmtPubKeyInterface(v.V.PubKey)
if err != nil {
panic(fmt.Errorf("failed to extract comet pub key: %w", err))
}
pkAny, err := codectypes.NewAnyWithValue(pk)
if err != nil {
panic(fmt.Errorf("failed to wrap pub key in any type: %w", err))
}
vals[i] = &StakingValidator{
V: stakingtypes.Validator{
OperatorAddress: sdk.ValAddress(v.V.Address).String(), // TODO: this relies on global bech32 config.
ConsensusPubkey: pkAny,
Status: stakingtypes.Bonded,
Tokens: sdk.DefaultPowerReduction,
DelegatorShares: sdkmath.LegacyOneDec(),
MinSelfDelegation: sdkmath.ZeroInt(),
// more fields uncopied from testutil/sims/app_helpers.go:220
},
C: v,
PK: v.PK,
}
}
return vals
}
// StakingValidators is a slice of [*StakingValidator].
type StakingValidators []*StakingValidator
// StakingValidator holds a [stakingtypes.Validator],
// and the CometGenesisValidator and ValidatorPrivKey required to derive the StakingValidator.
type StakingValidator struct {
V stakingtypes.Validator
C *CometGenesisValidator
PK *ValidatorPrivKey
}
// ToStakingType returns a new slice of [stakingtypes.Validator],
// useful for some interactions.
func (sv StakingValidators) ToStakingType() []stakingtypes.Validator {
vs := make([]stakingtypes.Validator, len(sv))
for i, v := range sv {
vs[i] = v.V
}
return vs
}
// BaseAccounts returns the BaseAccounts for this set of StakingValidators.
// The base accounts are important for [*GenesisBuilder.BaseAccounts].
func (sv StakingValidators) BaseAccounts() BaseAccounts {
ba := make(BaseAccounts, len(sv))
for i, v := range sv {
const accountNumber = 0
const sequenceNumber = 0
pubKey := v.PK.Del.PubKey()
bech, err := bech32.ConvertAndEncode("cosmos", pubKey.Address().Bytes()) // TODO: this shouldn't be hardcoded to cosmos!
if err != nil {
panic(err)
}
accAddr, err := sdk.AccAddressFromBech32(bech)
if err != nil {
panic(err)
}
ba[i] = authtypes.NewBaseAccount(
accAddr, pubKey, accountNumber, sequenceNumber,
)
}
return ba
}
// Balances returns the balances held by this set of StakingValidators.
func (sv StakingValidators) Balances() []banktypes.Balance {
bals := make([]banktypes.Balance, len(sv))
for i, v := range sv {
addr, err := bech32.ConvertAndEncode("cosmos", v.PK.Del.PubKey().Address().Bytes()) // TODO: this shouldn't be hardcoded to cosmos!
if err != nil {
panic(err)
}
bals[i] = banktypes.Balance{
Address: addr,
Coins: sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, v.V.Tokens)},
}
}
return bals
}