Skip to content

Commit

Permalink
feat(cli): add key conversion subcommand (#174)
Browse files Browse the repository at this point in the history
* feat(cli): add key conversion subcommand

* adds uncompressed pubkey (hex) as input
  • Loading branch information
leeren committed Oct 15, 2024
1 parent cab0581 commit d498f9c
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 60 deletions.
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

0 comments on commit d498f9c

Please sign in to comment.