Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
github.com/golang/mock v1.6.0
github.com/google/gofuzz v1.2.0
github.com/gorilla/mux v1.8.1
github.com/hexops/gotextdiff v1.0.3
github.com/manifoldco/promptui v0.9.0
github.com/ory/dockertest/v3 v3.10.0
github.com/rakyll/statik v0.1.7
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,8 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hdevalence/ed25519consensus v0.1.0 h1:jtBwzzcHuTmFrQN6xQZn6CQEO/V9f7HsjsjeEZ6auqU=
github.com/hdevalence/ed25519consensus v0.1.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
Expand Down
2 changes: 1 addition & 1 deletion proto/atomone/gov/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ option go_package = "github.com/atomone-hub/atomone/x/gov/types/v1";
service Query {
// Constitution queries the chain's constitution.
rpc Constitution(QueryConstitutionRequest) returns (QueryConstitutionResponse) {
option (google.api.http).get = "/cosmos/gov/v1/constitution";
option (google.api.http).get = "/atomone/gov/v1/constitution";
}

// Proposal queries proposal details based on ProposalID.
Expand Down
3 changes: 3 additions & 0 deletions proto/atomone/gov/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,9 @@ message MsgProposeConstitutionAmendment {
// authority is the address that controls the module (defaults to x/gov unless
// overwritten).
string authority = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];

// amendment is the amendment to the constitution. It must be in valid GNU patch format.
string amendment = 2;
}

// MsgProposeConstitutionAmendmentResponse defines the response structure for executing a
Expand Down
55 changes: 55 additions & 0 deletions tests/e2e/e2e_gov_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"path/filepath"
"strconv"
"strings"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
Expand Down Expand Up @@ -176,6 +177,35 @@ func (s *IntegrationTestSuite) testGovParamChange() {
})
}

func (s *IntegrationTestSuite) testGovConstitutionAmendment() {
s.Run("constitution amendment", func() {
chainAAPIEndpoint := fmt.Sprintf("http://%s", s.valResources[s.chainA.id][0].GetHostPort("1317/tcp"))
senderAddress, _ := s.chainA.validators[0].keyInfo.GetAddress()
sender := senderAddress.String()

res := s.queryConstitution(chainAAPIEndpoint)
newConstitution := "New test constitution"
amendment, err := govtypes.GenerateUnifiedDiff(res.Constitution, newConstitution)
s.Require().NoError(err)
s.writeGovConstitutionAmendmentProposal(s.chainA, amendment)
// Gov tests may be run in arbitrary order, each test must increment proposalCounter to have the correct proposal id to submit and query
proposalCounter++
submitGovFlags := []string{configFile(proposalConstitutionAmendmentFilename)}
depositGovFlags := []string{strconv.Itoa(proposalCounter), depositAmount.String()}
voteGovFlags := []string{strconv.Itoa(proposalCounter), "yes"}
s.submitGovProposal(chainAAPIEndpoint, sender, proposalCounter, "gov/MsgSubmitProposal", submitGovFlags, depositGovFlags, voteGovFlags, "vote")

s.Require().Eventually(
func() bool {
res := s.queryConstitution(chainAAPIEndpoint)
return res.Constitution == newConstitution
},
10*time.Second,
time.Second,
)
})
}

