Skip to content

Commit

Permalink
key assignment feature (#144)
Browse files Browse the repository at this point in the history
* key assignment feature

* lint

* Add migrate command

* Simplify key2shares. Make TestMultipleChainHorcrux use different priv keys for each chain

* fix key2shares test

* handle feedback

* Fix non-out-dir, update docs

* Fix test

* lint
  • Loading branch information
agouin committed May 3, 2023
1 parent 0d04e72 commit 955f172
Show file tree
Hide file tree
Showing 38 changed files with 1,353 additions and 656 deletions.
20 changes: 10 additions & 10 deletions cmd/horcrux/cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func configCmd() *cobra.Command {
}

cmd.AddCommand(initCmd())
cmd.AddCommand(migrateCmd())

return cmd
}
Expand Down Expand Up @@ -51,10 +52,10 @@ func initCmd() *cobra.Command {
var cfg signer.Config

cs, _ := cmdFlags.GetBool("cosigner")
keyFileFlag, _ := cmdFlags.GetString("keyfile")
var keyFile *string
if keyFileFlag != "" {
keyFile = &keyFileFlag
keyDirFlag, _ := cmdFlags.GetString("key-dir")
var keyDir *string
if keyDirFlag != "" {
keyDir = &keyDirFlag
}
debugAddr, _ := cmdFlags.GetString("debug-addr")
if cs {
Expand Down Expand Up @@ -84,7 +85,7 @@ func initCmd() *cobra.Command {
}

cfg = signer.Config{
PrivValKeyFile: keyFile,
PrivValKeyDir: keyDir,
CosignerConfig: &signer.CosignerConfig{
Threshold: threshold,
Shares: len(peers) + 1,
Expand All @@ -101,9 +102,9 @@ func initCmd() *cobra.Command {
} else {
// Single Signer Config
cfg = signer.Config{
PrivValKeyFile: keyFile,
ChainNodes: cn,
DebugAddr: debugAddr,
PrivValKeyDir: keyDir,
ChainNodes: cn,
DebugAddr: debugAddr,
}
if err = cfg.ValidateSingleSignerConfig(); err != nil {
return err
Expand Down Expand Up @@ -134,8 +135,7 @@ func initCmd() *cobra.Command {
cmd.Flags().IntP("threshold", "t", 0, "indicate number of signatures required for threshold signature")
cmd.Flags().StringP("listen", "l", "", "listen address of the signer")
cmd.Flags().StringP("debug-addr", "d", "", "listen address for Debug and Prometheus metrics in format localhost:8543")
cmd.Flags().StringP("keyfile", "k", "",
"priv val key file path (full key for single signer, or key share for cosigner)")
cmd.Flags().StringP("key-dir", "k", "", "priv val key directory")
cmd.Flags().String("timeout", "1500ms", "configure cosigner rpc server timeout value, \n"+
"accepts valid duration strings for Go's time.ParseDuration() e.g. 1s, 1000ms, 1.5m")
cmd.Flags().BoolP("overwrite", "o", false, "set to overwrite an existing config.yaml")
Expand Down
48 changes: 24 additions & 24 deletions cmd/horcrux/cmd/cosigner.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,18 @@ type AddressCmdOutput struct {

func addressCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "address [bech32]",
Use: "address chain-id [bech32]",
Short: "Get public key hex address and valcons address",
Example: `horcrux cosigner address cosmos`,
SilenceUsage: true,
Args: cobra.RangeArgs(0, 1),
Args: cobra.RangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
err := config.Config.ValidateCosignerConfig()
if err != nil {
return err
}

keyFile, err := config.KeyFileExistsCosigner()
keyFile, err := config.KeyFileExistsCosigner(args[0])
if err != nil {
return err
}
Expand All @@ -71,13 +71,13 @@ func addressCmd() *cobra.Command {
PubKey: pubKeyJSON,
}

if len(args) == 1 {
bech32ValConsAddress, err := bech32.ConvertAndEncode(args[0]+"valcons", pubKeyAddress)
if len(args) == 2 {
bech32ValConsAddress, err := bech32.ConvertAndEncode(args[1]+"valcons", pubKeyAddress)
if err != nil {
return err
}
output.ValConsAddress = bech32ValConsAddress
pubKeyBech32, err := signer.PubKey(args[0], pubKey)
pubKeyBech32, err := signer.PubKey(args[1], pubKey)
if err != nil {
return err
}
Expand Down Expand Up @@ -117,21 +117,28 @@ func startCosignerCmd() *cobra.Command {
return err
}

var (
// services to stop on shutdown
services []cometservice.Service
logger = cometlog.NewTMLogger(cometlog.NewSyncWriter(os.Stdout)).With("module", "validator")
out := cmd.OutOrStdout()

logger := cometlog.NewTMLogger(cometlog.NewSyncWriter(out)).With("module", "validator")

logger.Info(
"CometBFT Validator",
"mode", "threshold",
"priv-state-dir", config.StateDir,
)

keyFile, err := config.KeyFileExistsCosigner()
keyFile, err := config.KeyFileExistsCosignerRSA()
if err != nil {
return err
}

logger.Info("CometBFT Validator", "mode", "threshold",
"priv-key", config.Config.PrivValKeyFile, "priv-state-dir", config.StateDir)
logger.Info(
"CometBFT Validator",
"mode", "threshold",
"priv-state-dir", config.StateDir,
)

key, err := signer.LoadCosignerKey(keyFile)
key, err := signer.LoadCosignerKeyRSA(keyFile)
if err != nil {
return fmt.Errorf("error reading cosigner key (%s): %w", keyFile, err)
}
Expand Down Expand Up @@ -161,7 +168,7 @@ func startCosignerCmd() *cobra.Command {

localCosigner := signer.NewLocalCosigner(
&config,
key,
key.ID,
key.RSAKey,
peers,
cosignerConfig.P2PListen,
Expand All @@ -188,12 +195,11 @@ func startCosignerCmd() *cobra.Command {
if err := raftStore.Start(); err != nil {
return fmt.Errorf("error starting raft store: %w", err)
}
services = append(services, raftStore)
services := []cometservice.Service{raftStore}

val := signer.NewThresholdValidator(
logger,
&config,
key.PubKey,
cosignerConfig.Threshold,
localCosigner,
cosigners,
Expand All @@ -202,13 +208,7 @@ func startCosignerCmd() *cobra.Command {

raftStore.SetThresholdValidator(val)

pubkey, err := val.GetPubKey()
if err != nil {
return fmt.Errorf("failed to get public key: %w", err)
}
logger.Info("Signer", "address", pubkey.Address())

go EnableDebugAndMetrics(cmd.Context())
go EnableDebugAndMetrics(cmd.Context(), out)

services, err = signer.StartRemoteSigners(services, logger, val, config.Config.Nodes())
if err != nil {
Expand Down
192 changes: 153 additions & 39 deletions cmd/horcrux/cmd/key2shares.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,74 +16,188 @@ limitations under the License.
package cmd

import (
"errors"
"fmt"
"strconv"
"os"
"path/filepath"

"github.com/cometbft/cometbft/libs/os"
"github.com/spf13/cobra"
"github.com/strangelove-ventures/horcrux/signer"
)

func createCosignerDirectoryIfNecessary(out string, id int) (string, error) {
dir := filepath.Join(out, fmt.Sprintf("cosigner_%d", id))
dirStat, err := os.Stat(dir)
if err != nil {
if !os.IsNotExist(err) {
return "", fmt.Errorf("unexpected error fetching info for cosigner directory: %w", err)
}
if err := os.Mkdir(dir, 0700); err != nil {
return "", fmt.Errorf("failed to make directory for cosigner files: %w", err)
}
return dir, nil
}
if !dirStat.IsDir() {
return "", fmt.Errorf("path must be a directory: %s", dir)
}
return dir, nil
}

const (
flagOutputDir = "out"
flagThreshold = "threshold"
flagShares = "shares"
flagKeyFile = "key-file"
flagChainID = "chain-id"
)

func addOutputDirFlag(cmd *cobra.Command) {
cmd.Flags().StringP(flagOutputDir, "", "", "output directory")
}

func addShareFlag(cmd *cobra.Command) {
cmd.Flags().Uint8(flagShares, 0, "total key shares")
}

func addShardFlags(cmd *cobra.Command) {
addShareFlag(cmd)
cmd.Flags().Uint8(flagThreshold, 0, "threshold number of shares required to successfully sign")
cmd.Flags().String(flagKeyFile, "", "priv_validator_key.json file to shard")
cmd.Flags().String(flagChainID, "", "key shards will sign for this chain ID")
}

// CreateCosignerSharesCmd is a cobra command for creating cosigner shares from a priv validator
func createCosignerSharesCmd() *cobra.Command {
func createCosignerEd25519SharesCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "create-shares [priv_validator.json] [threshold] [shares]",
Use: "create-ed25519-shares chain-id priv-validator-key-file threshold shares",
Aliases: []string{"shard", "shares"},
Args: validateCreateCosignerShares,
Short: "Create cosigner shares",
Args: cobra.NoArgs,
Short: "Create cosigner Ed25519 shares",
RunE: func(cmd *cobra.Command, args []string) (err error) {
threshold, shares := args[1], args[2]
t, err := strconv.ParseInt(threshold, 10, 64)
if err != nil {
return fmt.Errorf("error parsing threshold (%s): %w", threshold, err)
flags := cmd.Flags()

chainID, _ := flags.GetString(flagChainID)
keyFile, _ := flags.GetString(flagKeyFile)
threshold, _ := flags.GetUint8(flagThreshold)
shares, _ := flags.GetUint8(flagShares)

var errs []error

if keyFile == "" {
errs = append(errs, fmt.Errorf("key-file flag must be provided and non-empty"))
}
n, err := strconv.ParseInt(shares, 10, 64)
if err != nil {
return fmt.Errorf("error parsing shares (%s): %w", shares, err)

if chainID == "" {
errs = append(errs, fmt.Errorf("chain-id flag must be provided and non-empty"))
}

if threshold == 0 {
errs = append(errs, fmt.Errorf("threshold flag must be provided and non-zero"))
}

if shares == 0 {
errs = append(errs, fmt.Errorf("shares flag must be provided and non-zero"))
}

if _, err := os.Stat(keyFile); err != nil {
errs = append(errs, fmt.Errorf("error accessing priv_validator_key file(%s): %w", keyFile, err))
}

if threshold > shares {
errs = append(errs, fmt.Errorf(
"threshold cannot be greater than total shares, got [threshold](%d) > [shares](%d)",
threshold, shares,
))
}

csKeys, err := signer.CreateCosignerSharesFromFile(args[0], t, n)
if threshold <= shares/2 {
errs = append(errs, fmt.Errorf("threshold must be greater than total shares "+
"divided by 2, got [threshold](%d) <= [shares](%d) / 2", threshold, shares))
}

if len(errs) > 0 {
return errors.Join(errs...)
}

csKeys, err := signer.CreateCosignerSharesFromFile(keyFile, threshold, shares)
if err != nil {
return err
}

out, _ := cmd.Flags().GetString(flagOutputDir)
if out != "" {
if err := os.MkdirAll(out, 0700); err != nil {
return err
}
}

// silence usage after all input has been validated
cmd.SilenceUsage = true

for _, c := range csKeys {
if err = signer.WriteCosignerShareFile(c, fmt.Sprintf("private_share_%d.json", c.ID)); err != nil {
dir, err := createCosignerDirectoryIfNecessary(out, c.ID)
if err != nil {
return err
}
filename := filepath.Join(dir, fmt.Sprintf("%s_share.json", chainID))
if err = signer.WriteCosignerShareFile(c, filename); err != nil {
return err
}
fmt.Printf("Created Share %d\n", c.ID)
fmt.Fprintf(cmd.OutOrStdout(), "Created Ed25519 Share %s\n", filename)
}
return nil
},
}
addShardFlags(cmd)
addOutputDirFlag(cmd)
return cmd
}

func validateCreateCosignerShares(_ *cobra.Command, args []string) error {
if len(args) != 3 {
return fmt.Errorf("wrong num args exp(3) got(%d)", len(args))
}
if !os.FileExists(args[0]) {
return fmt.Errorf("priv_validator.json file(%s) doesn't exist", args[0])
}
threshold, shares := args[1], args[2]
t, err := strconv.ParseInt(threshold, 10, 64)
if err != nil {
return fmt.Errorf("error parsing threshold (%s): %w", threshold, err)
}
n, err := strconv.ParseInt(shares, 10, 64)
if err != nil {
return fmt.Errorf("error parsing shares (%s): %w", shares, err)
}
if t > n {
return fmt.Errorf("threshold cannot be greater than total shares, got [threshold](%d) > [shares](%d)", t, n)
}
if t <= n/2 {
return fmt.Errorf("threshold must be greater than total shares "+
"divided by 2, got [threshold](%d) <= [shares](%d) / 2", t, n)
// CreateCosignerSharesCmd is a cobra command for creating cosigner shares from a priv validator
func createCosignerRSASharesCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "create-rsa-shares shares",
Aliases: []string{"shard", "shares"},
Args: cobra.NoArgs,
Short: "Create cosigner RSA shares",

RunE: func(cmd *cobra.Command, args []string) (err error) {
shares, _ := cmd.Flags().GetUint8(flagShares)

if shares <= 0 {
return fmt.Errorf("shares must be greater than zero (%d): %w", shares, err)
}

csKeys, err := signer.CreateCosignerSharesRSA(int(shares))
if err != nil {
return err
}

out, _ := cmd.Flags().GetString(flagOutputDir)
if out != "" {
if err := os.MkdirAll(out, 0700); err != nil {
return err
}
}

// silence usage after all input has been validated
cmd.SilenceUsage = true

for _, c := range csKeys {
dir, err := createCosignerDirectoryIfNecessary(out, c.ID)
if err != nil {
return err
}
filename := filepath.Join(dir, "rsa_keys.json")
if err = signer.WriteCosignerShareRSAFile(c, filename); err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "Created RSA Share %s\n", filename)
}
return nil
},
}
return nil
addShareFlag(cmd)
addOutputDirFlag(cmd)
return cmd
}
Loading

0 comments on commit 955f172

Please sign in to comment.