cosmos-sdk/x/group/simulation/msg_factory.go
2025-04-14 17:30:59 +00:00

491 lines
17 KiB
Go

package simulation
import (
"context"
"slices"
"strconv"
"sync/atomic"
"time"
"github.com/cosmos/cosmos-sdk/testutil/simsx"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/group"
"github.com/cosmos/cosmos-sdk/x/group/keeper"
)
func MsgCreateGroupFactory() simsx.SimMsgFactoryFn[*group.MsgCreateGroup] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgCreateGroup) {
admin := testData.AnyAccount(reporter, simsx.WithSpendableBalance())
members := genGroupMembersX(testData, reporter)
msg := &group.MsgCreateGroup{Admin: admin.AddressBech32, Members: members, Metadata: testData.Rand().StringN(10)}
return []simsx.SimAccount{admin}, msg
}
}
func MsgCreateGroupPolicyFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgCreateGroupPolicy] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgCreateGroupPolicy) {
groupInfo := randomGroupX(ctx, k, testData, reporter, s)
groupAdmin := testData.GetAccount(reporter, groupInfo.Admin)
if reporter.IsSkipped() {
return nil, nil
}
groupID := groupInfo.Id
r := testData.Rand()
msg, err := group.NewMsgCreateGroupPolicy(
groupAdmin.Address,
groupID,
r.StringN(10),
&group.ThresholdDecisionPolicy{
Threshold: strconv.Itoa(r.IntInRange(1, 10)),
Windows: &group.DecisionPolicyWindows{
VotingPeriod: time.Second * time.Duration(30*24*60*60),
},
},
)
if err != nil {
reporter.Skip(err.Error())
return nil, nil
}
return []simsx.SimAccount{groupAdmin}, msg
}
}
func MsgCreateGroupWithPolicyFactory() simsx.SimMsgFactoryFn[*group.MsgCreateGroupWithPolicy] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgCreateGroupWithPolicy) {
admin := testData.AnyAccount(reporter, simsx.WithSpendableBalance())
members := genGroupMembersX(testData, reporter)
r := testData.Rand()
msg := &group.MsgCreateGroupWithPolicy{
Admin: admin.AddressBech32,
Members: members,
GroupMetadata: r.StringN(10),
GroupPolicyMetadata: r.StringN(10),
GroupPolicyAsAdmin: r.Float32() < 0.5,
}
decisionPolicy := &group.ThresholdDecisionPolicy{
Threshold: strconv.Itoa(r.IntInRange(1, 10)),
Windows: &group.DecisionPolicyWindows{
VotingPeriod: time.Second * time.Duration(30*24*60*60),
},
}
if err := msg.SetDecisionPolicy(decisionPolicy); err != nil {
reporter.Skip(err.Error())
return nil, nil
}
return []simsx.SimAccount{admin}, msg
}
}
func MsgWithdrawProposalFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgWithdrawProposal] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgWithdrawProposal) {
groupInfo, groupPolicy := randomGroupPolicyX(ctx, testData, reporter, k, s)
if reporter.IsSkipped() {
return nil, nil
}
policy, err := groupPolicy.GetDecisionPolicy()
if err != nil {
reporter.Skip(err.Error())
return nil, nil
}
err = policy.Validate(*groupInfo, group.DefaultConfig())
if err != nil {
reporter.Skip(err.Error())
return nil, nil
}
proposalsResult, err := k.ProposalsByGroupPolicy(ctx,
&group.QueryProposalsByGroupPolicyRequest{Address: groupPolicy.Address},
)
if err != nil {
reporter.Skip(err.Error())
return nil, nil
}
now := simsx.BlockTime(ctx)
proposal := simsx.First(proposalsResult.GetProposals(), func(p *group.Proposal) bool {
return p.Status == group.PROPOSAL_STATUS_SUBMITTED && p.VotingPeriodEnd.After(now)
})
if proposal == nil {
reporter.Skip("no proposal found")
return nil, nil
}
// select a random proposer
r := testData.Rand()
proposer := testData.GetAccount(reporter, simsx.OneOf(r, (*proposal).Proposers))
msg := &group.MsgWithdrawProposal{
ProposalId: (*proposal).Id,
Address: proposer.AddressBech32,
}
return []simsx.SimAccount{proposer}, msg
}
}
func MsgVoteFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgVote] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgVote) {
groupInfo, groupPolicy := randomGroupPolicyX(ctx, testData, reporter, k, s)
if reporter.IsSkipped() {
return nil, nil
}
proposalsResult, err := k.ProposalsByGroupPolicy(ctx,
&group.QueryProposalsByGroupPolicyRequest{Address: groupPolicy.Address},
)
if err != nil {
reporter.Skip(err.Error())
return nil, nil
}
now := simsx.BlockTime(ctx)
proposal := simsx.First(proposalsResult.GetProposals(), func(p *group.Proposal) bool {
return p.Status == group.PROPOSAL_STATUS_SUBMITTED && p.VotingPeriodEnd.After(now)
})
if proposal == nil {
reporter.Skip("no proposal found")
return nil, nil
}
// select a random member
r := testData.Rand()
res, err := k.GroupMembers(ctx, &group.QueryGroupMembersRequest{GroupId: groupInfo.Id})
if err != nil {
reporter.Skip(err.Error())
return nil, nil
}
if len(res.Members) == 0 {
reporter.Skip("group has no members")
return nil, nil
}
voter := testData.GetAccount(reporter, simsx.OneOf(r, res.Members).Member.Address)
vRes, err := k.VotesByProposal(ctx, &group.QueryVotesByProposalRequest{
ProposalId: (*proposal).Id,
})
if err != nil {
reporter.Skip(err.Error())
return nil, nil
}
if slices.ContainsFunc(vRes.Votes, func(v *group.Vote) bool { return v.Voter == voter.AddressBech32 }) {
reporter.Skip("voted already on proposal")
return nil, nil
}
msg := &group.MsgVote{
ProposalId: (*proposal).Id,
Voter: voter.AddressBech32,
Option: group.VOTE_OPTION_YES,
Metadata: r.StringN(10),
}
return []simsx.SimAccount{voter}, msg
}
}
func MsgSubmitProposalFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgSubmitProposal] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgSubmitProposal) {
groupInfo, groupPolicy := randomGroupPolicyX(ctx, testData, reporter, k, s)
if reporter.IsSkipped() {
return nil, nil
}
// Return a no-op if we know the proposal cannot be created
policy, err := groupPolicy.GetDecisionPolicy()
if err != nil {
reporter.Skip(err.Error())
return nil, nil
}
if err = policy.Validate(*groupInfo, group.DefaultConfig()); err != nil {
reporter.Skip(err.Error())
return nil, nil
}
// Pick a random member from the group
r := testData.Rand()
res, err := k.GroupMembers(ctx, &group.QueryGroupMembersRequest{GroupId: groupInfo.Id})
if err != nil {
reporter.Skip(err.Error())
return nil, nil
}
if len(res.Members) == 0 {
reporter.Skip("group has no members")
return nil, nil
}
proposer := testData.GetAccount(reporter, simsx.OneOf(r, res.Members).Member.Address)
msg := &group.MsgSubmitProposal{
GroupPolicyAddress: groupPolicy.Address,
Proposers: []string{proposer.AddressBech32},
Metadata: r.StringN(10),
Title: "Test Proposal",
Summary: "Summary of the proposal",
}
return []simsx.SimAccount{proposer}, msg
}
}
func MsgExecFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgExec] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgExec) {
groupPolicy, policyAdmin := randomGroupPolicyWithAdmin(ctx, testData, reporter, k, s)
if reporter.IsSkipped() {
return nil, nil
}
proposalsResult, err := k.ProposalsByGroupPolicy(ctx,
&group.QueryProposalsByGroupPolicyRequest{Address: groupPolicy.Address},
)
if err != nil {
reporter.Skip(err.Error())
return nil, nil
}
proposal := simsx.First(proposalsResult.GetProposals(), func(p *group.Proposal) bool {
return p.Status == group.PROPOSAL_STATUS_ACCEPTED
})
if proposal == nil {
reporter.Skip("no proposal found")
return nil, nil
}
msg := &group.MsgExec{
ProposalId: (*proposal).Id,
Executor: policyAdmin.AddressBech32,
}
return []simsx.SimAccount{policyAdmin}, msg
}
}
func randomGroupPolicyWithAdmin(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter, k keeper.Keeper, s *SharedState) (*group.GroupPolicyInfo, simsx.SimAccount) {
for range 5 {
_, groupPolicy := randomGroupPolicyX(ctx, testData, reporter, k, s)
if groupPolicy != nil && testData.HasAccount(groupPolicy.Admin) {
return groupPolicy, testData.GetAccount(reporter, groupPolicy.Admin)
}
}
reporter.Skip("no group policy found with a sims account")
return nil, simsx.SimAccount{}
}
func MsgUpdateGroupMetadataFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgUpdateGroupMetadata] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgUpdateGroupMetadata) {
groupInfo := randomGroupX(ctx, k, testData, reporter, s)
groupAdmin := testData.GetAccount(reporter, groupInfo.Admin)
if reporter.IsSkipped() {
return nil, nil
}
msg := &group.MsgUpdateGroupMetadata{
GroupId: groupInfo.Id,
Admin: groupAdmin.AddressBech32,
Metadata: testData.Rand().StringN(10),
}
return []simsx.SimAccount{groupAdmin}, msg
}
}
func MsgUpdateGroupAdminFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgUpdateGroupAdmin] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgUpdateGroupAdmin) {
groupInfo := randomGroupX(ctx, k, testData, reporter, s)
groupAdmin := testData.GetAccount(reporter, groupInfo.Admin)
if reporter.IsSkipped() {
return nil, nil
}
newAdmin := testData.AnyAccount(reporter, simsx.ExcludeAccounts(groupAdmin))
msg := &group.MsgUpdateGroupAdmin{
GroupId: groupInfo.Id,
Admin: groupAdmin.AddressBech32,
NewAdmin: newAdmin.AddressBech32,
}
return []simsx.SimAccount{groupAdmin}, msg
}
}
func MsgUpdateGroupMembersFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgUpdateGroupMembers] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgUpdateGroupMembers) {
groupInfo := randomGroupX(ctx, k, testData, reporter, s)
groupAdmin := testData.GetAccount(reporter, groupInfo.Admin)
if reporter.IsSkipped() {
return nil, nil
}
res, err := k.GroupMembers(ctx, &group.QueryGroupMembersRequest{GroupId: groupInfo.Id})
if err != nil {
reporter.Skip("group members not found")
return nil, nil
}
oldMemberAddrs := simsx.Collect(res.Members, func(a *group.GroupMember) string { return a.Member.Address })
members := genGroupMembersX(testData, reporter, simsx.ExcludeAddresses(oldMemberAddrs...))
if len(res.Members) > 1 {
// set existing random group member weight to zero to remove from the group
obsoleteMember := simsx.OneOf(testData.Rand(), res.Members)
obsoleteMember.Member.Weight = "0"
members = append(members, group.MemberToMemberRequest(obsoleteMember.Member))
}
msg := &group.MsgUpdateGroupMembers{
GroupId: groupInfo.Id,
Admin: groupAdmin.AddressBech32,
MemberUpdates: members,
}
return []simsx.SimAccount{groupAdmin}, msg
}
}
func MsgUpdateGroupPolicyAdminFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgUpdateGroupPolicyAdmin] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgUpdateGroupPolicyAdmin) {
groupPolicy, policyAdmin := randomGroupPolicyWithAdmin(ctx, testData, reporter, k, s)
if reporter.IsSkipped() {
return nil, nil
}
newAdmin := testData.AnyAccount(reporter, simsx.ExcludeAccounts(policyAdmin))
msg := &group.MsgUpdateGroupPolicyAdmin{
Admin: policyAdmin.AddressBech32,
GroupPolicyAddress: groupPolicy.Address,
NewAdmin: newAdmin.AddressBech32,
}
return []simsx.SimAccount{policyAdmin}, msg
}
}
func MsgUpdateGroupPolicyDecisionPolicyFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgUpdateGroupPolicyDecisionPolicy] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgUpdateGroupPolicyDecisionPolicy) {
groupPolicy, policyAdmin := randomGroupPolicyWithAdmin(ctx, testData, reporter, k, s)
if reporter.IsSkipped() {
return nil, nil
}
r := testData.Rand()
policyAddr, err := k.AddressCodec().StringToBytes(groupPolicy.Address)
if err != nil {
reporter.Skip(err.Error())
return nil, nil
}
msg, err := group.NewMsgUpdateGroupPolicyDecisionPolicy(policyAdmin.Address, policyAddr, &group.ThresholdDecisionPolicy{
Threshold: strconv.Itoa(r.IntInRange(1, 10)),
Windows: &group.DecisionPolicyWindows{
VotingPeriod: time.Second * time.Duration(r.IntInRange(100, 1000)),
},
})
if err != nil {
reporter.Skip(err.Error())
return nil, nil
}
return []simsx.SimAccount{policyAdmin}, msg
}
}
func MsgUpdateGroupPolicyMetadataFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgUpdateGroupPolicyMetadata] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgUpdateGroupPolicyMetadata) {
groupPolicy, policyAdmin := randomGroupPolicyWithAdmin(ctx, testData, reporter, k, s)
if reporter.IsSkipped() {
return nil, nil
}
msg := &group.MsgUpdateGroupPolicyMetadata{
Admin: policyAdmin.AddressBech32,
GroupPolicyAddress: groupPolicy.Address,
Metadata: testData.Rand().StringN(10),
}
return []simsx.SimAccount{policyAdmin}, msg
}
}
func MsgLeaveGroupFactory(k keeper.Keeper, s *SharedState) simsx.SimMsgFactoryFn[*group.MsgLeaveGroup] {
return func(ctx context.Context, testData *simsx.ChainDataSource, reporter simsx.SimulationReporter) ([]simsx.SimAccount, *group.MsgLeaveGroup) {
groupInfo := randomGroupX(ctx, k, testData, reporter, s)
if reporter.IsSkipped() {
return nil, nil
}
res, err := k.GroupMembers(ctx, &group.QueryGroupMembersRequest{GroupId: groupInfo.Id})
if err != nil {
reporter.Skip("group members not found")
return nil, nil
}
if len(res.Members) == 0 {
reporter.Skip("group has no members")
return nil, nil
}
anyMember := simsx.OneOf(testData.Rand(), res.Members)
leaver := testData.GetAccount(reporter, anyMember.Member.Address)
msg := &group.MsgLeaveGroup{
GroupId: groupInfo.Id,
Address: leaver.AddressBech32,
}
return []simsx.SimAccount{leaver}, msg
}
}
func genGroupMembersX(testData *simsx.ChainDataSource, reporter simsx.SimulationReporter, filters ...simsx.SimAccountFilter) []group.MemberRequest {
r := testData.Rand()
membersCount := r.Intn(5) + 1
members := make([]group.MemberRequest, membersCount)
uniqueAccountsFilter := simsx.UniqueAccounts()
for i := 0; i < membersCount && !reporter.IsSkipped(); i++ {
m := testData.AnyAccount(reporter, append(filters, uniqueAccountsFilter)...)
members[i] = group.MemberRequest{
Address: m.AddressBech32,
Weight: strconv.Itoa(r.IntInRange(1, 10)),
Metadata: r.StringN(10),
}
}
return members
}
func randomGroupX(ctx context.Context, k keeper.Keeper, testdata *simsx.ChainDataSource, reporter simsx.SimulationReporter, s *SharedState) *group.GroupInfo {
r := testdata.Rand()
groupID := k.GetGroupSequence(sdk.UnwrapSDKContext(ctx))
if initialGroupID := s.getMinGroupID(); initialGroupID == unsetGroupID {
s.setMinGroupID(groupID)
} else if initialGroupID < groupID {
groupID = r.Uint64InRange(initialGroupID+1, groupID+1)
}
// when groupID is 0, it proves that SimulateMsgCreateGroup has never been called. that is, no group exists in the chain
if groupID == 0 {
reporter.Skip("no group exists")
return nil
}
res, err := k.GroupInfo(ctx, &group.QueryGroupInfoRequest{GroupId: groupID})
if err != nil {
reporter.Skip(err.Error())
return nil
}
return res.Info
}
func randomGroupPolicyX(
ctx context.Context,
testdata *simsx.ChainDataSource,
reporter simsx.SimulationReporter,
k keeper.Keeper,
s *SharedState,
) (*group.GroupInfo, *group.GroupPolicyInfo) {
for range 5 {
groupInfo := randomGroupX(ctx, k, testdata, reporter, s)
if reporter.IsSkipped() {
return nil, nil
}
groupID := groupInfo.Id
result, err := k.GroupPoliciesByGroup(ctx, &group.QueryGroupPoliciesByGroupRequest{GroupId: groupID})
if err != nil {
reporter.Skip(err.Error())
return nil, nil
}
if len(result.GroupPolicies) != 0 {
return groupInfo, simsx.OneOf(testdata.Rand(), result.GroupPolicies)
}
}
reporter.Skip("no group policies")
return nil, nil
}
// SharedState shared state between message invocations
type SharedState struct {
minGroupID atomic.Uint64
}
// NewSharedState constructor
func NewSharedState() *SharedState {
r := &SharedState{}
r.setMinGroupID(unsetGroupID)
return r
}
func (s *SharedState) getMinGroupID() uint64 {
return s.minGroupID.Load()
}
func (s *SharedState) setMinGroupID(id uint64) {
s.minGroupID.Store(id)
}