func (s *IntegrationTestSuite) submitLegacyGovProposal(chainAAPIEndpoint, sender string, proposalID int, proposalType string, submitFlags []string, depositFlags []string, voteFlags []string, voteCommand string, withDeposit bool) {
s.T().Logf("Submitting Gov Proposal: %s", proposalType)
// min deposit of 1000uatone is required in e2e tests, otherwise the gov antehandler causes the proposal to be dropped
Expand Down Expand Up @@ -283,3 +313,28 @@ func (s *IntegrationTestSuite) writeStakingParamChangeProposal(c *chain, params
err := writeFile(filepath.Join(c.validators[0].configDir(), "config", proposalParamChangeFilename), []byte(propMsgBody))
s.Require().NoError(err)
}

func (s *IntegrationTestSuite) writeGovConstitutionAmendmentProposal(c *chain, amendment string) {
govModuleAddress := authtypes.NewModuleAddress(govtypes.ModuleName).String()
// escape newlines in amendment
amendment = strings.ReplaceAll(amendment, "\n", "\\n")
template := `
{
"messages":[
{
"@type": "/atomone.gov.v1.MsgProposeConstitutionAmendment",
"authority": "%s",
"amendment": "%s"
}
],
"deposit": "100uatone",
"proposer": "Proposing validator address",
"metadata": "Constitution Amendment",
"title": "Constitution Amendment",
"summary": "summary"
}
`
propMsgBody := fmt.Sprintf(template, govModuleAddress, amendment)
err := writeFile(filepath.Join(c.validators[0].configDir(), "config", proposalConstitutionAmendmentFilename), []byte(propMsgBody))
s.Require().NoError(err)
}
9 changes: 5 additions & 4 deletions tests/e2e/e2e_setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,11 @@ const (
numberOfEvidences = 10
slashingShares int64 = 10000

proposalBypassMsgFilename = "proposal_bypass_msg.json"
proposalMaxTotalBypassFilename = "proposal_max_total_bypass.json"
proposalCommunitySpendFilename = "proposal_community_spend.json"
proposalParamChangeFilename = "param_change.json"
proposalBypassMsgFilename = "proposal_bypass_msg.json"
proposalMaxTotalBypassFilename = "proposal_max_total_bypass.json"
proposalCommunitySpendFilename = "proposal_community_spend.json"
proposalParamChangeFilename = "param_change.json"
proposalConstitutionAmendmentFilename = "constitution_amendment.json"

// hermesBinary = "hermes"
// hermesConfigWithGasPrices = "/root/.hermes/config.toml"
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func (s *IntegrationTestSuite) TestGov() {
s.testGovCancelSoftwareUpgrade()
s.testGovCommunityPoolSpend()
s.testGovParamChange()
s.testGovConstitutionAmendment()
}

func (s *IntegrationTestSuite) TestSlashing() {
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ func modifyGenesis(path, moniker, amountStr string, addrAll []sdk.AccAddress, de
govv1.DefaultQuorumTimeout, govv1.DefaultMaxVotingPeriodExtension, govv1.DefaultQuorumCheckCount,
),
)
govGenState.Constitution = "This is a test constitution"
govGenStateBz, err := cdc.MarshalJSON(govGenState)
if err != nil {
return fmt.Errorf("failed to marshal gov genesis state: %w", err)
Expand Down
10 changes: 10 additions & 0 deletions tests/e2e/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"

govtypesv1 "github.com/atomone-hub/atomone/x/gov/types/v1"
govtypesv1beta1 "github.com/atomone-hub/atomone/x/gov/types/v1beta1"
)

Expand Down Expand Up @@ -268,3 +269,12 @@ func (s *IntegrationTestSuite) queryStakingParams(endpoint string) stakingtypes.
s.Require().NoError(err)
return res
}

func (s *IntegrationTestSuite) queryConstitution(endpoint string) govtypesv1.QueryConstitutionResponse {
var res govtypesv1.QueryConstitutionResponse
body, err := httpGet(fmt.Sprintf("%s/atomone/gov/v1/constitution", endpoint))
s.Require().NoError(err)
err = cdc.UnmarshalJSON(body, &res)
s.Require().NoError(err)
return res
}
70 changes: 70 additions & 0 deletions x/gov/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import (
"github.com/cosmos/cosmos-sdk/client/tx"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"

"github.com/atomone-hub/atomone/x/gov/client/utils"
govutils "github.com/atomone-hub/atomone/x/gov/client/utils"
"github.com/atomone-hub/atomone/x/gov/types"
v1 "github.com/atomone-hub/atomone/x/gov/types/v1"
Expand Down Expand Up @@ -72,6 +74,7 @@ func NewTxCmd(legacyPropCmds []*cobra.Command) *cobra.Command {
NewCmdWeightedVote(),
NewCmdSubmitProposal(),
NewCmdDraftProposal(),
NewCmdGenerateConstitutionAmendment(),

// Deprecated
cmdSubmitLegacyProp,
Expand Down Expand Up @@ -375,3 +378,70 @@ $ %s tx gov weighted-vote 1 yes=0.6,no=0.3,abstain=0.1 --from mykey

return cmd
}

// NewCmdConstitutionAmendmentMsg returns the command to generate the sdk.Msg
// required for a constitution amendment proposal generating the unified diff
// between the current constitution (queried) and the updated constitution
// from the provided markdown file.
func NewCmdGenerateConstitutionAmendment() *cobra.Command {
cmd := &cobra.Command{
Use: "generate-constitution-amendment [path/to/updated/constitution.md]",
Args: cobra.ExactArgs(1),
Short: "Generate a constitution amendment proposal message",
Long: strings.TrimSpace(
fmt.Sprintf(`Generate a constitution amendment proposal message from the current
constitution and the provided updated constitution.
Queries the current constitution from the node and generates a
valid constitution amendment proposal message containing the unified diff
between the current constitution and the updated constitution provided
in a markdown file.

NOTE: this is just a utility command, it is not able to generate or submit a valid Tx
to submit on-chain. Use the 'tx gov submit-proposal' command in conjunction with the
result of this one to submit the proposal.

Example:
$ %s tx gov generate-constitution-amendment path/to/updated/constitution.md
`,
version.AppName,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
// Read the updated constitution from the provided markdown file
updatedConstitution, err := readFromMarkdownFile(args[0])
if err != nil {
return err
}

// Query the current constitution
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
queryClient := v1.NewQueryClient(clientCtx)
resp, err := queryClient.Constitution(cmd.Context(), &v1.QueryConstitutionRequest{})
if err != nil {
return err
}

// Generate the unified diff between the current and updated constitutions
diff, err := utils.GenerateUnifiedDiff(resp.Constitution, updatedConstitution)
if err != nil {
return err
}

// Generate the sdk.Msg for the constitution amendment proposal
msg := v1.NewMsgProposeConstitutionAmendment(authtypes.NewModuleAddress(types.ModuleName), diff)
return clientCtx.PrintProto(msg)
},
}

// This is not a tx command (but a utility for the proposal tx), so we don't need to add tx flags.
// It might actually be confusing, so we just add the query flags.
flags.AddQueryFlagsToCmd(cmd)
// query commands have the FlagOutput default to "text", but we want to override it to "json"
// in this case.
cmd.Flags().Set(flags.FlagOutput, "json")

return cmd
}
29 changes: 29 additions & 0 deletions x/gov/client/cli/tx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,3 +496,32 @@ func (s *CLITestSuite) TestNewCmdWeightedVote() {
})
}
}

