forked from cosmos/cosmos-sdk
-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test(accounts): add multisig e2e tests (cosmos#20310)
- Loading branch information
1 parent
559f784
commit 1a8425a
Showing
7 changed files
with
497 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,265 @@ | ||
package multisig | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/suite" | ||
|
||
bankv1beta1 "cosmossdk.io/api/cosmos/bank/v1beta1" | ||
basev1beta1 "cosmossdk.io/api/cosmos/base/v1beta1" | ||
"cosmossdk.io/core/header" | ||
"cosmossdk.io/math" | ||
v1 "cosmossdk.io/x/accounts/defaults/multisig/v1" | ||
accountsv1 "cosmossdk.io/x/accounts/v1" | ||
|
||
codectypes "github.com/cosmos/cosmos-sdk/codec/types" | ||
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" | ||
sdk "github.com/cosmos/cosmos-sdk/types" | ||
) | ||
|
||
func TestE2ETestSuite(t *testing.T) { | ||
suite.Run(t, NewE2ETestSuite()) | ||
} | ||
|
||
// TestSimpleSendProposal creates a multisig account with 1 member, sends a tx, votes and executes it. | ||
func (s *E2ETestSuite) TestSimpleSendProposal() { | ||
ctx := sdk.NewContext(s.app.CommitMultiStore(), false, s.app.Logger()).WithHeaderInfo(header.Info{ | ||
Time: time.Now(), | ||
}) | ||
|
||
randAcc := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) | ||
addr, err := s.app.AuthKeeper.AddressCodec().BytesToString(randAcc) | ||
s.NoError(err) | ||
|
||
initialMembers := map[string]uint64{ | ||
s.membersAddr[0]: 100, | ||
} | ||
accountAddr, accAddrStr := s.initAccount(ctx, s.members[0], initialMembers) | ||
|
||
balance := s.app.BankKeeper.GetBalance(ctx, randAcc, "stake") | ||
s.Equal(math.NewInt(0), balance.Amount) | ||
|
||
// do a simple bank send | ||
msg := &bankv1beta1.MsgSend{ | ||
FromAddress: accAddrStr, | ||
ToAddress: addr, | ||
Amount: []*basev1beta1.Coin{ | ||
{ | ||
Denom: "stake", | ||
Amount: "100", | ||
}, | ||
}, | ||
} | ||
anyMsg, err := codectypes.NewAnyWithValue(msg) | ||
s.NoError(err) | ||
|
||
s.createProposal(ctx, accountAddr, s.members[0], anyMsg) | ||
|
||
// now we vote for it | ||
voteReq := &v1.MsgVote{ | ||
ProposalId: 0, | ||
Vote: v1.VoteOption_VOTE_OPTION_YES, | ||
} | ||
|
||
err = s.executeTx(ctx, voteReq, accountAddr, s.members[0]) | ||
s.NoError(err) | ||
|
||
// now we execute it | ||
err = s.executeProposal(ctx, accountAddr, s.members[0], 0) | ||
s.NoError(err) | ||
|
||
foundPropResult := false | ||
for _, v := range ctx.EventManager().Events() { | ||
if v.Type == "proposal_tally" { | ||
foundPropResult = true | ||
status, found := v.GetAttribute("status") | ||
s.True(found) | ||
s.Equal(v1.ProposalStatus_PROPOSAL_STATUS_PASSED.String(), status.Value) | ||
|
||
yesVotes, found := v.GetAttribute("yes_votes") | ||
s.True(found) | ||
s.Equal("100", yesVotes.Value) | ||
|
||
noVotes, found := v.GetAttribute("no_votes") | ||
s.True(found) | ||
s.Equal("0", noVotes.Value) | ||
|
||
propID, found := v.GetAttribute("proposal_id") | ||
s.True(found) | ||
s.Equal("0", propID.Value) | ||
|
||
execErr, found := v.GetAttribute("exec_err") | ||
s.True(found) | ||
s.Equal("<nil>", execErr.Value) | ||
|
||
rejectErr, found := v.GetAttribute("reject_err") | ||
s.True(found) | ||
s.Equal("<nil>", rejectErr.Value) | ||
} | ||
} | ||
s.True(foundPropResult) | ||
|
||
balance = s.app.BankKeeper.GetBalance(ctx, randAcc, "stake") | ||
s.Equal(int64(100), balance.Amount.Int64()) | ||
|
||
// try to execute again, should fail | ||
err = s.executeProposal(ctx, accountAddr, s.members[0], 0) | ||
s.Error(err) | ||
} | ||
|
||
// TestConfigUpdate creates a multisig with 1 member, adds 2 more members and | ||
// changes the config to require 2/3 majority (also through a proposal). | ||
func (s *E2ETestSuite) TestConfigUpdate() { | ||
ctx := sdk.NewContext(s.app.CommitMultiStore(), false, s.app.Logger()).WithHeaderInfo(header.Info{ | ||
Time: time.Now(), | ||
}) | ||
|
||
initialMembers := map[string]uint64{ | ||
s.membersAddr[0]: 100, | ||
} | ||
accountAddr, accAddrStr := s.initAccount(ctx, s.members[0], initialMembers) | ||
|
||
// Add 2 members and pass the proposal | ||
// create proposal | ||
updateMsg := &v1.MsgUpdateConfig{ | ||
UpdateMembers: []*v1.Member{ | ||
{ | ||
Address: s.membersAddr[1], | ||
Weight: 100, | ||
}, | ||
{ | ||
Address: s.membersAddr[2], | ||
Weight: 100, | ||
}, | ||
}, | ||
Config: &v1.Config{ | ||
Threshold: 200, // 3 members with 100 power each, 2/3 majority | ||
Quorum: 200, | ||
VotingPeriod: 120, | ||
Revote: false, | ||
EarlyExecution: false, | ||
}, | ||
} | ||
|
||
msgExec := &accountsv1.MsgExecute{ | ||
Sender: accAddrStr, | ||
Target: accAddrStr, | ||
Message: codectypes.UnsafePackAny(updateMsg), | ||
Funds: []sdk.Coin{}, | ||
} | ||
|
||
s.createProposal(ctx, accountAddr, s.members[0], codectypes.UnsafePackAny(msgExec)) | ||
|
||
// vote | ||
voteReq := &v1.MsgVote{ | ||
ProposalId: 0, | ||
Vote: v1.VoteOption_VOTE_OPTION_YES, | ||
} | ||
|
||
err := s.executeTx(ctx, voteReq, accountAddr, s.members[0]) | ||
s.NoError(err) | ||
|
||
err = s.executeProposal(ctx, accountAddr, s.members[0], 0) | ||
s.NoError(err) | ||
|
||
// get members | ||
res, err := s.queryAcc(ctx, &v1.QueryConfig{}, accountAddr) | ||
s.NoError(err) | ||
resp := res.(*v1.QueryConfigResponse) | ||
s.Len(resp.Members, 3) | ||
s.Equal(int64(200), resp.Config.Threshold) | ||
|
||
// Try to remove a member, but it doesn't reach passing threshold | ||
// create proposal | ||
msgExec = &accountsv1.MsgExecute{ | ||
Sender: accAddrStr, | ||
Target: accAddrStr, | ||
Message: codectypes.UnsafePackAny(&v1.MsgUpdateConfig{ | ||
UpdateMembers: []*v1.Member{ | ||
{ | ||
Address: s.membersAddr[1], | ||
Weight: 0, | ||
}, | ||
}, | ||
Config: &v1.Config{ | ||
Threshold: 200, // 3 members with 100 power each, 2/3 majority | ||
Quorum: 200, | ||
VotingPeriod: 120, | ||
Revote: false, | ||
EarlyExecution: false, | ||
}, | ||
}), | ||
Funds: []sdk.Coin{}, | ||
} | ||
|
||
s.createProposal(ctx, accountAddr, s.members[0], codectypes.UnsafePackAny(msgExec)) | ||
|
||
// vote | ||
voteReq = &v1.MsgVote{ | ||
ProposalId: 1, | ||
Vote: v1.VoteOption_VOTE_OPTION_NO, | ||
} | ||
|
||
err = s.executeTx(ctx, voteReq, accountAddr, s.members[0]) | ||
s.NoError(err) | ||
|
||
// need to wait until voting period is over because we disabled early execution on the last | ||
// config update | ||
err = s.executeProposal(ctx, accountAddr, s.members[0], 0) | ||
s.ErrorContains(err, "voting period has not ended yet, and early execution is not enabled") | ||
|
||
// vote with member 1 | ||
voteReq = &v1.MsgVote{ | ||
ProposalId: 1, | ||
Vote: v1.VoteOption_VOTE_OPTION_NO, | ||
} | ||
|
||
err = s.executeTx(ctx, voteReq, accountAddr, s.members[1]) | ||
s.NoError(err) | ||
|
||
// need to wait until voting period is over because we disabled early execution on the last | ||
// config update | ||
err = s.executeProposal(ctx, accountAddr, s.members[0], 1) | ||
s.ErrorContains(err, "voting period has not ended yet, and early execution is not enabled") | ||
|
||
headerInfo := ctx.HeaderInfo() | ||
headerInfo.Time = headerInfo.Time.Add(time.Second * 121) | ||
ctx = ctx.WithHeaderInfo(headerInfo) | ||
|
||
// now it should work, but the proposal will fail | ||
err = s.executeProposal(ctx, accountAddr, s.members[0], 1) | ||
s.NoError(err) | ||
|
||
foundPropResult := false | ||
for _, v := range ctx.EventManager().Events() { | ||
if v.Type == "proposal_tally" { | ||
propID, found := v.GetAttribute("proposal_id") | ||
s.True(found) | ||
|
||
if propID.Value == "1" { | ||
foundPropResult = true | ||
status, found := v.GetAttribute("status") | ||
s.True(found) | ||
s.Equal(v1.ProposalStatus_PROPOSAL_STATUS_REJECTED.String(), status.Value) | ||
|
||
// exec_err is nil because the proposal didn't execute | ||
execErr, found := v.GetAttribute("exec_err") | ||
s.True(found) | ||
s.Equal("<nil>", execErr.Value) | ||
|
||
rejectErr, found := v.GetAttribute("reject_err") | ||
s.True(found) | ||
s.Equal("threshold not reached", rejectErr.Value) | ||
} | ||
} | ||
} | ||
s.True(foundPropResult) | ||
|
||
// get members | ||
res, err = s.queryAcc(ctx, &v1.QueryConfig{}, accountAddr) | ||
s.NoError(err) | ||
resp = res.(*v1.QueryConfigResponse) | ||
s.Len(resp.Members, 3) | ||
s.Equal(int64(200), resp.Config.Threshold) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
package multisig | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
"github.com/stretchr/testify/suite" | ||
"google.golang.org/protobuf/runtime/protoiface" | ||
|
||
"cosmossdk.io/math" | ||
"cosmossdk.io/simapp" | ||
multisigaccount "cosmossdk.io/x/accounts/defaults/multisig" | ||
v1 "cosmossdk.io/x/accounts/defaults/multisig/v1" | ||
"cosmossdk.io/x/bank/testutil" | ||
|
||
codectypes "github.com/cosmos/cosmos-sdk/codec/types" | ||
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" | ||
sdk "github.com/cosmos/cosmos-sdk/types" | ||
) | ||
|
||
type ProtoMsg = protoiface.MessageV1 | ||
|
||
type E2ETestSuite struct { | ||
suite.Suite | ||
|
||
app *simapp.SimApp | ||
members []sdk.AccAddress | ||
membersAddr []string | ||
} | ||
|
||
func NewE2ETestSuite() *E2ETestSuite { | ||
return &E2ETestSuite{} | ||
} | ||
|
||
func (s *E2ETestSuite) SetupSuite() { | ||
s.app = setupApp(s.T()) | ||
|
||
s.members = []sdk.AccAddress{} | ||
for i := 0; i < 10; i++ { | ||
addr := secp256k1.GenPrivKey().PubKey().Address() | ||
addrStr, err := s.app.AuthKeeper.AddressCodec().BytesToString(addr) | ||
require.NoError(s.T(), err) | ||
s.membersAddr = append(s.membersAddr, addrStr) | ||
s.members = append(s.members, sdk.AccAddress(addr)) | ||
} | ||
} | ||
|
||
func (s *E2ETestSuite) TearDownSuite() {} | ||
|
||
func setupApp(t *testing.T) *simapp.SimApp { | ||
t.Helper() | ||
app := simapp.Setup(t, false) | ||
return app | ||
} | ||
|
||
func (s *E2ETestSuite) executeTx(ctx context.Context, msg sdk.Msg, accAddr, sender []byte) error { | ||
_, err := s.app.AccountsKeeper.Execute(ctx, accAddr, sender, msg, nil) | ||
return err | ||
} | ||
|
||
func (s *E2ETestSuite) queryAcc(ctx context.Context, req sdk.Msg, accAddr []byte) (ProtoMsg, error) { | ||
resp, err := s.app.AccountsKeeper.Query(ctx, accAddr, req) | ||
return resp, err | ||
} | ||
|
||
func (s *E2ETestSuite) fundAccount(ctx context.Context, addr sdk.AccAddress, amt sdk.Coins) { | ||
require.NoError(s.T(), testutil.FundAccount(ctx, s.app.BankKeeper, addr, amt)) | ||
} | ||
|
||
// initAccount initializes a multisig account with the given members and powers | ||
// and returns the account address | ||
func (s *E2ETestSuite) initAccount(ctx context.Context, sender []byte, membersPowers map[string]uint64) ([]byte, string) { | ||
s.fundAccount(ctx, sender, sdk.Coins{sdk.NewCoin("stake", math.NewInt(1000000))}) | ||
|
||
members := []*v1.Member{} | ||
for addrStr, power := range membersPowers { | ||
members = append(members, &v1.Member{Address: addrStr, Weight: power}) | ||
} | ||
|
||
_, accountAddr, err := s.app.AccountsKeeper.Init(ctx, multisigaccount.MULTISIG_ACCOUNT, sender, | ||
&v1.MsgInit{ | ||
Members: members, | ||
Config: &v1.Config{ | ||
Threshold: 100, | ||
Quorum: 100, | ||
VotingPeriod: 120, | ||
Revote: false, | ||
EarlyExecution: true, | ||
}, | ||
}, sdk.Coins{sdk.NewCoin("stake", math.NewInt(1000))}) | ||
s.NoError(err) | ||
|
||
accountAddrStr, err := s.app.AuthKeeper.AddressCodec().BytesToString(accountAddr) | ||
s.NoError(err) | ||
|
||
return accountAddr, accountAddrStr | ||
} | ||
|
||
// createProposal | ||
func (s *E2ETestSuite) createProposal(ctx context.Context, accAddr, sender []byte, msgs ...*codectypes.Any) { | ||
propReq := &v1.MsgCreateProposal{ | ||
Proposal: &v1.Proposal{ | ||
Title: "test", | ||
Summary: "test", | ||
Messages: msgs, | ||
}, | ||
} | ||
err := s.executeTx(ctx, propReq, accAddr, sender) | ||
s.NoError(err) | ||
} | ||
|
||
func (s *E2ETestSuite) executeProposal(ctx context.Context, accAddr, sender []byte, proposalID uint64) error { | ||
execReq := &v1.MsgExecuteProposal{ | ||
ProposalId: proposalID, | ||
} | ||
return s.executeTx(ctx, execReq, accAddr, sender) | ||
} |
Oops, something went wrong.