diff --git a/app/abci/vote_extension_test.go b/app/abci/vote_extension_test.go index f3faa4d6..ebc153f5 100644 --- a/app/abci/vote_extension_test.go +++ b/app/abci/vote_extension_test.go @@ -34,9 +34,9 @@ func TestExtendVerifyVoteHandlers(t *testing.T) { tmpDir := t.TempDir() valAddr := sdk.ValAddress(simtestutil.CreateRandomAccounts(1)[0]) - pubKeys, err := utils.GenerateSEDAKeys(valAddr, tmpDir) + pubKeys, err := utils.GenerateSEDAKeys(valAddr, tmpDir, "", false) require.NoError(t, err) - signer, err := utils.LoadSEDASigner(filepath.Join(tmpDir, utils.SEDAKeyFileName)) + signer, err := utils.LoadSEDASigner(filepath.Join(tmpDir, utils.SEDAKeyFileName), true) require.NoError(t, err) secp256k1PubKey := pubKeys[utils.SEDAKeyIndexSecp256k1].PubKey diff --git a/app/app.go b/app/app.go index 6da8975d..af91882c 100644 --- a/app/app.go +++ b/app/app.go @@ -1007,7 +1007,7 @@ func NewApp( // Register ABCI handlers for batch signing. pvKeyFile := filepath.Join(homePath, cast.ToString(appOpts.Get("priv_validator_key_file"))) - signer, err := utils.LoadSEDASigner(filepath.Join(filepath.Dir(pvKeyFile), utils.SEDAKeyFileName)) + signer, err := utils.LoadSEDASigner(filepath.Join(filepath.Dir(pvKeyFile), utils.SEDAKeyFileName), utils.ShouldAllowUnencryptedSedaKeys(appOpts)) if err != nil { app.Logger().Error("error loading SEDA signer", "err", err) } diff --git a/app/utils/seda_keys.go b/app/utils/seda_keys.go index bcb6c2d0..96ba153d 100644 --- a/app/utils/seda_keys.go +++ b/app/utils/seda_keys.go @@ -1,8 +1,11 @@ package utils import ( + "crypto/aes" + "crypto/cipher" "crypto/ecdsa" "crypto/rand" + "encoding/base64" "encoding/hex" "encoding/json" "fmt" @@ -11,14 +14,48 @@ import ( "sort" ethcrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/spf13/cast" cmtos "github.com/cometbft/cometbft/libs/os" + servertypes "github.com/cosmos/cosmos-sdk/server/types" sdk "github.com/cosmos/cosmos-sdk/types" pubkeytypes "github.com/sedaprotocol/seda-chain/x/pubkey/types" ) +const ( + // FlagAllowUnencryptedSedaKeys is a flag that allows unencrypted SEDA keys. + FlagAllowUnencryptedSedaKeys = "allow-unencrypted-seda-keys" + // EnvAllowUnencryptedSedaKeys is an environment variable that allows unencrypted SEDA keys. + EnvAllowUnencryptedSedaKeys = "SEDA_ALLOW_UNENCRYPTED_KEYS" + // SEDAKeyEncryptionKeyEnvVar is the environment variable that should contain the SEDA key encryption key. + SEDAKeyEncryptionKeyEnvVar = "SEDA_KEYS_ENCRYPTION_KEY" +) + +func ShouldAllowUnencryptedSedaKeys(appOpts servertypes.AppOptions) bool { + allowUnencryptedFlag := cast.ToBool(appOpts.Get(FlagAllowUnencryptedSedaKeys)) + _, allowUnencryptedInEnv := os.LookupEnv(EnvAllowUnencryptedSedaKeys) + + return allowUnencryptedFlag || allowUnencryptedInEnv +} + +// ReadSEDAKeyEncryptionKeyFromEnv reads the SEDA key encryption key from +// the environment variable. Returns an empty string if the environment +// variable is not set. +func ReadSEDAKeyEncryptionKeyFromEnv() string { + return os.Getenv(SEDAKeyEncryptionKeyEnvVar) +} + +func GenerateSEDAKeyEncryptionKey() (string, error) { + key := make([]byte, 32) + _, err := rand.Read(key) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(key), nil +} + // SEDAKeyIndex enumerates the SEDA key indices. type SEDAKeyIndex uint32 @@ -106,10 +143,11 @@ func (k *indexedPrivKey) UnmarshalJSON(data []byte) error { } // saveSEDAKeyFile saves a given list of indexedPrivKey in the directory -// at dirPath. -func saveSEDAKeyFile(keys []indexedPrivKey, valAddr sdk.ValAddress, dirPath string) error { +// at dirPath. When encryptionKey is not empty, the file is encrypted +// using the provided key and stored as base64 encoded. +func saveSEDAKeyFile(keys []indexedPrivKey, valAddr sdk.ValAddress, dirPath string, encryptionKey string, forceKeyFile bool) error { savePath := filepath.Join(dirPath, SEDAKeyFileName) - if cmtos.FileExists(savePath) { + if SEDAKeyFileExists(dirPath) && !forceKeyFile { return fmt.Errorf("SEDA key file already exists at %s", savePath) } err := cmtos.EnsureDir(filepath.Dir(savePath), 0o700) @@ -125,6 +163,14 @@ func saveSEDAKeyFile(keys []indexedPrivKey, valAddr sdk.ValAddress, dirPath stri return fmt.Errorf("failed to marshal SEDA keys: %v", err) } + if encryptionKey != "" { + encryptedData, err := encryptBytes(jsonBytes, encryptionKey) + if err != nil { + return fmt.Errorf("failed to encrypt SEDA keys: %v", err) + } + jsonBytes = []byte(base64.StdEncoding.EncodeToString(encryptedData)) + } + err = os.WriteFile(savePath, jsonBytes, 0o600) if err != nil { return fmt.Errorf("failed to write SEDA key file: %v", err) @@ -132,12 +178,31 @@ func saveSEDAKeyFile(keys []indexedPrivKey, valAddr sdk.ValAddress, dirPath stri return nil } -// loadSEDAKeyFile loads the SEDA key file from the given path. -func loadSEDAKeyFile(loadPath string) (sedaKeyFile, error) { +func SEDAKeyFileExists(dirPath string) bool { + return cmtos.FileExists(filepath.Join(dirPath, SEDAKeyFileName)) +} + +// loadSEDAKeyFile loads the SEDA key file from the given path. When +// encryptionKey is not empty, the file is processed as base64 encoded +// and then decrypted using the provided key. +func loadSEDAKeyFile(loadPath string, encryptionKey string) (sedaKeyFile, error) { keysJSONBytes, err := os.ReadFile(loadPath) if err != nil { return sedaKeyFile{}, fmt.Errorf("failed to read SEDA keys from %v: %v", loadPath, err) } + + if encryptionKey != "" { + decodedBytes, err := base64.StdEncoding.DecodeString(string(keysJSONBytes)) + if err != nil { + return sedaKeyFile{}, fmt.Errorf("failed to base64 decode SEDA keys: %v", err) + } + decryptedData, err := decryptBytes(decodedBytes, encryptionKey) + if err != nil { + return sedaKeyFile{}, fmt.Errorf("failed to decrypt SEDA keys: %v", err) + } + keysJSONBytes = decryptedData + } + var keyFile sedaKeyFile err = json.Unmarshal(keysJSONBytes, &keyFile) if err != nil { @@ -147,16 +212,13 @@ func loadSEDAKeyFile(loadPath string) (sedaKeyFile, error) { } // LoadSEDAPubKeys loads the SEDA key file from the given path and -// returns a list of index-public key pairs. -func LoadSEDAPubKeys(loadPath string) ([]pubkeytypes.IndexedPubKey, error) { - keysJSONBytes, err := os.ReadFile(loadPath) +// returns a list of index-public key pairs. When encryptionKey is not +// empty, the file is processed as base64 encoded and then decrypted +// using the provided key. +func LoadSEDAPubKeys(loadPath string, encryptionKey string) ([]pubkeytypes.IndexedPubKey, error) { + keyFile, err := loadSEDAKeyFile(loadPath, encryptionKey) if err != nil { - return nil, fmt.Errorf("failed to read SEDA keys from %v: %v", loadPath, err) - } - var keyFile sedaKeyFile - err = json.Unmarshal(keysJSONBytes, &keyFile) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal SEDA keys from %v: %v", loadPath, err) + return nil, err } result := make([]pubkeytypes.IndexedPubKey, len(keyFile.Keys)) @@ -174,8 +236,11 @@ func LoadSEDAPubKeys(loadPath string) ([]pubkeytypes.IndexedPubKey, error) { // GenerateSEDAKeys generates a new set of SEDA keys and saves them to // the SEDA key file, along with the provided validator address. It // returns the resulting index-public key pairs. The key file is stored -// in the directory given by dirPath. -func GenerateSEDAKeys(valAddr sdk.ValAddress, dirPath string) ([]pubkeytypes.IndexedPubKey, error) { +// in the directory given by dirPath. When encryptionKey is not empty, +// the file is encrypted using the provided key and stored as base64 +// encoded. If forceKeyFile is true, the key file is overwritten if it +// already exists. +func GenerateSEDAKeys(valAddr sdk.ValAddress, dirPath string, encryptionKey string, forceKeyFile bool) ([]pubkeytypes.IndexedPubKey, error) { privKeys := make([]indexedPrivKey, 0, len(sedaKeyGenerators)) pubKeys := make([]pubkeytypes.IndexedPubKey, 0, len(sedaKeyGenerators)) for keyIndex, generator := range sedaKeyGenerators { @@ -194,7 +259,7 @@ func GenerateSEDAKeys(valAddr sdk.ValAddress, dirPath string) ([]pubkeytypes.Ind } // The key file is placed in the same directory as the validator key file. - err := saveSEDAKeyFile(privKeys, valAddr, dirPath) + err := saveSEDAKeyFile(privKeys, valAddr, dirPath, encryptionKey, forceKeyFile) if err != nil { return nil, err } @@ -235,3 +300,54 @@ func PubKeyToEthAddress(uncompressed []byte) ([]byte, error) { } return ethcrypto.Keccak256(uncompressed[1:])[12:], nil } + +func encryptBytes(data []byte, key string) ([]byte, error) { + keyBytes, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return nil, err + } + + aes, err := aes.NewCipher(keyBytes) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(aes) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + _, err = rand.Read(nonce) + if err != nil { + return nil, err + } + + return gcm.Seal(nonce, nonce, data, nil), nil +} + +func decryptBytes(data []byte, key string) ([]byte, error) { + keyBytes, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return nil, err + } + + aes, err := aes.NewCipher(keyBytes) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(aes) + if err != nil { + return nil, err + } + + nonceSize := gcm.NonceSize() + nonce, encryptedData := data[:nonceSize], data[nonceSize:] + + decryptedData, err := gcm.Open(nil, nonce, encryptedData, nil) + if err != nil { + return nil, err + } + return decryptedData, nil +} diff --git a/app/utils/seda_keys_test.go b/app/utils/seda_keys_test.go new file mode 100644 index 00000000..69957b78 --- /dev/null +++ b/app/utils/seda_keys_test.go @@ -0,0 +1,107 @@ +package utils_test + +import ( + "encoding/hex" + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/suite" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/stretchr/testify/require" + + "github.com/sedaprotocol/seda-chain/app/params" + "github.com/sedaprotocol/seda-chain/app/utils" +) + +type SEDAKeysTestSuite struct { + suite.Suite +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(SEDAKeysTestSuite)) +} + +func (s *SEDAKeysTestSuite) SetupSuite() { + config := sdk.GetConfig() + config.SetBech32PrefixForAccount(params.Bech32PrefixAccAddr, params.Bech32PrefixAccPub) + config.SetBech32PrefixForValidator(params.Bech32PrefixValAddr, params.Bech32PrefixValPub) + config.SetBech32PrefixForConsensusNode(params.Bech32PrefixConsAddr, params.Bech32PrefixConsPub) + config.Seal() +} + +func (s *SEDAKeysTestSuite) TestSEDAKeyEncryptionDecryption() { + valAddr, err := sdk.ValAddressFromBech32("sedavaloper12rype4zl8wxcgqwl237fll6hvufkgcj8act8xw") + require.NoError(s.T(), err) + + encryptionKey, err := utils.GenerateSEDAKeyEncryptionKey() + require.NoError(s.T(), err) + + tempDir := s.T().TempDir() + generatedKeys, err := utils.GenerateSEDAKeys(valAddr, tempDir, encryptionKey, false) + + s.Require().NoError(err) + s.Require().Equal(utils.SEDAKeyIndex(generatedKeys[0].Index), utils.SEDAKeyIndexSecp256k1) + s.Require().NotEmpty(generatedKeys[0].PubKey, "public key should not be empty") + + keyfilePath := filepath.Join(tempDir, utils.SEDAKeyFileName) + + invalidKey, err := utils.GenerateSEDAKeyEncryptionKey() + s.Require().NoError(err) + + _, err = utils.LoadSEDAPubKeys(keyfilePath, invalidKey) + s.Require().ErrorContains(err, "cipher: message authentication failed") + + loadedKeys, err := utils.LoadSEDAPubKeys(keyfilePath, encryptionKey) + s.Require().NoError(err) + s.Require().Equal(generatedKeys[0].PubKey, loadedKeys[0].PubKey) +} + +func (s *SEDAKeysTestSuite) TestSEDAKeyDecryptionExistingFile() { + tempDir := s.T().TempDir() + err := os.WriteFile(filepath.Join(tempDir, utils.SEDAKeyFileName), []byte("kNYhCAjfN9BhJ46iYzJWCUXn9efOAGf30D81UjF5tRlRtdiziW1zGVK+6ehxeJXKcPAmWjQkTxAKcJv7ozAA0xdleR4yO6HakROtFRXlOBy3K9Fv6rkDfCmbIUUjOH9oGP2F5+ldKeE5030MOdNORWUKW7fIlnKUyBWTZfLSmsKi+iCaIyZ/bFh2+NDiESPHAYl+X8t+SKKy6MgAwarrW9W1/6enNLoVmF8dAJ1dhxeKyXF/aXWKR7HaMRwe7V1NjfnaFcI09CeibpWud9rYKhbjV3K0/RdBobjPTIHAnLd5erh/3eVo9RGm8bC8a97obKm68lDernSN9HvjoTO3QlvI0k7cVDAhiuphS4qlgjOVW+eWm+S5dlD2gpCExcmrqxbggLOtjoZbQyrKhQFmfn5UGonoDTSbwtbZZtvY1N48AVT4eueReBWumcipO0ViWnkxLNIJ8vFA"), 0o600) + s.Require().NoError(err) + + _, err = utils.LoadSEDAPubKeys(filepath.Join(tempDir, utils.SEDAKeyFileName), "xmp1EDn7ndgZIdgwupJ9yfDWlSssubKpgo2ZHqjx+4w=") + s.Require().ErrorContains(err, "cipher: message authentication failed") + + keys, err := utils.LoadSEDAPubKeys(filepath.Join(tempDir, utils.SEDAKeyFileName), "La1PSNwUBZXEoIQ1CM0VF+kRr9vqforxE97afYdTF+c=") + s.Require().NoError(err) + s.Require().Equal(hex.EncodeToString(keys[0].PubKey), "04be41e55492d9d823c435b6b6801413223b31fdfa0318d2dea51e1886215e8664e234c34afa7af32ec02a1d0289ce656bab3ed106646836c9d26ce35968b2ff68") +} + +func (s *SEDAKeysTestSuite) TestSEDAKeyWithoutEncryption() { + valAddr, err := sdk.ValAddressFromBech32("sedavaloper12rype4zl8wxcgqwl237fll6hvufkgcj8act8xw") + require.NoError(s.T(), err) + + tempDir := s.T().TempDir() + generatedKeys, err := utils.GenerateSEDAKeys(valAddr, tempDir, "", false) + + s.Require().NoError(err) + s.Require().Equal(utils.SEDAKeyIndex(generatedKeys[0].Index), utils.SEDAKeyIndexSecp256k1) + s.Require().NotEmpty(generatedKeys[0].PubKey, "public key should not be empty") + + keyfilePath := filepath.Join(tempDir, utils.SEDAKeyFileName) + keys, err := os.ReadFile(keyfilePath) + s.Require().NoError(err) + + // We only verify the top level of the JSON schema, this should be enough + // to ensure that the file was not encrypted. + type jsonSchema struct { + ValidatorAddr sdk.ValAddress `json:"validator_addr"` + Keys []interface{} `json:"keys"` + } + + var sedaKeyFile jsonSchema + s.Require().NoError(json.Unmarshal(keys, &sedaKeyFile)) + s.Require().Equal(sedaKeyFile.ValidatorAddr, valAddr) + s.Require().Equal(len(sedaKeyFile.Keys), 1) + + // Test that the file can be loaded without encryption. + loadedKeys, err := utils.LoadSEDAPubKeys(keyfilePath, "") + s.Require().NoError(err) + s.Require().Equal(generatedKeys[0].PubKey, loadedKeys[0].PubKey) +} diff --git a/app/utils/seda_signer.go b/app/utils/seda_signer.go index a1469c6a..ae4ade6d 100644 --- a/app/utils/seda_signer.go +++ b/app/utils/seda_signer.go @@ -31,8 +31,8 @@ type sedaKeys struct { // LoadSEDASigner loads the SEDA keys from a given file path and // returns a SEDASigner interface. -func LoadSEDASigner(keyFilePath string) (SEDASigner, error) { - keys, err := loadSEDAKeys(keyFilePath) +func LoadSEDASigner(keyFilePath string, allowUnencrypted bool) (SEDASigner, error) { + keys, err := loadSEDAKeys(keyFilePath, allowUnencrypted) if err != nil { keys.keyPath = keyFilePath keys.isLoaded = false @@ -41,8 +41,15 @@ func LoadSEDASigner(keyFilePath string) (SEDASigner, error) { return &keys, nil } -func loadSEDAKeys(keyFilePath string) (keys sedaKeys, err error) { - keyFile, err := loadSEDAKeyFile(keyFilePath) +func loadSEDAKeys(keyFilePath string, allowUnencrypted bool) (keys sedaKeys, err error) { + encryptionKey := ReadSEDAKeyEncryptionKeyFromEnv() + if encryptionKey == "" && !allowUnencrypted { + panic(fmt.Sprintf("SEDA key encryption key is not set, set the %s environment variable or run with --%s", SEDAKeyEncryptionKeyEnvVar, FlagAllowUnencryptedSedaKeys)) + } else if encryptionKey != "" && allowUnencrypted { + panic(fmt.Sprintf("SEDA key encryption key is set, but --%s flag is also set", FlagAllowUnencryptedSedaKeys)) + } + + keyFile, err := loadSEDAKeyFile(keyFilePath, encryptionKey) if err != nil { return keys, err } @@ -124,7 +131,10 @@ func (s *sedaKeys) IsLoaded() bool { // Reload reloads the signer from the key file. func (s *sedaKeys) reload() error { - keys, err := loadSEDAKeys(s.keyPath) + // Reload should run from the same process as the one that loaded the signer, + // so the check for the encryption key should already have passed if we're + // hitting this function. + keys, err := loadSEDAKeys(s.keyPath, true) if err != nil { s.valAddr = nil s.keys = nil diff --git a/cmd/sedad/cmd/root.go b/cmd/sedad/cmd/root.go index 0e094cf1..03a8ec15 100644 --- a/cmd/sedad/cmd/root.go +++ b/cmd/sedad/cmd/root.go @@ -53,6 +53,7 @@ import ( "github.com/sedaprotocol/seda-chain/app" appparams "github.com/sedaprotocol/seda-chain/app/params" + "github.com/sedaprotocol/seda-chain/app/utils" _ "github.com/sedaprotocol/seda-chain/client/docs/statik" // for swagger docs "github.com/sedaprotocol/seda-chain/cmd/sedad/gentx" ) @@ -77,8 +78,11 @@ func NewRootCmd() *cobra.Command { map[int64]bool{}, app.DefaultNodeHome, 0, - simtestutil.NewAppOptionsWithFlagHome(tempDir()), - tempDir(), + simtestutil.AppOptionsMap{ + // taken from simtestutil.NewAppOptionsWithFlagHome(tempDir()) + sdkflags.FlagHome: tempDir(), + utils.FlagAllowUnencryptedSedaKeys: "true", + }, tempDir(), baseapp.SetChainID("tempchainid"), ) encodingConfig := app.EncodingConfig{ @@ -269,6 +273,7 @@ func txCommand(_ module.BasicManager) *cobra.Command { func addModuleInitFlags(startCmd *cobra.Command) { crisis.AddModuleInitFlags(startCmd) + startCmd.Flags().Bool(utils.FlagAllowUnencryptedSedaKeys, false, "Allow an unencrypted SEDA key file") } func overwriteFlagDefaults(c *cobra.Command, defaults map[string]string) { //nolint:unused // unused diff --git a/cmd/sedad/gentx/gentx.go b/cmd/sedad/gentx/gentx.go index 200fded4..b14e2952 100644 --- a/cmd/sedad/gentx/gentx.go +++ b/cmd/sedad/gentx/gentx.go @@ -27,7 +27,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/genutil/types" "github.com/cosmos/cosmos-sdk/x/staking/client/cli" - "github.com/sedaprotocol/seda-chain/app/utils" + pubkeycli "github.com/sedaprotocol/seda-chain/x/pubkey/client/cli" customcli "github.com/sedaprotocol/seda-chain/x/staking/client/cli" stakingtypes "github.com/sedaprotocol/seda-chain/x/staking/types" ) @@ -146,7 +146,7 @@ $ %s gentx my-key-name 1000000seda --home=/path/to/home/dir --keyring-backend=os if valAddr.Empty() { return fmt.Errorf("set the from address using --from flag") } - pks, err := utils.GenerateSEDAKeys(valAddr, filepath.Dir(config.PrivValidatorKeyFile())) + pks, err := pubkeycli.LoadOrGenerateSEDAKeys(cmd, valAddr) if err != nil { return err } @@ -255,6 +255,7 @@ $ %s gentx my-key-name 1000000seda --home=/path/to/home/dir --keyring-backend=os cmd.Flags().String(flags.FlagHome, defaultNodeHome, "The application home directory") cmd.Flags().String(flags.FlagOutputDocument, "", "Write the genesis transaction JSON document to the given file instead of the default location") cmd.Flags().AddFlagSet(fsCreateValidator) + pubkeycli.AddSedaKeysFlagsToCmd(cmd) flags.AddTxFlagsToCmd(cmd) _ = cmd.Flags().MarkHidden(flags.FlagOutput) // signing makes sense to output only json diff --git a/dockerfiles/Dockerfile.e2e b/dockerfiles/Dockerfile.e2e index 11139964..4b7cb250 100644 --- a/dockerfiles/Dockerfile.e2e +++ b/dockerfiles/Dockerfile.e2e @@ -69,4 +69,7 @@ EXPOSE 26656 26657 1317 9090 # USER nonroot -CMD ["sedad", "start"] +# Allow CLI commands to not encrypt the SEDA keys file +ENV SEDA_ALLOW_UNENCRYPTED_KEYS=true + +CMD ["sedad", "start", "--allow-unencrypted-seda-keys"] diff --git a/e2e/e2e_setup_test.go b/e2e/e2e_setup_test.go index e789a727..fd789bab 100644 --- a/e2e/e2e_setup_test.go +++ b/e2e/e2e_setup_test.go @@ -208,7 +208,7 @@ func (s *IntegrationTestSuite) initGenesis(c *chain) { selfDelegationCoin, err := sdk.ParseCoinNormalized(selfDelegationStr) s.Require().NoError(err) - createValmsg, err := val.buildCreateValidatorMsg(selfDelegationCoin) + createValmsg, err := val.buildCreateValidatorMsg(selfDelegationCoin, val.configDir()) s.Require().NoError(err) signedTx, err := val.signMsg(createValmsg) diff --git a/e2e/validator.go b/e2e/validator.go index 099b18a4..aeeaba04 100644 --- a/e2e/validator.go +++ b/e2e/validator.go @@ -30,6 +30,8 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/sedaprotocol/seda-chain/app" + "github.com/sedaprotocol/seda-chain/app/utils" + sedastakingtypes "github.com/sedaprotocol/seda-chain/x/staking/types" ) type validator struct { @@ -218,7 +220,7 @@ func (v *validator) createKey(name string) error { return v.createKeyFromMnemonic(name, mnemonic) } -func (v *validator) buildCreateValidatorMsg(amount sdk.Coin) (sdk.Msg, error) { +func (v *validator) buildCreateValidatorMsg(amount sdk.Coin, valHomeDir string) (sdk.Msg, error) { description := stakingtypes.NewDescription(v.moniker, "", "", "", "") commissionRates := stakingtypes.CommissionRates{ Rate: math.LegacyMustNewDecFromStr("0.1"), @@ -236,9 +238,15 @@ func (v *validator) buildCreateValidatorMsg(amount sdk.Coin) (sdk.Msg, error) { return nil, err } - return stakingtypes.NewMsgCreateValidator( + sedaPubKeys, err := utils.GenerateSEDAKeys(sdk.ValAddress(valAddr), filepath.Join(valHomeDir, "config"), "", true) + if err != nil { + return nil, err + } + + return sedastakingtypes.NewMsgCreateSEDAValidator( sdk.ValAddress(valAddr).String(), valPubKey, + sedaPubKeys, amount, description, commissionRates, diff --git a/go.mod b/go.mod index 72ca8ecf..75769bcf 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/CosmWasm/wasmd v0.53.0 github.com/CosmWasm/wasmvm/v2 v2.1.3 github.com/aws/aws-sdk-go v1.55.5 + github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 github.com/cometbft/cometbft v0.38.12 github.com/cosmos/cosmos-db v1.1.0 github.com/cosmos/cosmos-proto v1.0.0-beta.5 @@ -79,7 +80,6 @@ require ( github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect - github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/btcsuite/btcd v0.21.0-beta // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect diff --git a/scripts/deploy_contracts.sh b/scripts/deploy_contracts.sh index 7ece0cf0..5b6e09f7 100755 --- a/scripts/deploy_contracts.sh +++ b/scripts/deploy_contracts.sh @@ -13,12 +13,19 @@ VOTE_ACCOUNT=$($BIN keys show satoshi --keyring-backend test -a) # for sending v echo "Deploying core contract" -OUTPUT="$($BIN tx wasm store $CONTRACT_WASM --node $RPC_URL --from $DEV_ACCOUNT --keyring-backend test --gas-prices 100000000000aseda --gas auto --gas-adjustment 1.3 -y --output json --chain-id $CHAIN_ID)" -TXHASH=$(echo $OUTPUT | jq -r '.txhash') - -sleep 10 +OUTPUT="$( + $BIN tx wasm store $CONTRACT_WASM \ + --node $RPC_URL \ + --chain-id $CHAIN_ID \ + --from $DEV_ACCOUNT \ + --keyring-backend test \ + --gas-prices 100000000000aseda \ + --gas auto \ + --gas-adjustment 1.3 \ + -y --output json \ + | $BIN query wait-tx --node $RPC_URL --output json\ +)" -OUTPUT="$($BIN query tx $TXHASH --node $RPC_URL --output json)" CORE_CODE_ID=$(echo "$OUTPUT" | jq -r '.events[] | select(.type=="store_code") | .attributes[] | select(.key=="code_id") | .value') echo "Instantiating core contract on code id $CORE_CODE_ID" @@ -29,19 +36,28 @@ OUTPUT=$($BIN tx wasm-storage submit-proposal instantiate-core-contract $CORE_CO --admin $DEV_ACCOUNT \ --label core$CORE_CODE_ID \ --title 'Core Contract' --summary 'Instantiates and registers core contract' --deposit 10000000aseda \ - --from $DEV_ACCOUNT --keyring-backend test \ --node $RPC_URL \ - --gas-prices 100000000000aseda --gas auto --gas-adjustment 1.5 \ - --output json --chain-id $CHAIN_ID -y) -TXHASH=$(echo "$OUTPUT" | jq -r '.txhash') + --chain-id $CHAIN_ID \ + --from $DEV_ACCOUNT \ + --keyring-backend test \ + --gas-prices 100000000000aseda \ + --gas auto \ + --gas-adjustment 1.5 \ + -y --output json \ + | $BIN query wait-tx --node $RPC_URL --output json\ +) -sleep 10 - -PROPOSAL_ID="$($BIN query tx $TXHASH --output json | jq '.events[] | select(.type == "submit_proposal") | .attributes[] | select(.key == "proposal_id") | .value' | sed 's/^"\(.*\)"$/\1/')" +PROPOSAL_ID="$(echo "$OUTPUT" | jq '.events[] | select(.type == "submit_proposal") | .attributes[] | select(.key == "proposal_id") | .value' | sed 's/^"\(.*\)"$/\1/')" $BIN tx gov vote $PROPOSAL_ID yes \ - --from $VOTE_ACCOUNT --keyring-backend test \ - --gas-prices 100000000000aseda --gas auto --gas-adjustment 1.6 \ - --chain-id $CHAIN_ID -y + --node $RPC_URL \ + --chain-id $CHAIN_ID \ + --from $VOTE_ACCOUNT \ + --keyring-backend test \ + --gas-prices 100000000000aseda \ + --gas auto \ + --gas-adjustment 1.6 \ + -y \ + | $BIN query wait-tx --node $RPC_URL --output json sleep $VOTING_PERIOD diff --git a/scripts/local_multi_setup.sh b/scripts/local_multi_setup.sh index 25d38313..8888e0b3 100755 --- a/scripts/local_multi_setup.sh +++ b/scripts/local_multi_setup.sh @@ -39,7 +39,7 @@ $BIN keys add validator4 --keyring-backend=test --home=$HOME/.sedad/validator4 # create validator node with tokens to transfer to the three other nodes $BIN add-genesis-account $($BIN keys show validator1 -a --keyring-backend=test --home=$HOME/.sedad/validator1) 100000000000000000000aseda --home=$HOME/.sedad/validator1 -$BIN gentx validator1 10000000000000000000aseda --keyring-backend=test --home=$HOME/.sedad/validator1 --chain-id=testing +$BIN gentx validator1 10000000000000000000aseda --keyring-backend=test --home=$HOME/.sedad/validator1 --key-file-no-encryption --chain-id=testing $BIN collect-gentxs --home=$HOME/.sedad/validator1 # change app.toml values @@ -116,19 +116,19 @@ sed -i '' -E "s|persistent_peers = \"\"|persistent_peers = \"${NODE1_ID}@localho # start all four validators tmux new-session -s validator1 -d tmux send -t validator1 'BIN=./build/sedad' ENTER -tmux send -t validator1 '$BIN start --home=$HOME/.sedad/validator1 --log_level debug > val1_multi_local.log 2>&1 &' ENTER +tmux send -t validator1 '$BIN start --home=$HOME/.sedad/validator1 --allow-unencrypted-seda-keys --log_level debug > val1_multi_local.log 2>&1 &' ENTER tmux new-session -s validator2 -d tmux send -t validator2 'BIN=./build/sedad' ENTER -tmux send -t validator2 '$BIN start --home=$HOME/.sedad/validator2 --log_level debug > val2_multi_local.log 2>&1 &' ENTER +tmux send -t validator2 '$BIN start --home=$HOME/.sedad/validator2 --allow-unencrypted-seda-keys --log_level debug > val2_multi_local.log 2>&1 &' ENTER tmux new-session -s validator3 -d tmux send -t validator3 'BIN=./build/sedad' ENTER -tmux send -t validator3 '$BIN start --home=$HOME/.sedad/validator3 --log_level debug > val3_multi_local.log 2>&1 &' ENTER +tmux send -t validator3 '$BIN start --home=$HOME/.sedad/validator3 --allow-unencrypted-seda-keys --log_level debug > val3_multi_local.log 2>&1 &' ENTER tmux new-session -s validator4 -d tmux send -t validator4 'BIN=./build/sedad' ENTER -tmux send -t validator4 '$BIN start --home=$HOME/.sedad/validator4 --log_level debug > val4_multi_local.log 2>&1 &' ENTER +tmux send -t validator4 '$BIN start --home=$HOME/.sedad/validator4 --allow-unencrypted-seda-keys --log_level debug > val4_multi_local.log 2>&1 &' ENTER echo "begin sending funds to validators 2, 3, & 4" sleep 10 @@ -155,7 +155,7 @@ cat << EOF > validator2.json "min-self-delegation": "1" } EOF -$BIN tx staking create-validator validator2.json --from=validator2 --keyring-backend=test --home=$HOME/.sedad/validator2 --broadcast-mode sync --chain-id=testing --node http://localhost:26657 --yes --gas-prices 10000000000aseda --gas auto --gas-adjustment 1.7 +$BIN tx staking create-validator validator2.json --from=validator2 --keyring-backend=test --home=$HOME/.sedad/validator2 --broadcast-mode sync --chain-id=testing --node http://localhost:26657 --yes --gas-prices 10000000000aseda --gas auto --gas-adjustment 1.7 --key-file-no-encryption rm validator2.json cat << EOF > validator3.json @@ -173,7 +173,7 @@ cat << EOF > validator3.json "min-self-delegation": "1" } EOF -$BIN tx staking create-validator validator3.json --from=validator3 --keyring-backend=test --home=$HOME/.sedad/validator3 --broadcast-mode sync --chain-id=testing --node http://localhost:26657 --yes --gas-prices 10000000000aseda --gas auto --gas-adjustment 1.7 +$BIN tx staking create-validator validator3.json --from=validator3 --keyring-backend=test --home=$HOME/.sedad/validator3 --broadcast-mode sync --chain-id=testing --node http://localhost:26657 --yes --gas-prices 10000000000aseda --gas auto --gas-adjustment 1.7 --key-file-no-encryption rm validator3.json cat << EOF > validator4.json diff --git a/scripts/local_setup.sh b/scripts/local_setup.sh index ebcab8a1..78191f71 100755 --- a/scripts/local_setup.sh +++ b/scripts/local_setup.sh @@ -90,10 +90,10 @@ VESTING_END=$((VESTING_START + 100000)) $BIN add-genesis-account vesttest 10000seda --vesting-amount 10000seda --vesting-start-time $VESTING_START --vesting-end-time $VESTING_END --funder seda1jq60my60e87arglrazfpqn753hx0pzcatdek76 --keyring-backend test # create a default validator -$BIN gentx satoshi 10000000000000000seda --keyring-backend test +$BIN gentx satoshi 10000000000000000seda --keyring-backend test --key-file-no-encryption # collect genesis txns $BIN collect-gentxs # start the chain -$BIN start --log_level debug 2>&1 | tee local_chain.log & +$BIN start --log_level debug --allow-unencrypted-seda-keys 2>&1 | tee local_chain.log diff --git a/simulation/.gitignore b/simulation/.gitignore new file mode 100644 index 00000000..2eeaac97 --- /dev/null +++ b/simulation/.gitignore @@ -0,0 +1,2 @@ +# Keys generated by the simulation +seda_keys.json diff --git a/simulation/simulation_test.go b/simulation/simulation_test.go index 9d207d38..e2f4e5b5 100644 --- a/simulation/simulation_test.go +++ b/simulation/simulation_test.go @@ -38,6 +38,7 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/sedaprotocol/seda-chain/app" + "github.com/sedaprotocol/seda-chain/app/utils" ) // Get flags every time the simulator is run @@ -73,6 +74,7 @@ func BenchmarkSimulation(b *testing.B) { appOptions := make(simtestutil.AppOptionsMap, 0) appOptions[flags.FlagHome] = app.DefaultNodeHome appOptions[server.FlagInvCheckPeriod] = simcli.FlagPeriodValue + appOptions[utils.FlagAllowUnencryptedSedaKeys] = "true" bApp := app.NewApp( logger, @@ -135,6 +137,7 @@ func TestAppStateDeterminism(t *testing.T) { ) appOptions[flags.FlagHome] = app.DefaultNodeHome appOptions[server.FlagInvCheckPeriod] = simcli.FlagPeriodValue + appOptions[utils.FlagAllowUnencryptedSedaKeys] = "true" for i := 0; i < numSeeds; i++ { config.Seed = r.Int63() @@ -227,6 +230,7 @@ func TestAppExportImport(t *testing.T) { appOptions := make(simtestutil.AppOptionsMap, 0) appOptions[flags.FlagHome] = app.DefaultNodeHome appOptions[server.FlagInvCheckPeriod] = simcli.FlagPeriodValue + appOptions[utils.FlagAllowUnencryptedSedaKeys] = "true" bApp := app.NewApp( logger, @@ -382,6 +386,7 @@ func TestAppSimulationAfterImport(t *testing.T) { appOptions := make(simtestutil.AppOptionsMap, 0) appOptions[flags.FlagHome] = app.DefaultNodeHome appOptions[server.FlagInvCheckPeriod] = simcli.FlagPeriodValue + appOptions[utils.FlagAllowUnencryptedSedaKeys] = "true" bApp := app.NewApp( logger, diff --git a/x/pubkey/client/cli/keyfile.go b/x/pubkey/client/cli/keyfile.go new file mode 100644 index 00000000..13354ac5 --- /dev/null +++ b/x/pubkey/client/cli/keyfile.go @@ -0,0 +1,133 @@ +package cli + +import ( + "bufio" + "crypto/aes" + "encoding/base64" + "fmt" + "os" + "path/filepath" + + "github.com/bgentry/speakeasy" + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client/input" + "github.com/cosmos/cosmos-sdk/server" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/sedaprotocol/seda-chain/app/utils" + "github.com/sedaprotocol/seda-chain/x/pubkey/types" +) + +func LoadOrGenerateSEDAKeys(cmd *cobra.Command, valAddr sdk.ValAddress) ([]types.IndexedPubKey, error) { + serverCfg := server.GetServerContextFromCmd(cmd).Config + + encryptionKeyEnv := utils.ReadSEDAKeyEncryptionKeyFromEnv() + useCustomEncryptionKey, err := cmd.Flags().GetBool(FlagEncryptionKey) + if err != nil { + return nil, err + } + + if useCustomEncryptionKey && encryptionKeyEnv != "" { + return nil, fmt.Errorf("both --%s flag and %s environment variable are set", FlagEncryptionKey, utils.SEDAKeyEncryptionKeyEnvVar) + } + + encryptionKey := encryptionKeyEnv + if useCustomEncryptionKey { + customKey, err := speakeasy.FAsk(os.Stderr, "Enter the custom encryption key\n") + if err != nil { + return nil, err + } + confirmation, err := speakeasy.FAsk(os.Stderr, "Confirm the custom encryption key\n") + if err != nil { + return nil, err + } + if confirmation != customKey { + return nil, fmt.Errorf("custom encryption key confirmation does not match") + } + + customKeyBytes, err := base64.StdEncoding.DecodeString(customKey) + if err != nil { + return nil, fmt.Errorf("invalid base64 encoded key: %w", err) + } + + _, err = aes.NewCipher(customKeyBytes) + if err != nil { + return nil, fmt.Errorf("invalid AES key: %w", err) + } + + encryptionKey = customKey + } + + var pks []types.IndexedPubKey + keyFile, err := cmd.Flags().GetString(FlagKeyFile) + if err != nil { + return nil, err + } + + if keyFile != "" { + pks, err = utils.LoadSEDAPubKeys(keyFile, encryptionKey) + if err != nil { + return nil, err + } + } else { + encryptionKey, err = getSEDAKeysEncryptionKey(cmd, encryptionKey) + if err != nil { + return nil, err + } + + forceKeyFile, err := cmd.Flags().GetBool(FlagForceKeyFile) + if err != nil { + return nil, err + } + + if utils.SEDAKeyFileExists(filepath.Dir(serverCfg.PrivValidatorKeyFile())) && !forceKeyFile { + reader := bufio.NewReader(os.Stdin) + overwrite, err := input.GetConfirmation("SEDA key file already exists, overwrite?", reader, os.Stderr) + if err != nil { + return nil, err + } + + forceKeyFile = overwrite + } + + pks, err = utils.GenerateSEDAKeys(valAddr, filepath.Dir(serverCfg.PrivValidatorKeyFile()), encryptionKey, forceKeyFile) + if err != nil { + return nil, err + } + } + + return pks, nil +} + +func getSEDAKeysEncryptionKey(cmd *cobra.Command, encryptionKey string) (string, error) { + if encryptionKey != "" { + return encryptionKey, nil + } + + noEncryptionFlag, err := cmd.Flags().GetBool(FlagNoEncryption) + if err != nil { + return "", err + } + + _, allowUnencrypted := os.LookupEnv(utils.EnvAllowUnencryptedSedaKeys) + if noEncryptionFlag || allowUnencrypted { + return "", nil + } + + encryptionKey, err = utils.GenerateSEDAKeyEncryptionKey() + if err != nil { + return "", err + } + + reader := bufio.NewReader(os.Stdin) + confirmation, err := input.GetConfirmation(fmt.Sprintf("\n**Important** take note of this encryption key.\nIt is required as an env variable (%s) when running the node.\n\n%s\n", utils.SEDAKeyEncryptionKeyEnvVar, encryptionKey), reader, os.Stderr) + if err != nil { + return "", err + } + if !confirmation { + return "", fmt.Errorf("user did not confirm the generated encryption key") + } + + return encryptionKey, nil +} diff --git a/x/pubkey/client/cli/tx.go b/x/pubkey/client/cli/tx.go index 4b0bacff..a01c0469 100644 --- a/x/pubkey/client/cli/tx.go +++ b/x/pubkey/client/cli/tx.go @@ -2,7 +2,6 @@ package cli import ( "fmt" - "path/filepath" "github.com/spf13/cobra" @@ -11,18 +10,29 @@ import ( "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/server" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/sedaprotocol/seda-chain/app/utils" "github.com/sedaprotocol/seda-chain/x/pubkey/types" ) const ( // FlagKeyFile defines a flag to specify an existing key file. FlagKeyFile = "key-file" + // FlagForceKeyFile defines a flag to specify that the key file should be overwritten if it already exists. + FlagForceKeyFile = "key-file-force" + // FlagEncryptionKey defines a flag to specify an existing encryption key. + FlagEncryptionKey = "key-file-custom-encryption-key" + // FlagNoEncryption defines a flag to specify that the generated key file should not be encrypted. + FlagNoEncryption = "key-file-no-encryption" ) +func AddSedaKeysFlagsToCmd(cmd *cobra.Command) { + cmd.Flags().String(FlagKeyFile, "", "path to an existing SEDA key file") + cmd.Flags().Bool(FlagForceKeyFile, false, "overwrite the existing key file if it already exists") + cmd.Flags().Bool(FlagEncryptionKey, false, "use a custom AES encryption key for the SEDA key file (if not set, a random key will be generated)") + cmd.Flags().Bool(FlagNoEncryption, false, "do not encrypt the generated SEDA key file (if the key file is not provided)") +} + // GetTxCmd returns the CLI transaction commands for this module func GetTxCmd(valAddrCodec address.Codec) *cobra.Command { cmd := &cobra.Command{ @@ -49,7 +59,6 @@ func AddKey(ac address.Codec) *cobra.Command { if err != nil { return err } - serverCfg := server.GetServerContextFromCmd(cmd).Config valAddr := sdk.ValAddress(clientCtx.GetFromAddress()) if valAddr.Empty() { @@ -60,18 +69,9 @@ func AddKey(ac address.Codec) *cobra.Command { return err } - var pks []types.IndexedPubKey - keyFile, _ := cmd.Flags().GetString(FlagKeyFile) - if keyFile != "" { - pks, err = utils.LoadSEDAPubKeys(keyFile) - if err != nil { - return err - } - } else { - pks, err = utils.GenerateSEDAKeys(valAddr, filepath.Dir(serverCfg.PrivValidatorKeyFile())) - if err != nil { - return err - } + pks, err := LoadOrGenerateSEDAKeys(cmd, valAddr) + if err != nil { + return err } msg := &types.MsgAddKey{ @@ -82,7 +82,7 @@ func AddKey(ac address.Codec) *cobra.Command { }, } - cmd.Flags().String(FlagKeyFile, "", "path to an existing SEDA key file") + AddSedaKeysFlagsToCmd(cmd) flags.AddTxFlagsToCmd(cmd) return cmd } diff --git a/x/pubkey/client/cli/tx_test.go b/x/pubkey/client/cli/tx_test.go index a522fe52..82355c38 100644 --- a/x/pubkey/client/cli/tx_test.go +++ b/x/pubkey/client/cli/tx_test.go @@ -83,6 +83,9 @@ func (s *CLITestSuite) SetupSuite() { fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin("seda", sdkmath.NewInt(10))).String()), fmt.Sprintf("--%s=%s", flags.FlagFrom, account.String()), + // Generating a new encryption key or manually setting the encryption key + // requires user interaction, so we disable it here for testing + fmt.Sprintf("--%s", cli.FlagNoEncryption), } } diff --git a/x/staking/client/cli/tx.go b/x/staking/client/cli/tx.go index a852fb58..648a1c31 100644 --- a/x/staking/client/cli/tx.go +++ b/x/staking/client/cli/tx.go @@ -2,7 +2,6 @@ package cli import ( "fmt" - "path/filepath" "strings" "github.com/spf13/cobra" @@ -15,14 +14,12 @@ import ( "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/server" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/version" stakingcli "github.com/cosmos/cosmos-sdk/x/staking/client/cli" sdktypes "github.com/cosmos/cosmos-sdk/x/staking/types" - "github.com/sedaprotocol/seda-chain/app/utils" pubkeycli "github.com/sedaprotocol/seda-chain/x/pubkey/client/cli" pubkeytypes "github.com/sedaprotocol/seda-chain/x/pubkey/types" "github.com/sedaprotocol/seda-chain/x/staking/types" @@ -92,7 +89,6 @@ where we can get the pubkey using "%s tendermint show-validator" if err != nil { return err } - serverCfg := server.GetServerContextFromCmd(cmd).Config txf, err := tx.NewFactoryCLI(clientCtx, cmd.Flags()) if err != nil { @@ -108,17 +104,9 @@ where we can get the pubkey using "%s tendermint show-validator" var pks []pubkeytypes.IndexedPubKey withoutSEDAKeys, _ := cmd.Flags().GetBool(FlagWithoutSEDAKeys) if !withoutSEDAKeys { - keyFile, _ := cmd.Flags().GetString(pubkeycli.FlagKeyFile) - if keyFile != "" { - pks, err = utils.LoadSEDAPubKeys(keyFile) - if err != nil { - return err - } - } else { - pks, err = utils.GenerateSEDAKeys(valAddr, filepath.Dir(serverCfg.PrivValidatorKeyFile())) - if err != nil { - return err - } + pks, err = pubkeycli.LoadOrGenerateSEDAKeys(cmd, valAddr) + if err != nil { + return err } } @@ -137,7 +125,7 @@ where we can get the pubkey using "%s tendermint show-validator" } cmd.Flags().Bool(FlagWithoutSEDAKeys, false, "skip generating and uploading SEDA keys") - cmd.Flags().String(pubkeycli.FlagKeyFile, "", "path to an existing SEDA key file") + pubkeycli.AddSedaKeysFlagsToCmd(cmd) cmd.Flags().String(stakingcli.FlagIP, "", fmt.Sprintf("The node's public IP. It takes effect only when used in combination with --%s", flags.FlagGenerateOnly)) cmd.Flags().String(stakingcli.FlagNodeID, "", "The node's ID") flags.AddTxFlagsToCmd(cmd) diff --git a/x/staking/module.go b/x/staking/module.go index 8db034b4..d63b1b41 100644 --- a/x/staking/module.go +++ b/x/staking/module.go @@ -14,6 +14,7 @@ import ( cdctypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" sdkstaking "github.com/cosmos/cosmos-sdk/x/staking" sdkkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" sdktypes "github.com/cosmos/cosmos-sdk/x/staking/types" @@ -21,6 +22,7 @@ import ( "github.com/sedaprotocol/seda-chain/app/params" "github.com/sedaprotocol/seda-chain/x/staking/client/cli" "github.com/sedaprotocol/seda-chain/x/staking/keeper" + "github.com/sedaprotocol/seda-chain/x/staking/simulation" "github.com/sedaprotocol/seda-chain/x/staking/types" ) @@ -135,3 +137,11 @@ func (am AppModule) RegisterServices(cfg module.Configurator) { func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) { keeper.RegisterInvariants(ir, am.keeper) } + +// WeightedOperations returns the all the staking module operations with their respective weights. +func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { + return simulation.WeightedOperations( + simState.AppParams, simState.Cdc, simState.TxConfig, + am.accountKeeper, am.bankKeeper, am.keeper, + ) +} diff --git a/x/staking/simulation/operations.go b/x/staking/simulation/operations.go new file mode 100644 index 00000000..1300b257 --- /dev/null +++ b/x/staking/simulation/operations.go @@ -0,0 +1,184 @@ +package simulation + +import ( + "math/rand" + + "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + sdkstakingsimulation "github.com/cosmos/cosmos-sdk/x/staking/simulation" + sdktypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/sedaprotocol/seda-chain/app/utils" + "github.com/sedaprotocol/seda-chain/x/staking/keeper" + "github.com/sedaprotocol/seda-chain/x/staking/types" +) + +func WeightedOperations( + appParams simtypes.AppParams, + _ codec.JSONCodec, + txGen client.TxConfig, + ak sdktypes.AccountKeeper, + bk sdktypes.BankKeeper, + k *keeper.Keeper, +) simulation.WeightedOperations { + var ( + // We'll reuse all the settings for the original MsgCreateValidator from the staking module, + // only replacing the implementation of SimulateMsgCreateValidator with our own. + weightMsgCreateValidator int + weightMsgEditValidator int + weightMsgDelegate int + weightMsgUndelegate int + weightMsgBeginRedelegate int + weightMsgCancelUnbondingDelegation int + ) + + appParams.GetOrGenerate(sdkstakingsimulation.OpWeightMsgCreateValidator, &weightMsgCreateValidator, nil, func(_ *rand.Rand) { + weightMsgCreateValidator = sdkstakingsimulation.DefaultWeightMsgCreateValidator + }) + + appParams.GetOrGenerate(sdkstakingsimulation.OpWeightMsgEditValidator, &weightMsgEditValidator, nil, func(_ *rand.Rand) { + weightMsgEditValidator = sdkstakingsimulation.DefaultWeightMsgEditValidator + }) + + appParams.GetOrGenerate(sdkstakingsimulation.OpWeightMsgDelegate, &weightMsgDelegate, nil, func(_ *rand.Rand) { + weightMsgDelegate = sdkstakingsimulation.DefaultWeightMsgDelegate + }) + + appParams.GetOrGenerate(sdkstakingsimulation.OpWeightMsgUndelegate, &weightMsgUndelegate, nil, func(_ *rand.Rand) { + weightMsgUndelegate = sdkstakingsimulation.DefaultWeightMsgUndelegate + }) + + appParams.GetOrGenerate(sdkstakingsimulation.OpWeightMsgBeginRedelegate, &weightMsgBeginRedelegate, nil, func(_ *rand.Rand) { + weightMsgBeginRedelegate = sdkstakingsimulation.DefaultWeightMsgBeginRedelegate + }) + + appParams.GetOrGenerate(sdkstakingsimulation.OpWeightMsgCancelUnbondingDelegation, &weightMsgCancelUnbondingDelegation, nil, func(_ *rand.Rand) { + weightMsgCancelUnbondingDelegation = sdkstakingsimulation.DefaultWeightMsgCancelUnbondingDelegation + }) + + return simulation.WeightedOperations{ + simulation.NewWeightedOperation( + weightMsgCreateValidator, + SimulateMsgCreateSEDAValidator(txGen, ak, bk, k), + ), + simulation.NewWeightedOperation( + weightMsgEditValidator, + sdkstakingsimulation.SimulateMsgEditValidator(txGen, ak, bk, k.Keeper), + ), + simulation.NewWeightedOperation( + weightMsgDelegate, + sdkstakingsimulation.SimulateMsgDelegate(txGen, ak, bk, k.Keeper), + ), + simulation.NewWeightedOperation( + weightMsgUndelegate, + sdkstakingsimulation.SimulateMsgUndelegate(txGen, ak, bk, k.Keeper), + ), + simulation.NewWeightedOperation( + weightMsgBeginRedelegate, + sdkstakingsimulation.SimulateMsgBeginRedelegate(txGen, ak, bk, k.Keeper), + ), + simulation.NewWeightedOperation( + weightMsgCancelUnbondingDelegation, + sdkstakingsimulation.SimulateMsgCancelUnbondingDelegate(txGen, ak, bk, k.Keeper), + ), + } +} + +// SimulateMsgCreateSEDAValidator generates a MsgCreateSEDAValidator with random values. +// Mostly copied from the original SimulateMsgCreateValidator from the staking module, +func SimulateMsgCreateSEDAValidator( + txGen client.TxConfig, + ak sdktypes.AccountKeeper, + bk sdktypes.BankKeeper, + k *keeper.Keeper, +) simtypes.Operation { + return func( + r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, _ string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + msgType := sdk.MsgTypeURL(&types.MsgCreateSEDAValidator{}) + + simAccount, _ := simtypes.RandomAcc(r, accs) + address := sdk.ValAddress(simAccount.Address) + + // ensure the validator doesn't exist already + _, err := k.GetValidator(ctx, address) + if err == nil { + return simtypes.NoOpMsg(sdktypes.ModuleName, msgType, "validator already exists"), nil, nil + } + + denom, err := k.BondDenom(ctx) + if err != nil { + return simtypes.NoOpMsg(sdktypes.ModuleName, msgType, "bond denom not found"), nil, err + } + + balance := bk.GetBalance(ctx, simAccount.Address, denom).Amount + if !balance.IsPositive() { + return simtypes.NoOpMsg(sdktypes.ModuleName, msgType, "balance is negative"), nil, nil + } + + amount, err := simtypes.RandPositiveInt(r, balance) + if err != nil { + return simtypes.NoOpMsg(sdktypes.ModuleName, msgType, "unable to generate positive amount"), nil, err + } + + selfDelegation := sdk.NewCoin(denom, amount) + + account := ak.GetAccount(ctx, simAccount.Address) + spendable := bk.SpendableCoins(ctx, account.GetAddress()) + + var fees sdk.Coins + + coins, hasNeg := spendable.SafeSub(selfDelegation) + if !hasNeg { + fees, err = simtypes.RandomFees(r, ctx, coins) + if err != nil { + return simtypes.NoOpMsg(sdktypes.ModuleName, msgType, "unable to generate fees"), nil, err + } + } + + description := sdktypes.NewDescription( + simtypes.RandStringOfLength(r, 10), + simtypes.RandStringOfLength(r, 10), + simtypes.RandStringOfLength(r, 10), + simtypes.RandStringOfLength(r, 10), + simtypes.RandStringOfLength(r, 10), + ) + + maxCommission := math.LegacyNewDecWithPrec(int64(simtypes.RandIntBetween(r, 0, 100)), 2) + commission := sdktypes.NewCommissionRates( + simtypes.RandomDecAmount(r, maxCommission), + maxCommission, + simtypes.RandomDecAmount(r, maxCommission), + ) + + sedaPubKeys, err := utils.GenerateSEDAKeys(address, "", "", true) + if err != nil { + return simtypes.NoOpMsg(sdktypes.ModuleName, msgType, "unable to generate SEDA keys"), nil, err + } + + msg, err := types.NewMsgCreateSEDAValidator(address.String(), simAccount.ConsKey.PubKey(), sedaPubKeys, selfDelegation, description, commission, math.OneInt()) + if err != nil { + return simtypes.NoOpMsg(sdktypes.ModuleName, sdk.MsgTypeURL(msg), "unable to create CreateValidator message"), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: txGen, + Cdc: nil, + Msg: msg, + Context: ctx, + SimAccount: simAccount, + AccountKeeper: ak, + ModuleName: sdktypes.ModuleName, + } + + return simulation.GenAndDeliverTx(txCtx, fees) + } +}