diff --git a/client/cmd/cmd.go b/client/cmd/cmd.go index 295ac820..91e20cdb 100644 --- a/client/cmd/cmd.go +++ b/client/cmd/cmd.go @@ -26,6 +26,7 @@ func New() *cobra.Command { buildinfo.NewVersionCmd(), newValidatorCmds(), newStatusCmd(), + newKeyCmds(), newRollbackCmd(app.CreateApp), ) } diff --git a/client/cmd/flags.go b/client/cmd/flags.go index b0de2471..74ef07a0 100644 --- a/client/cmd/flags.go +++ b/client/cmd/flags.go @@ -120,6 +120,14 @@ func bindStatusFlags(flags *pflag.FlagSet, cfg *StatusConfig) { libcmd.BindHomeFlag(flags, &cfg.HomeDir) } +func bindKeyConvertFlags(cmd *cobra.Command, cfg *keyConfig) { + cmd.Flags().StringVar(&cfg.ValidatorKeyFile, "validator-key-file", "", "Path to the validator key file") + cmd.Flags().StringVar(&cfg.PrivateKeyFile, "private-key-file", "", "Path to the EVM private key env file") + cmd.Flags().StringVar(&cfg.PubKeyHex, "pubkey-hex", "", "Public key in hex format") + cmd.Flags().StringVar(&cfg.PubKeyBase64, "pubkey-base64", "", "Public key in base64 format") + cmd.Flags().StringVar(&cfg.PubKeyHexUncompressed, "pubkey-hex-uncompressed", "", "Uncompressed public key in hex format") +} + func bindRollbackFlags(cmd *cobra.Command, cfg *config.Config) { cmd.Flags().BoolVar(&cfg.RemoveBlock, "hard", false, "remove last block as well as state") } @@ -189,3 +197,26 @@ func validateValidatorUnstakeOnBehalfFlags(cfg stakeConfig) error { "unstake": cfg.StakeAmount, }) } + +func validateKeyConvertFlags(cfg keyConfig) error { + flagMap := map[string]string{ + "validator-key-file": cfg.ValidatorKeyFile, + "private-key-file": cfg.PrivateKeyFile, + "pubkey-hex": cfg.PubKeyHex, + "pubkey-base64": cfg.PubKeyBase64, + "pubkey-hex-uncompressed": cfg.PubKeyHexUncompressed, + } + + for _, value := range flagMap { + if value != "" { + return nil + } + } + + flagNames := make([]string, 0, len(flagMap)) + for flag := range flagMap { + flagNames = append(flagNames, "--"+flag) + } + + return fmt.Errorf("at least one of %s must be provided", strings.Join(flagNames, ", ")) +} diff --git a/client/cmd/key_utils.go b/client/cmd/key_utils.go index cb5b0746..8541e466 100644 --- a/client/cmd/key_utils.go +++ b/client/cmd/key_utils.go @@ -1,13 +1,20 @@ package cmd import ( + "crypto/ecdsa" "crypto/elliptic" "encoding/base64" "encoding/hex" + "encoding/json" "fmt" + "math/big" + "os" + cosmosk1 "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cosmostypes "github.com/cosmos/cosmos-sdk/types" "github.com/decred/dcrd/dcrec/secp256k1" "github.com/ethereum/go-ethereum/crypto" + "github.com/joho/godotenv" "github.com/piplabs/story/lib/errors" ) @@ -23,24 +30,88 @@ type KeyInfo struct { Value string `json:"value"` } -func uncompressPubKey(compressedPubKeyBase64 string) (string, error) { - compressedPubKeyBytes, err := base64.StdEncoding.DecodeString(compressedPubKeyBase64) +func loadValidatorFile(path string) ([]byte, error) { + keyFileBytes, err := os.ReadFile(path) if err != nil { - return "", errors.Wrap(err, "failed to decode base64 public key") + return nil, errors.Wrap(err, "failed to read validator key file") } + + var keyData ValidatorKey + if err := json.Unmarshal(keyFileBytes, &keyData); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal validator key file") + } + + privKeyBytes, err := base64.StdEncoding.DecodeString(keyData.PrivKey.Value) + if err != nil { + return nil, errors.Wrap(err, "failed to decode private key") + } + + return privKeyBytes, nil +} + +func loadPrivKeyFile(path string) ([]byte, error) { + envMap, err := godotenv.Read(path) + if err != nil { + return nil, errors.Wrap(err, "failed to read .env file") + } + + privateKey, exists := envMap["PRIVATE_KEY"] + if !exists || privateKey == "" { + return nil, errors.New("no private key found in file") + } + + privKeyBytes, err := hex.DecodeString(privateKey) + if err != nil { + return nil, errors.Wrap(err, "failed to decode private key") + } + + return privKeyBytes, nil +} + +func privKeyFileToCmpPubKey(path string) ([]byte, error) { + privKeyBytes, err := loadPrivKeyFile(path) + if err != nil { + return nil, errors.Wrap(err, "failed to load priv key file") + } + + return privKeyToCmpPubKey(privKeyBytes) +} + +func validatorKeyFileToCmpPubKey(path string) ([]byte, error) { + privKeyBytes, err := loadValidatorFile(path) + if err != nil { + return nil, errors.Wrap(err, "failed to load validator key file") + } + + return privKeyToCmpPubKey(privKeyBytes) +} + +func privKeyToCmpPubKey(privateKeyBytes []byte) ([]byte, error) { + privateKey, err := crypto.ToECDSA(privateKeyBytes) + if err != nil { + return nil, errors.Wrap(err, "invalid private key") + } + + publicKey := &privateKey.PublicKey + + compressedPubKeyBytes := crypto.CompressPubkey(publicKey) + + return compressedPubKeyBytes, nil +} + +func cmpPubKeyToUncmpPubKey(compressedPubKeyBytes []byte) ([]byte, error) { if len(compressedPubKeyBytes) != secp256k1.PubKeyBytesLenCompressed { - return "", fmt.Errorf("invalid compressed public key length: %d", len(compressedPubKeyBytes)) + return nil, fmt.Errorf("invalid compressed public key length: %d", len(compressedPubKeyBytes)) } pubKey, err := secp256k1.ParsePubKey(compressedPubKeyBytes) if err != nil { - return "", errors.Wrap(err, "failed to parse compressed public key") + return nil, errors.Wrap(err, "failed to parse compressed public key") } uncompressedPubKeyBytes := pubKey.SerializeUncompressed() - uncompressedPubKeyHex := hex.EncodeToString(uncompressedPubKeyBytes) - return uncompressedPubKeyHex, nil + return uncompressedPubKeyBytes, nil } func uncompressPrivateKey(privateKeyHex string) ([]byte, error) { @@ -57,3 +128,86 @@ func uncompressPrivateKey(privateKeyHex string) ([]byte, error) { return uncompressedPubKey, nil } + +func cmpPubKeyToEVMAddress(cmpPubKey []byte) (string, error) { + if len(cmpPubKey) != secp256k1.PubKeyBytesLenCompressed { + return "", fmt.Errorf("invalid compressed public key length: %d", len(cmpPubKey)) + } + + pubKey, err := crypto.DecompressPubkey(cmpPubKey) + if err != nil { + return "", errors.Wrap(err, "failed to decompress public key") + } + evmAddress := crypto.PubkeyToAddress(*pubKey).Hex() + + return evmAddress, nil +} + +func cmpPubKeyToDelegatorAddress(cmpPubKey []byte) (string, error) { + if len(cmpPubKey) != secp256k1.PubKeyBytesLenCompressed { + return "", fmt.Errorf("invalid compressed public key length: %d", len(cmpPubKey)) + } + + pubKey := &cosmosk1.PubKey{Key: cmpPubKey} + + return cosmostypes.AccAddress(pubKey.Address().Bytes()).String(), nil +} + +func cmpPubKeyToValidatorAddress(cmpPubKey []byte) (string, error) { + if len(cmpPubKey) != secp256k1.PubKeyBytesLenCompressed { + return "", fmt.Errorf("invalid compressed public key length: %d", len(cmpPubKey)) + } + pubKey := &cosmosk1.PubKey{Key: cmpPubKey} + + return cosmostypes.ValAddress(pubKey.Address().Bytes()).String(), nil +} + +func uncmpPubKeyToCmpPubKey(uncmpPubKey []byte) ([]byte, error) { + if len(uncmpPubKey) != 65 || uncmpPubKey[0] != 0x04 { + return nil, errors.New("invalid uncompressed public key length or format") + } + + x := new(big.Int).SetBytes(uncmpPubKey[1:33]) + y := new(big.Int).SetBytes(uncmpPubKey[33:]) + + pubKey := ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: x, + Y: y, + } + + return crypto.CompressPubkey(&pubKey), nil +} + +func printKeyFormats(compressedPubKeyBytes []byte) error { + compressedPubKeyBase64 := base64.StdEncoding.EncodeToString(compressedPubKeyBytes) + evmAddress, err := cmpPubKeyToEVMAddress(compressedPubKeyBytes) + if err != nil { + return errors.Wrap(err, "failed to convert compressed pub key to EVM address") + } + + uncompressedPubKeyBytes, err := cmpPubKeyToUncmpPubKey(compressedPubKeyBytes) + if err != nil { + return errors.Wrap(err, "failed to convert compressed pub key to uncompressed format") + } + uncompressedPubKeyHex := hex.EncodeToString(uncompressedPubKeyBytes) + + validatorAddress, err := cmpPubKeyToValidatorAddress(compressedPubKeyBytes) + if err != nil { + return errors.Wrap(err, "failed to convert compressed pub key to validator address") + } + + delegatorAddress, err := cmpPubKeyToDelegatorAddress(compressedPubKeyBytes) + if err != nil { + return errors.Wrap(err, "failed to convert compressed pub key to delegator address") + } + + fmt.Println("Compressed Public Key (hex):", hex.EncodeToString(compressedPubKeyBytes)) + fmt.Println("Compressed Public Key (base64):", compressedPubKeyBase64) + fmt.Println("Uncompressed Public Key (hex):", uncompressedPubKeyHex) + fmt.Println("EVM Address:", evmAddress) + fmt.Println("Validator Address:", validatorAddress) + fmt.Println("Delegator Address:", delegatorAddress) + + return nil +} diff --git a/client/cmd/keys.go b/client/cmd/keys.go new file mode 100644 index 00000000..ac0d1225 --- /dev/null +++ b/client/cmd/keys.go @@ -0,0 +1,94 @@ +package cmd + +import ( + "context" + "encoding/base64" + "encoding/hex" + "strings" + + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +type keyConfig struct { + ValidatorKeyFile string + PrivateKeyFile string + PubKeyHex string + PubKeyBase64 string + PubKeyHexUncompressed string +} + +func newKeyCmds() *cobra.Command { + cmd := &cobra.Command{ + Use: "key", + Short: "Commands for key management", + Args: cobra.NoArgs, + } + + cmd.AddCommand( + newKeyConvertCmd(), + ) + + return cmd +} + +func newKeyConvertCmd() *cobra.Command { + var cfg keyConfig + + cmd := &cobra.Command{ + Use: "convert", + Short: "Convert between various key formats", + Args: cobra.NoArgs, + RunE: runValidatorCommand( + func() error { return validateKeyConvertFlags(cfg) }, + func(ctx context.Context) error { return convertKey(ctx, cfg) }, + ), + } + + bindKeyConvertFlags(cmd, &cfg) + + return cmd +} + +func convertKey(_ context.Context, cfg keyConfig) error { + var compressedPubKeyBytes []byte + var err error + + switch { + case cfg.ValidatorKeyFile != "": + compressedPubKeyBytes, err = validatorKeyFileToCmpPubKey(cfg.ValidatorKeyFile) + if err != nil { + return errors.Wrap(err, "failed to load validator private key") + } + case cfg.PrivateKeyFile != "": + compressedPubKeyBytes, err = privKeyFileToCmpPubKey(cfg.PrivateKeyFile) + if err != nil { + return errors.Wrap(err, "failed to load private key file") + } + case cfg.PubKeyHex != "": + pubKeyHex := strings.TrimPrefix(cfg.PubKeyHex, "0x") + compressedPubKeyBytes, err = hex.DecodeString(pubKeyHex) + if err != nil { + return errors.Wrap(err, "failed to decode hex public key") + } + case cfg.PubKeyBase64 != "": + compressedPubKeyBytes, err = base64.StdEncoding.DecodeString(cfg.PubKeyBase64) + if err != nil { + return errors.Wrap(err, "failed to decode base64 public key") + } + case cfg.PubKeyHexUncompressed != "": + pubKeyHex := strings.TrimPrefix(cfg.PubKeyHexUncompressed, "0x") + uncompressedPubKeyBytes, err := hex.DecodeString(pubKeyHex) + if err != nil { + return errors.Wrap(err, "failed to decode hex public key") + } + compressedPubKeyBytes, err = uncmpPubKeyToCmpPubKey(uncompressedPubKeyBytes) + if err != nil { + return errors.Wrap(err, "failed to convert uncompressed pub key") + } + default: + return errors.New("no valid key input provided") + } + + return printKeyFormats(compressedPubKeyBytes) +} diff --git a/client/cmd/validator.go b/client/cmd/validator.go index 10e47c49..263ed153 100644 --- a/client/cmd/validator.go +++ b/client/cmd/validator.go @@ -2,10 +2,8 @@ package cmd import ( "context" - "crypto/ecdsa" "encoding/base64" "encoding/hex" - "encoding/json" "fmt" "math/big" "os" @@ -302,6 +300,9 @@ func newValidatorKeyExportCmd() *cobra.Command { cmd := &cobra.Command{ Use: "export", Short: "Export the EVM private key from the Tendermint key file", + PreRunE: func(_ *cobra.Command, _ []string) error { + return nil + }, RunE: runValidatorCommand( func() error { return nil }, func(ctx context.Context) error { return exportKey(ctx, cfg) }, @@ -349,51 +350,25 @@ func runValidatorCommand( } func exportKey(_ context.Context, cfg exportKeyConfig) error { - keyFileBytes, err := os.ReadFile(cfg.ValidatorKeyFile) - if err != nil { - return errors.Wrap(err, "failed to read key file") - } - - var keyData ValidatorKey - if err := json.Unmarshal(keyFileBytes, &keyData); err != nil { - return errors.Wrap(err, "failed to unmarshal key file") - } - - privKeyBytes, err := base64.StdEncoding.DecodeString(keyData.PrivKey.Value) - if err != nil { - return errors.Wrap(err, "failed to decode private key") - } - - privateKey, err := crypto.ToECDSA(privKeyBytes) + privKeyBytes, err := loadValidatorFile(cfg.ValidatorKeyFile) if err != nil { - return errors.Wrap(err, "invalid private key") - } - - publicKey, ok := privateKey.Public().(*ecdsa.PublicKey) - if !ok { - return errors.New("failed to cast public key to ecdsa.PublicKey") + return errors.Wrap(err, "failed to load validator key file") } - evmPublicKey := crypto.PubkeyToAddress(*publicKey).Hex() - compressedPubKeyBytes, err := base64.StdEncoding.DecodeString(keyData.PubKey.Value) + compressedPubKeyBytes, err := privKeyToCmpPubKey(privKeyBytes) if err != nil { - return errors.Wrap(err, "failed to decode base64 pub key") + return errors.Wrap(err, "failed to decode compressed pub key") } - compressedPubKeyHex := hex.EncodeToString(compressedPubKeyBytes) - uncompressedPubKeyHex, err := uncompressPubKey(keyData.PubKey.Value) - if err != nil { + if err := printKeyFormats(compressedPubKeyBytes); err != nil { return err } - fmt.Println("------------------------------------------------------") - fmt.Println("EVM Public Key:", evmPublicKey) - fmt.Println("Compressed Public Key (base64):", keyData.PubKey.Value) - fmt.Println("Compressed Public Key (hex):", compressedPubKeyHex) - fmt.Println("Uncompressed Public Key:", uncompressedPubKeyHex) - fmt.Println("------------------------------------------------------") - if cfg.ExportEVMKey { + privateKey, err := crypto.ToECDSA(privKeyBytes) + if err != nil { + return errors.Wrap(err, "invalid private key") + } evmPrivateKey := hex.EncodeToString(crypto.FromECDSA(privateKey)) keyContent := "PRIVATE_KEY=" + evmPrivateKey if err := os.WriteFile(cfg.EvmKeyFile, []byte(keyContent), 0600); err != nil { @@ -408,24 +383,15 @@ func exportKey(_ context.Context, cfg exportKeyConfig) error { } func createValidator(ctx context.Context, cfg createValidatorConfig) error { - keyFileBytes, err := os.ReadFile(cfg.ValidatorKeyFile) + compressedPubKeyBytes, err := validatorKeyFileToCmpPubKey(cfg.ValidatorKeyFile) if err != nil { - return errors.Wrap(err, "invalid key file") - } - - var keyFileData ValidatorKey - if err := json.Unmarshal(keyFileBytes, &keyFileData); err != nil { - return errors.Wrap(err, "failed to unmarshal priv_validator_key.json") + return errors.Wrap(err, "failed to extract compressed pub key") } - uncompressedPubKeyHex, err := uncompressPubKey(keyFileData.PubKey.Value) + uncompressedPubKeyBytes, err := cmpPubKeyToUncmpPubKey(compressedPubKeyBytes) if err != nil { return err } - uncompressedPubKeyBytes, err := hex.DecodeString(uncompressedPubKeyHex) - if err != nil { - return errors.Wrap(err, "failed to decode uncompressed public key hex") - } stakeAmount, ok := new(big.Int).SetString(cfg.StakeAmount, 10) if !ok { @@ -523,13 +489,14 @@ func stake(ctx context.Context, cfg stakeConfig) error { } func stakeOnBehalf(ctx context.Context, cfg stakeConfig) error { - uncompressedDelegatorPubKeyHex, err := uncompressPubKey(cfg.DelegatorPubKey) + delegatorPubKeyBytes, err := base64.StdEncoding.DecodeString(cfg.DelegatorPubKey) if err != nil { - return err + return errors.Wrap(err, "failed to decode base64 delegator public key") } - uncompressedDelegatorPubKeyBytes, err := hex.DecodeString(uncompressedDelegatorPubKeyHex) + + uncompressedDelegatorPubKeyBytes, err := cmpPubKeyToUncmpPubKey(delegatorPubKeyBytes) if err != nil { - return errors.Wrap(err, "failed to decode uncompressed delegator public key") + return errors.Wrap(err, "failed to uncompress delegator public key") } validatorPubKeyBytes, err := base64.StdEncoding.DecodeString(cfg.ValidatorPubKey)