func (s *CLITestSuite) TestCmdGenerateConstitutionAmendment() {
newConstitution := `Modified Constitution`
newConstitutionFile := testutil.WriteToNewTempFile(s.T(), newConstitution)
defer newConstitutionFile.Close()

testCases := []struct {
name string
args []string
expCmdOutput string
}{
{
"generate constitution amendment",
[]string{newConstitutionFile.Name()},
"{\"authority\":\"cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn\",\"amendment\":\"--- src\\n+++ dst\\n@@ -1 +1 @@\\n-\\n+Modified Constitution\\n\"}",
},
}

for _, tc := range testCases {
tc := tc

s.Run(tc.name, func() {
cmd := cli.NewCmdGenerateConstitutionAmendment()
out, err := clitestutil.ExecTestCLICmd(s.clientCtx, cmd, tc.args)
s.Require().NoError(err)
s.Require().Contains(out.String(), tc.expCmdOutput)
})
}
}
27 changes: 27 additions & 0 deletions x/gov/client/cli/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,30 @@ func ReadGovPropFlags(clientCtx client.Context, flagSet *pflag.FlagSet) (*govv1.

return rv, nil
}

// readFromMarkdownFile reads the contents of a markdown file
// and returns it as a string.
func readFromMarkdownFile(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()

stat, err := file.Stat()
if err != nil {
return "", fmt.Errorf("failed to get file info: %w", err)
}

if stat.Size() == 0 {
return "", fmt.Errorf("file is empty")
}

contents := make([]byte, stat.Size())
_, err = file.Read(contents)
if err != nil {
return "", fmt.Errorf("failed to read file: %w", err)
}

return string(contents), nil
}
35 changes: 35 additions & 0 deletions x/gov/client/utils/unified_diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package utils

import (
"fmt"

"github.com/hexops/gotextdiff"
"github.com/hexops/gotextdiff/myers"
"github.com/hexops/gotextdiff/span"
)

// GenerateUnifiedDiff generates a unified diff from src and dst strings using gotextdiff.
// This is the only function that uses the gotextdiff library as its primary use is for
// clients.
func GenerateUnifiedDiff(src, dst string) (string, error) {
// Create spans for the source and destination texts
srcURI := span.URIFromPath("src")

if src == "" || src[len(src)-1] != '\n' {
src += "\n" // Add an EOL to src if it's empty or newline is missing
}
if dst == "" || dst[len(dst)-1] != '\n' {
dst += "\n" // Add an EOL to dst if it's empty or newline is missing
}

// Compute the edits using the Myers diff algorithm
eds := myers.ComputeEdits(srcURI, src, dst)

// Generate the unified diff string
diff := gotextdiff.ToUnified("src", "dst", src, eds)

// Convert the diff to a string
diffStr := fmt.Sprintf("%v", diff)

return diffStr, nil
}
Loading