diff --git a/app/app.go b/app/app.go index 67b0b7800..cd05f8084 100644 --- a/app/app.go +++ b/app/app.go @@ -94,6 +94,7 @@ import ( "github.com/spf13/cast" "github.com/babylonchain/babylon/app/upgrades" + "github.com/babylonchain/babylon/app/upgrades/vanilla" bbn "github.com/babylonchain/babylon/types" appkeepers "github.com/babylonchain/babylon/app/keepers" @@ -158,8 +159,10 @@ var ( } // software upgrades and forks - Upgrades = []upgrades.Upgrade{} - Forks = []upgrades.Fork{} + Upgrades = []upgrades.Upgrade{ + vanilla.Upgrade, + } + Forks = []upgrades.Fork{} ) func init() { diff --git a/app/upgrades/vanilla/upgrades.go b/app/upgrades/vanilla/upgrades.go index 1cc0c9060..156e5de72 100644 --- a/app/upgrades/vanilla/upgrades.go +++ b/app/upgrades/vanilla/upgrades.go @@ -32,7 +32,6 @@ func CreateUpgradeHandler( keepers *keepers.AppKeepers, ) upgradetypes.UpgradeHandler { return func(context context.Context, _plan upgradetypes.Plan, fromVM module.VersionMap) (module.VersionMap, error) { - ctx := sdk.UnwrapSDKContext(context) propVanilla(ctx, &keepers.AccountKeeper, &keepers.BTCStakingKeeper) diff --git a/test/e2e/configurer/base.go b/test/e2e/configurer/base.go index 6744fff94..d7d474be1 100644 --- a/test/e2e/configurer/base.go +++ b/test/e2e/configurer/base.go @@ -19,6 +19,7 @@ import ( "github.com/babylonchain/babylon/types" types2 "github.com/babylonchain/babylon/x/btccheckpoint/types" "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" ) // baseConfigurer is the base implementation for the @@ -39,16 +40,18 @@ const defaultSyncUntilHeight = 3 func (bc *baseConfigurer) ClearResources() error { bc.t.Log("tearing down e2e integration test suite...") - if err := bc.containerManager.ClearResources(); err != nil { - return err - } + g := new(errgroup.Group) + g.Go(func() error { + return bc.containerManager.ClearResources() + }) for _, chainConfig := range bc.chainConfigs { - if err := os.RemoveAll(chainConfig.DataDir); err != nil { - return err - } + chainConfig := chainConfig + g.Go(func() error { + return os.RemoveAll(chainConfig.DataDir) + }) } - return nil + return g.Wait() } func (bc *baseConfigurer) GetChainConfig(chainIndex int) *chain.Config { diff --git a/test/e2e/configurer/chain/chain.go b/test/e2e/configurer/chain/chain.go index 4d5e1ec03..2c16e9f4b 100644 --- a/test/e2e/configurer/chain/chain.go +++ b/test/e2e/configurer/chain/chain.go @@ -2,10 +2,12 @@ package chain import ( "fmt" - ibctesting "github.com/cosmos/ibc-go/v8/testing" "testing" "time" + govv1 "cosmossdk.io/api/cosmos/gov/v1" + ibctesting "github.com/cosmos/ibc-go/v8/testing" + coretypes "github.com/cometbft/cometbft/rpc/core/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" @@ -173,3 +175,10 @@ func (c *Config) GetNodeAtIndex(nodeIndex int) (*NodeConfig, error) { } return c.NodeConfigs[nodeIndex], nil } + +// TxGovVoteFromAllNodes votes in a gov prop from all nodes wallet. +func (c *Config) TxGovVoteFromAllNodes(propID int, option govv1.VoteOption, overallFlags ...string) { + for _, n := range c.NodeConfigs { + n.TxGovVote(n.WalletName, propID, option, overallFlags...) + } +} diff --git a/test/e2e/configurer/chain/commands.go b/test/e2e/configurer/chain/commands.go index 8d8c59b1a..bb789ec4b 100644 --- a/test/e2e/configurer/chain/commands.go +++ b/test/e2e/configurer/chain/commands.go @@ -12,6 +12,7 @@ import ( "strings" "time" + govv1 "cosmossdk.io/api/cosmos/gov/v1" txformat "github.com/babylonchain/babylon/btctxformatter" "github.com/babylonchain/babylon/test/e2e/containers" "github.com/babylonchain/babylon/test/e2e/initialization" @@ -349,6 +350,43 @@ func (n *NodeConfig) TxMultisignBroadcast(walletNameMultisig, txFileFullPath str n.TxBroadcast(signedTxToBroadcast) } +// TxGovPropSubmitProposal submits a governance proposal from the file inside the container, +// if the file is local, remind to add it to the mounting point in container. +func (n *NodeConfig) TxGovPropSubmitProposal(proposalJsonFilePath, from string, overallFlags ...string) int { + n.LogActionF("submitting new v1 proposal type %s", proposalJsonFilePath) + + cmd := []string{ + "babylond", "tx", "gov", "submit-proposal", proposalJsonFilePath, + fmt.Sprintf("--from=%s", from), + } + + _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainId, n.Name, append(cmd, overallFlags...)) + require.NoError(n.t, err) + + n.WaitForNextBlock() + + props := n.QueryProposals() + require.GreaterOrEqual(n.t, len(props.Proposals), 1) + + n.LogActionF("successfully submitted new v1 proposal type") + return int(props.Proposals[len(props.Proposals)-1].ProposalId) +} + +// TxGovVote votes in a governance proposal +func (n *NodeConfig) TxGovVote(from string, propID int, option govv1.VoteOption, overallFlags ...string) { + n.LogActionF("submitting vote %s to prop %d", option, propID) + + cmd := []string{ + "babylond", "tx", "gov", "vote", fmt.Sprintf("%d", propID), option.String(), + fmt.Sprintf("--from=%s", from), + } + + _, _, err := n.containerManager.ExecTxCmd(n.t, n.chainId, n.Name, append(cmd, overallFlags...)) + require.NoError(n.t, err) + + n.LogActionF("successfully submitted vote %s to prop %d", option, propID) +} + // WriteFile writes a new file in the config dir of the node where it is volume mounted to the // babylon home inside the container and returns the full file path inside the container. func (n *NodeConfig) WriteFile(fileName, content string) (fullFilePathInContainer string) { diff --git a/test/e2e/configurer/chain/node.go b/test/e2e/configurer/chain/node.go index 8e185ec51..2a8f1ccd5 100644 --- a/test/e2e/configurer/chain/node.go +++ b/test/e2e/configurer/chain/node.go @@ -153,6 +153,13 @@ func (n *NodeConfig) WaitForNextBlocks(numberOfBlocks uint64) { }, fmt.Sprintf("Timed out waiting for block %d. Current height is: %d", latest, blockToWait)) } +func (n *NodeConfig) WaitForBlockHeight(blkHeight uint64) { + n.WaitForCondition(func() bool { + newLatest := n.LatestBlockNumber() + return newLatest > blkHeight + }, fmt.Sprintf("Timed out waiting for block %d.", blkHeight)) +} + func (n *NodeConfig) extractOperatorAddressIfValidator() error { if !n.IsValidator { n.t.Logf("node (%s) is not a validator, skipping", n.Name) diff --git a/test/e2e/configurer/chain/queries.go b/test/e2e/configurer/chain/queries.go index c0bb049f8..f7b2c9cb5 100644 --- a/test/e2e/configurer/chain/queries.go +++ b/test/e2e/configurer/chain/queries.go @@ -19,6 +19,7 @@ import ( "github.com/cosmos/cosmos-sdk/types/query" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" "github.com/stretchr/testify/require" "github.com/babylonchain/babylon/test/e2e/util" @@ -406,6 +407,29 @@ func (n *NodeConfig) QueryWasmSmart(contract string, msg string, result any) err return nil } +func (n *NodeConfig) QueryProposal(proposalNumber int) govtypesv1.QueryProposalResponse { + path := fmt.Sprintf("cosmos/gov/v1beta1/proposals/%d", proposalNumber) + bz, err := n.QueryGRPCGateway(path, url.Values{}) + require.NoError(n.t, err) + + var resp govtypesv1.QueryProposalResponse + err = util.Cdc.UnmarshalJSON(bz, &resp) + require.NoError(n.t, err) + + return resp +} + +func (n *NodeConfig) QueryProposals() govtypesv1.QueryProposalsResponse { + bz, err := n.QueryGRPCGateway("cosmos/gov/v1beta1/proposals", url.Values{}) + require.NoError(n.t, err) + + var resp govtypesv1.QueryProposalsResponse + err = util.Cdc.UnmarshalJSON(bz, &resp) + require.NoError(n.t, err) + + return resp +} + func (n *NodeConfig) QueryWasmSmartObject(contract string, msg string) (resultObject map[string]interface{}, err error) { err = n.QueryWasmSmart(contract, msg, &resultObject) if err != nil { @@ -413,3 +437,19 @@ func (n *NodeConfig) QueryWasmSmartObject(contract string, msg string) (resultOb } return resultObject, nil } + +func (n *NodeConfig) QueryTx(txHash string, overallFlags ...string) sdk.TxResponse { + cmd := []string{ + "babylond", "q", "tx", "--type=hash", txHash, "--output=json", + n.FlagChainID(), + } + + out, _, err := n.containerManager.ExecCmd(n.t, n.Name, append(cmd, overallFlags...), "") + require.NoError(n.t, err) + + var txResp sdk.TxResponse + err = util.Cdc.UnmarshalJSON(out.Bytes(), &txResp) + require.NoError(n.t, err) + + return txResp +} diff --git a/test/e2e/configurer/factory.go b/test/e2e/configurer/factory.go index bcac549e5..0182a40dc 100644 --- a/test/e2e/configurer/factory.go +++ b/test/e2e/configurer/factory.go @@ -195,3 +195,20 @@ func NewBTCStakingConfigurer(t *testing.T, isDebugLogEnabled bool) (Configurer, containerManager, ), nil } + +// NewSoftwareUpgradeTest returns a new Configurer for Software Upgrade testing +func NewSoftwareUpgradeTest(t *testing.T, isDebugLogEnabled bool) (Configurer, error) { + containerManager, err := containers.NewManager(isDebugLogEnabled, false) + if err != nil { + return nil, err + } + + return NewCurrentBranchConfigurer(t, + []*chain.Config{ + // we only need 1 chain for testing upgrade + chain.New(t, containerManager, initialization.ChainAID, validatorConfigsChainA, nil), + }, + baseSetup, // base set up + containerManager, + ), nil +} diff --git a/test/e2e/containers/containers.go b/test/e2e/containers/containers.go index 09782d7f1..737af78ea 100644 --- a/test/e2e/containers/containers.go +++ b/test/e2e/containers/containers.go @@ -13,6 +13,7 @@ import ( "github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" ) const ( @@ -264,6 +265,7 @@ func (m *Manager) RunNodeResource(chainId string, containerName, valCondifDir st Mounts: []string{ fmt.Sprintf("%s/:%s", valCondifDir, BabylonHomePath), fmt.Sprintf("%s/bytecode:/bytecode", pwd), + fmt.Sprintf("%s/upgrades:/upgrades", pwd), }, } @@ -321,17 +323,20 @@ func (m *Manager) RemoveNodeResource(containerName string) error { } // ClearResources removes all outstanding Docker resources created by the Manager. -func (m *Manager) ClearResources() error { +func (m *Manager) ClearResources() (e error) { + g := new(errgroup.Group) for _, resource := range m.resources { - if err := m.pool.Purge(resource); err != nil { - return err - } + resource := resource + g.Go(func() error { + return m.pool.Purge(resource) + }) } - if err := m.pool.RemoveNetwork(m.network); err != nil { - return err - } - return nil + g.Go(func() error { + return m.pool.RemoveNetwork(m.network) + }) + + return g.Wait() } func noRestart(config *docker.HostConfig) { diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 4c877c6c8..0eb9240dd 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -35,3 +35,8 @@ func TestBTCTimestampingPhase2RlyTestSuite(t *testing.T) { func TestBTCStakingTestSuite(t *testing.T) { suite.Run(t, new(BTCStakingTestSuite)) } + +// TestSoftwareUpgradeCurrentBranchTestSuite tests software upgrade protocol end-to-end +func TestSoftwareUpgradeCurrentBranchTestSuite(t *testing.T) { + suite.Run(t, new(SoftwareUpgradeCurrentBranchTestSuite)) +} diff --git a/test/e2e/initialization/config.go b/test/e2e/initialization/config.go index 9e5f95a16..9af54728e 100644 --- a/test/e2e/initialization/config.go +++ b/test/e2e/initialization/config.go @@ -6,7 +6,7 @@ import ( "path/filepath" "time" - "cosmossdk.io/math" + sdkmath "cosmossdk.io/math" cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" @@ -17,6 +17,8 @@ import ( crisistypes "github.com/cosmos/cosmos-sdk/x/crisis/types" "github.com/cosmos/cosmos-sdk/x/genutil" genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" staketypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/cosmos/gogoproto/proto" @@ -67,9 +69,9 @@ const ( ) var ( - StakeAmountIntA = math.NewInt(StakeAmountA) + StakeAmountIntA = sdkmath.NewInt(StakeAmountA) StakeAmountCoinA = sdk.NewCoin(BabylonDenom, StakeAmountIntA) - StakeAmountIntB = math.NewInt(StakeAmountB) + StakeAmountIntB = sdkmath.NewInt(StakeAmountB) StakeAmountCoinB = sdk.NewCoin(BabylonDenom, StakeAmountIntB) InitBalanceStrA = fmt.Sprintf("%d%s", BabylonBalanceA, BabylonDenom) @@ -216,6 +218,11 @@ func initGenesis(chain *internalChain, votingPeriod, expeditedVotingPeriod time. return err } + err = updateModuleGenesis(appGenState, govtypes.ModuleName, &govv1.GenesisState{}, updateGovGenesis) + if err != nil { + return err + } + err = updateModuleGenesis(appGenState, minttypes.ModuleName, &minttypes.GenesisState{}, updateMintGenesis) if err != nil { return err @@ -288,6 +295,13 @@ func updateBankGenesis(bankGenState *banktypes.GenesisState) { }) } +func updateGovGenesis(govGenState *govv1.GenesisState) { + govGenState.Params.MinDeposit = sdk.NewCoins(sdk.NewCoin(BabylonDenom, sdkmath.NewInt(100))) + + votingPeriod := time.Duration(time.Second * 10) + govGenState.Params.VotingPeriod = &votingPeriod +} + func updateMintGenesis(mintGenState *minttypes.GenesisState) { mintGenState.Params.MintDenom = BabylonDenom } @@ -299,7 +313,7 @@ func updateStakeGenesis(stakeGenState *staketypes.GenesisState) { MaxEntries: 7, HistoricalEntries: 10000, UnbondingTime: staketypes.DefaultUnbondingTime, - MinCommissionRate: math.LegacyZeroDec(), + MinCommissionRate: sdkmath.LegacyZeroDec(), } } diff --git a/test/e2e/software_upgrade_current_branch_e2e_test.go b/test/e2e/software_upgrade_current_branch_e2e_test.go new file mode 100644 index 000000000..298285437 --- /dev/null +++ b/test/e2e/software_upgrade_current_branch_e2e_test.go @@ -0,0 +1,85 @@ +package e2e + +import ( + "fmt" + + govv1 "cosmossdk.io/api/cosmos/gov/v1" + "github.com/stretchr/testify/suite" + + "github.com/babylonchain/babylon/test/e2e/configurer" +) + +const ( + // Mount path in container is fmt.Sprintf("%s/upgrades:/upgrades", pwd) + vanillaUpgradeFilePath = "/upgrades/vanilla.json" +) + +type SoftwareUpgradeCurrentBranchTestSuite struct { + suite.Suite + + configurer configurer.Configurer +} + +func (s *SoftwareUpgradeCurrentBranchTestSuite) SetupSuite() { + s.T().Log("setting up e2e integration test suite...") + var err error + + // The e2e test flow is as follows: + // + // 1. Configure 1 chain with some validator nodes + // 2. Execute various e2e tests + s.configurer, err = configurer.NewSoftwareUpgradeTest(s.T(), true) + s.NoError(err) + err = s.configurer.ConfigureChains() + s.NoError(err) + err = s.configurer.RunSetup() + s.NoError(err) +} + +func (s *SoftwareUpgradeCurrentBranchTestSuite) TearDownSuite() { + err := s.configurer.ClearResources() + s.Require().NoError(err) +} + +// Test1UpgradeVanilla is an end-to-end test for +// running a software upgrade proposal +func (s *SoftwareUpgradeCurrentBranchTestSuite) Test1UpgradeVanilla() { + // chain is already start the chain with software upgrade available + chainA := s.configurer.GetChainConfig(0) + chainA.WaitUntilHeight(1) + + nonValidatorNode, err := chainA.GetNodeAtIndex(2) + s.NoError(err) + + fpsBeforeUpgrade := nonValidatorNode.QueryFinalityProviders() + + // software upgrade gov prop + propID := nonValidatorNode.TxGovPropSubmitProposal(vanillaUpgradeFilePath, nonValidatorNode.WalletName) + s.Equal(1, propID) + + // vote from all nodes + chainA.TxGovVoteFromAllNodes(propID, govv1.VoteOption_VOTE_OPTION_YES) + + tx := nonValidatorNode.QueryProposal(propID) + fmt.Printf("\n prop %+v", tx) + + // waits for block to reach from plan + 1 + // tricky to get current heigth and set it in the json, because the file is + // load at the mounting point of the node it could be created at runtime and + // stored in the filesystem of the container + nonValidatorNode.WaitForBlockHeight(21) + + tx = nonValidatorNode.QueryProposal(propID) + fmt.Printf("\n prop %+v", tx) + + // verifies vanilla upgrade was completed + fpsAfterUpgrade := nonValidatorNode.QueryFinalityProviders() + s.Equal(len(fpsBeforeUpgrade)+1, len(fpsAfterUpgrade)) + + // docker logs -f + // 2:09AM ERR BINARY UPDATED BEFORE TRIGGER! UPGRADE "vanilla" - in binary but not executed on chain. Downgrade your binary module=x/upgrade + // 2:09AM ERR error in proxyAppConn.FinalizeBlock err="BINARY UPDATED BEFORE TRIGGER! UPGRADE \"vanilla\" - in binary but not executed on chain. Downgrade your binary" module=state + tx = nonValidatorNode.QueryProposal(propID) + fmt.Printf("\n prop %+v", tx) + +} diff --git a/test/e2e/upgrades/vanilla.json b/test/e2e/upgrades/vanilla.json new file mode 100644 index 000000000..c9fd6c9d3 --- /dev/null +++ b/test/e2e/upgrades/vanilla.json @@ -0,0 +1,14 @@ +{ + "title": "any title", + "summary": "any summary", + "messages": [ + { + "@type": "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade", + "authority": "bbn10d07y265gmmuvt4z0w9aw880jnsr700jduz5f2", + "plan": { "name": "vanilla", "info": "Msg info", "height": 20 } + } + ], + "deposit": "500000000ubbn", + "initial_deposit": "500000000ubbn", + "initialDeposit": "500000000ubbn" +}