diff --git a/CHANGELOG.md b/CHANGELOG.md index acd0c2586..f924c2005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking +* [#1317](https://github.com/NibiruChain/nibiru/pull/1317) - feat(sudo): Implement and test CLI commands for tx and queries. * [#1307](https://github.com/NibiruChain/nibiru/pull/1307) - feat(sudo): Create the x/sudo module + integration tests * [#1299](https://github.com/NibiruChain/nibiru/pull/1299) - feat(wasm): Add peg shift bindings * [#1292](https://github.com/NibiruChain/nibiru/pull/1292) - feat(wasm): Add module bindings for execute calls in x/perp: OpenPosition, ClosePosition, AddMargin, RemoveMargin. @@ -69,8 +70,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Improvements -* [#1321](https://github.com/NibiruChain/nibiru/pull/1321) - build(deps): bump github.com/prometheus/client_golang from 1.15.0 to 1.15.1 +* [#1317](https://github.com/NibiruChain/nibiru/pull/1317) - feat(testutil): Use secp256k1 algo for private key generation in common/testutil. * [#1322](https://gitub.com/NibiruChain/nibiru/pull/1322) - build(deps): Bumps github.com/armon/go-metrics from 0.4.0 to 0.4.1. +* [#1321](https://github.com/NibiruChain/nibiru/pull/1321) - build(deps): bump github.com/prometheus/client_golang from 1.15.0 to 1.15.1 * [#1295](https://github.com/NibiruChain/nibiru/pull/1295) - refactor(app): Organize keepers, store keys, and module manager initialization in app.go * [#1248](https://github.com/NibiruChain/nibiru/pull/1248) - refactor(common): Combine x/testutil and x/common/testutil. * [#1245](https://github.com/NibiruChain/nibiru/pull/1245) - fix(localnet.sh): force localnet.sh to work even if Coingecko is down diff --git a/x/common/testutil/cli/query.go b/x/common/testutil/cli/query.go index 38fdb7d83..5fc2a5fd4 100644 --- a/x/common/testutil/cli/query.go +++ b/x/common/testutil/cli/query.go @@ -17,6 +17,8 @@ import ( perpammtypes "github.com/NibiruChain/nibiru/x/perp/amm/types" perpcli "github.com/NibiruChain/nibiru/x/perp/client/cli" perptypes "github.com/NibiruChain/nibiru/x/perp/types" + sudocli "github.com/NibiruChain/nibiru/x/sudo/cli" + sudotypes "github.com/NibiruChain/nibiru/x/sudo/pb" ) // ExecQueryOption defines a type which customizes a CLI query operation. @@ -114,3 +116,16 @@ func QueryCumulativePremiumFraction(clientCtx client.Context, pair asset.Pair) ( } return &queryResp, nil } + +func QuerySudoers(clientCtx client.Context) (*sudotypes.QuerySudoersResponse, error) { + var queryResp sudotypes.QuerySudoersResponse + if err := ExecQuery( + clientCtx, + sudocli.CmdQuerySudoers(), + []string{}, + &queryResp, + ); err != nil { + return nil, err + } + return &queryResp, nil +} diff --git a/x/common/testutil/genesis/perp_genesis.go b/x/common/testutil/genesis/perp_genesis.go index 2c9237933..25544709a 100644 --- a/x/common/testutil/genesis/perp_genesis.go +++ b/x/common/testutil/genesis/perp_genesis.go @@ -4,9 +4,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/x/common" "github.com/NibiruChain/nibiru/x/common/asset" "github.com/NibiruChain/nibiru/x/common/denoms" + oracletypes "github.com/NibiruChain/nibiru/x/oracle/types" perpammtypes "github.com/NibiruChain/nibiru/x/perp/amm/types" perptypes "github.com/NibiruChain/nibiru/x/perp/types" diff --git a/x/common/testutil/genesis/sudo_genesis.go b/x/common/testutil/genesis/sudo_genesis.go new file mode 100644 index 000000000..6ca750766 --- /dev/null +++ b/x/common/testutil/genesis/sudo_genesis.go @@ -0,0 +1,41 @@ +package genesis + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/NibiruChain/nibiru/app" + + "github.com/NibiruChain/nibiru/x/sudo" + sudotypes "github.com/NibiruChain/nibiru/x/sudo/pb" + + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + + "github.com/NibiruChain/nibiru/x/common/testutil" +) + +func AddSudoGenesis(gen app.GenesisState) ( + genState app.GenesisState, + rootPrivKey cryptotypes.PrivKey, + rootAddr sdk.AccAddress, +) { + sudoGenesis, rootPrivKey, rootAddr := SudoGenesis() + gen[sudotypes.ModuleName] = TEST_ENCODING_CONFIG.Marshaler. + MustMarshalJSON(sudoGenesis) + return gen, rootPrivKey, rootAddr +} + +func SudoGenesis() ( + genState *sudotypes.GenesisState, + rootPrivKey cryptotypes.PrivKey, + rootAddr sdk.AccAddress, +) { + sudoGenesis := sudo.DefaultGenesis() + + // Set the root user + privKeys, addrs := testutil.PrivKeyAddressPairs(1) + rootPrivKey = privKeys[0] + rootAddr = addrs[0] + sudoGenesis.Sudoers.Root = rootAddr.String() + + return sudoGenesis, rootPrivKey, rootAddr +} diff --git a/x/common/testutil/sample.go b/x/common/testutil/sample.go index ee98e152b..79363d6cf 100644 --- a/x/common/testutil/sample.go +++ b/x/common/testutil/sample.go @@ -3,7 +3,8 @@ package testutil import ( "math/rand" - "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/cosmos-sdk/store" sdk "github.com/cosmos/cosmos-sdk/types" @@ -13,15 +14,23 @@ import ( tmdb "github.com/tendermint/tm-db" ) -// AccAddress Returns a sample account address (sdk.AccAddress) +// AccAddress returns a sample address (sdk.AccAddress) created using secp256k1. // Note that AccAddress().String() can be used to get a string representation. func AccAddress() sdk.AccAddress { - pk := ed25519.GenPrivKey().PubKey() - addr := pk.Address() - return sdk.AccAddress(addr) + _, accAddr := PrivKey() + return accAddr +} + +// PrivKey returns a private key and corresponding on-chain address. +func PrivKey() (*secp256k1.PrivKey, sdk.AccAddress) { + privKey := secp256k1.GenPrivKey() + pubKey := privKey.PubKey() + addr := pubKey.Address() + return privKey, sdk.AccAddress(addr) } -// PrivKeyAddressPairs generates (deterministically) a total of n private keys and addresses. +// PrivKeyAddressPairs generates (deterministically) a total of n private keys +// and addresses. func PrivKeyAddressPairs(n int) (keys []cryptotypes.PrivKey, addrs []sdk.AccAddress) { r := rand.New(rand.NewSource(12345)) // make the generation deterministic keys = make([]cryptotypes.PrivKey, n) @@ -32,7 +41,7 @@ func PrivKeyAddressPairs(n int) (keys []cryptotypes.PrivKey, addrs []sdk.AccAddr if err != nil { panic("Could not read randomness") } - keys[i] = ed25519.GenPrivKeyFromSecret(secret) + keys[i] = secp256k1.GenPrivKeyFromSecret(secret) addrs[i] = sdk.AccAddress(keys[i].PubKey().Address()) } return diff --git a/x/sudo/cli/cli.go b/x/sudo/cli/cli.go new file mode 100644 index 000000000..0d2d41126 --- /dev/null +++ b/x/sudo/cli/cli.go @@ -0,0 +1,143 @@ +package cli + +import ( + "fmt" + "os" + "strings" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/version" + + "github.com/spf13/cobra" + + "github.com/NibiruChain/nibiru/x/sudo/pb" +) + +// GetTxCmd returns a cli command for this module's transactions +func GetTxCmd() *cobra.Command { + txCmd := &cobra.Command{ + Use: pb.ModuleName, + Short: fmt.Sprintf("x/%s transaction subcommands", pb.ModuleName), + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + // Add subcommands + txCmd.AddCommand( + CmdEditSudoers(), + ) + + return txCmd +} + +// GetQueryCmd returns a cli command for this module's queries +func GetQueryCmd() *cobra.Command { + moduleQueryCmd := &cobra.Command{ + Use: pb.ModuleName, + Short: fmt.Sprintf( + "Query commands for the x/%s module", pb.ModuleName), + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + // Add subcommands + cmds := []*cobra.Command{ + CmdQuerySudoers(), + } + for _, cmd := range cmds { + moduleQueryCmd.AddCommand(cmd) + } + + return moduleQueryCmd +} + +// CmdEditSudoers is a terminal command corresponding to the EditSudoers +// function of the sdk.Msg handler for x/sudo. +func CmdEditSudoers() *cobra.Command { + cmd := &cobra.Command{ + Use: "edit [edit-json]", + Args: cobra.ExactArgs(1), + Short: "Edit the x/sudo state (sudoers) by adding or removing contracts", + Example: strings.TrimSpace(fmt.Sprintf(` + Example: + $ %s tx sudo edit --from= + `, version.AppName)), + Long: strings.TrimSpace( + `Adds or removes contracts from the x/sudo state, giving the + contracts permissioned access to certain bindings in x/wasm. + + The edit.json for 'EditSudoers' is of the form: + { + "action": "add_contracts", + "contracts": "..." + } + + - Valid action types: "add_contracts", "remove_contracts" + `), + RunE: func(cmd *cobra.Command, args []string) (err error) { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + msg := new(pb.MsgEditSudoers) + + // marshals contents into the proto.Message to which 'msg' points. + contents, err := os.ReadFile(args[0]) + if err != nil { + return err + } + if err = clientCtx.Codec.UnmarshalJSON(contents, msg); err != nil { + return err + } + + // Parse the message sender + from := clientCtx.GetFromAddress() + msg.Sender = from.String() + + if err = msg.ValidateBasic(); err != nil { + return err + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +} + +func CmdQuerySudoers() *cobra.Command { + cmd := &cobra.Command{ + Use: "state", + Short: "displays the internal state (sudoers) of the x/sudo module", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + queryClient := pb.NewQueryClient(clientCtx) + + req := new(pb.QuerySudoersRequest) + resp, err := queryClient.QuerySudoers( + cmd.Context(), req, + ) + if err != nil { + return err + } + + return clientCtx.PrintProto(resp) + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} diff --git a/x/sudo/cli/cli_test.go b/x/sudo/cli/cli_test.go new file mode 100644 index 000000000..1ceebeea0 --- /dev/null +++ b/x/sudo/cli/cli_test.go @@ -0,0 +1,260 @@ +package cli_test + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/gogo/protobuf/jsonpb" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/x/common" + "github.com/NibiruChain/nibiru/x/common/denoms" + "github.com/NibiruChain/nibiru/x/common/set" + "github.com/NibiruChain/nibiru/x/common/testutil" + testutilcli "github.com/NibiruChain/nibiru/x/common/testutil/cli" + "github.com/NibiruChain/nibiru/x/common/testutil/genesis" + "github.com/NibiruChain/nibiru/x/sudo/cli" + "github.com/NibiruChain/nibiru/x/sudo/pb" + + "github.com/cosmos/cosmos-sdk/crypto" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdktestutil "github.com/cosmos/cosmos-sdk/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// ——————————————————————————————————————————————————————————————————— +// MsgEditSudoersPlus +// ——————————————————————————————————————————————————————————————————— + +// MsgEditSudoersPlus is a wrapper struct to extend the default MsgEditSudoers +// type with convenience functions +type MsgEditSudoersPlus struct { + pb.MsgEditSudoers +} + +// ToJson converts the message into a json string and saves it in a temporary +// file, returning the json bytes and file name if done successfully. +func (msg MsgEditSudoersPlus) ToJson(t *testing.T) (fileJsonBz []byte, fileName string) { + require.NoError(t, msg.ValidateBasic()) + + // msgJsonStr showcases a valid example for the cmd args json file. + msgJsonStr := fmt.Sprintf(` + { + "action": "%v", + "contracts": ["%s"], + "sender": "%v" + } + `, msg.Action, strings.Join(msg.Contracts, `", "`), msg.Sender) + + t.Log("check the unmarshal json → proto") + tempMsg := new(pb.MsgEditSudoers) + err := jsonpb.UnmarshalString(msgJsonStr, tempMsg) + assert.NoErrorf(t, err, "DEBUG tempMsg: %v\njsonStr: %v", tempMsg, msgJsonStr) + + t.Log("save example json to a file") + jsonFile := sdktestutil.WriteToNewTempFile( + t, msgJsonStr, + ) + + fileName = jsonFile.Name() + fileJsonBz, err = os.ReadFile(fileName) + assert.NoError(t, err) + return fileJsonBz, fileName +} + +func (msg MsgEditSudoersPlus) Exec( + t *testing.T, + network *testutilcli.Network, + fileName string, + from sdk.AccAddress, +) (*sdk.TxResponse, error) { + args := []string{ + fileName, + } + return testutilcli.ExecTx(network, cli.CmdEditSudoers(), from, args) +} + +type IntegrationSuite struct { + suite.Suite + cfg testutilcli.Config + network *testutilcli.Network + root Account +} + +type Account struct { + privKey cryptotypes.PrivKey + addr sdk.AccAddress + passphrase string +} + +func TestSuite_IntegrationSuite_RunAll(t *testing.T) { + suite.Run(t, new(IntegrationSuite)) +} + +// ——————————————————————————————————————————————————————————————————— +// IntegrationSuite - Setup +// ——————————————————————————————————————————————————————————————————— + +func (s *IntegrationSuite) SetupSuite() { + app.SetPrefixes(app.AccountAddressPrefix) + + genState := genesis.NewTestGenesisState() + genState, rootPrivKey, rootAddr := genesis.AddSudoGenesis(genState) + s.root = Account{ + privKey: rootPrivKey, + addr: rootAddr, + passphrase: "secure-password", + } + s.cfg = testutilcli.BuildNetworkConfig(genState) + s.network = testutilcli.NewNetwork(s.T(), s.cfg) + s.FundRoot(s.root) + s.AddRootToKeyring(s.root) +} + +func (s *IntegrationSuite) FundRoot(root Account) { + val := s.network.Validators[0] + funds := sdk.NewCoins( + sdk.NewInt64Coin(denoms.NIBI, 420*common.TO_MICRO), + ) + feeDenom := denoms.NIBI + s.NoError(testutilcli.FillWalletFromValidator( + root.addr, funds, val, feeDenom, + )) +} + +func (s *IntegrationSuite) AddRootToKeyring(root Account) { + s.T().Log("add the x/sudo root account to the clientCtx.Keyring") + // Encrypt the x/sudo root account's private key to get its "armor" + passphrase := root.passphrase + privKey := root.privKey + armor := crypto.EncryptArmorPrivKey(privKey, passphrase, privKey.Type()) + // Import this account to the keyring + val := s.network.Validators[0] + s.NoError( + val.ClientCtx.Keyring.ImportPrivKey("root", armor, passphrase), + ) +} + +// ——————————————————————————————————————————————————————————————————— +// IntegrationSuite - Tests +// ——————————————————————————————————————————————————————————————————— + +func (s *IntegrationSuite) TestCmdEditSudoers() { + val := s.network.Validators[0] + + _, contractAddrs := testutil.PrivKeyAddressPairs(3) + var contracts []string + for _, addr := range contractAddrs { + contracts = append(contracts, addr.String()) + } + + var sender sdk.AccAddress = s.root.addr + + s.Run("add_contracts", func() { + pbMsg := pb.MsgEditSudoers{ + Action: "add_contracts", + Contracts: []string{contracts[0], contracts[1], contracts[2]}, + Sender: sender.String(), + } + + msg := MsgEditSudoersPlus{pbMsg} + jsonBz, fileName := msg.ToJson(s.T()) + + s.T().Log("sending from the wrong address should fail.") + wrongSender := testutil.AccAddress() + msg.Sender = wrongSender.String() + out, err := msg.Exec(s.T(), s.network, fileName, wrongSender) + s.Assert().Errorf(err, "out: %s\n", out) + s.Contains(err.Error(), "key not found", "msg: %s\nout: %s", jsonBz, out) + + s.T().Log("happy - add_contracts exec tx") + msg.Sender = sender.String() + out, err = msg.Exec(s.T(), s.network, fileName, sender) + s.NoErrorf(err, "msg: %s\nout: %s", jsonBz, out) + }) + + s.Run("query state after add_contracts", func() { + state, err := testutilcli.QuerySudoers(val.ClientCtx) + s.NoError(err) + + gotRoot := state.Sudoers.Root + s.Equal(s.root.addr.String(), gotRoot) + + gotContracts := set.New(state.Sudoers.Contracts...) + s.Equal(len(contracts), gotContracts.Len()) + for _, contract := range contracts { + s.True(gotContracts.Has(contract)) + } + }) + + s.Run("remove_contracts", func() { + pbMsg := pb.MsgEditSudoers{ + Action: "remove_contracts", + Contracts: []string{contracts[1]}, + Sender: sender.String(), + } + + msg := MsgEditSudoersPlus{pbMsg} + jsonBz, fileName := msg.ToJson(s.T()) + + s.T().Log("happy - remove_contracts exec tx") + out, err := msg.Exec(s.T(), s.network, fileName, sender) + s.NoErrorf(err, "msg: %s\nout: %s", jsonBz, out) + }) + + s.Run("query state after remove_contracts", func() { + state, err := testutilcli.QuerySudoers(val.ClientCtx) + s.NoError(err) + + gotRoot := state.Sudoers.Root + s.Equal(s.root.addr.String(), gotRoot) + + wantContracts := []string{contracts[0], contracts[2]} + gotContracts := set.New(state.Sudoers.Contracts...) + s.Equal(len(wantContracts), gotContracts.Len()) + for _, contract := range wantContracts { + s.True(gotContracts.Has(contract)) + } + }) +} + +// TestMarshal_EditSudoers verifies that the expected proto.Message for +// the EditSudoders fn marshals and unmarshals properly from JSON. +// This unmarshaling is used in the main body of the CmdEditSudoers command. +func (s *IntegrationSuite) TestMarshal_EditSudoers() { + t := s.T() + + t.Log("create valid example json for the message") + _, addrs := testutil.PrivKeyAddressPairs(4) + var contracts []string + sender := addrs[0] + for _, addr := range addrs[1:] { + contracts = append(contracts, addr.String()) + } + msg := pb.MsgEditSudoers{ + Action: "add_contracts", + Contracts: contracts, + Sender: sender.String(), + } + require.NoError(t, msg.ValidateBasic()) + + msgPlus := MsgEditSudoersPlus{msg} + fileJsonBz, _ := msgPlus.ToJson(t) + + t.Log("check unmarshal file → proto") + cdc := genesis.TEST_ENCODING_CONFIG.Marshaler + newMsg := new(pb.MsgEditSudoers) + err := cdc.UnmarshalJSON(fileJsonBz, newMsg) + assert.NoErrorf(t, err, "fileJsonBz: #%v", fileJsonBz) + require.NoError(t, newMsg.ValidateBasic(), newMsg.String()) +} + +func (s *IntegrationSuite) TearDownSuite() { + s.T().Log("tearing down integration test suite") + s.network.Cleanup() +} diff --git a/x/sudo/module.go b/x/sudo/module.go index b72c22cbf..712e6af92 100644 --- a/x/sudo/module.go +++ b/x/sudo/module.go @@ -15,8 +15,7 @@ import ( "github.com/spf13/cobra" abci "github.com/tendermint/tendermint/abci/types" - // "github.com/NibiruChain/nibiru/x/sudo/cli" - "github.com/NibiruChain/nibiru/x/perp/client/cli" + "github.com/NibiruChain/nibiru/x/sudo/cli" "github.com/NibiruChain/nibiru/x/sudo/pb" ) diff --git a/x/sudo/sudo.go b/x/sudo/sudo.go index 909e526e1..5359b801a 100644 --- a/x/sudo/sudo.go +++ b/x/sudo/sudo.go @@ -51,6 +51,7 @@ func NewHandler(k Keeper) sdk.Handler { // Ensure the interface is properly implemented at compile time var _ pb.MsgServer = Keeper{} +// EditSudoers adds or removes sudo contracts from state. func (k Keeper) EditSudoers( goCtx context.Context, msg *pb.MsgEditSudoers, ) (*pb.MsgEditSudoersResponse, error) {