Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): add key conversion subcommand #174

Merged
merged 5 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all 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 client/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func New() *cobra.Command {
buildinfo.NewVersionCmd(),
newValidatorCmds(),
newStatusCmd(),
newKeyCmds(),
newRollbackCmd(app.CreateApp),
)
}
Expand Down
31 changes: 31 additions & 0 deletions client/cmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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, ", "))
}
168 changes: 161 additions & 7 deletions client/cmd/key_utils.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand All @@ -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) {
Expand All @@ -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
}
94 changes: 94 additions & 0 deletions client/cmd/keys.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading
Loading