From 219ce9f38af1053d82848d3001af2f8f37dda406 Mon Sep 17 00:00:00 2001 From: Andrew Gouin Date: Wed, 3 May 2023 09:27:54 -0600 Subject: [PATCH] key assignment feature (#144) * 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 --- cmd/horcrux/cmd/config.go | 20 +- cmd/horcrux/cmd/cosigner.go | 48 +-- cmd/horcrux/cmd/key2shares.go | 192 ++++++++-- cmd/horcrux/cmd/key2shares_test.go | 98 ++++- cmd/horcrux/cmd/metrics.go | 12 +- cmd/horcrux/cmd/migrate.go | 217 +++++++++++ cmd/horcrux/cmd/migrate_test.go | 118 ++++++ cmd/horcrux/cmd/root.go | 3 +- cmd/horcrux/cmd/signer.go | 39 +- cmd/horcrux/cmd/testdata/config-migrated.yaml | 14 + cmd/horcrux/cmd/testdata/config-v2.yaml | 15 + .../cosigner-key-migrated-ed25519.json | 1 + .../testdata/cosigner-key-migrated-rsa.json | 1 + cmd/horcrux/cmd/testdata/cosigner-key-v2.json | 11 + cmd/horcrux/cmd/testdata/testdata.go | 20 + docs/migrating.md | 44 ++- signer/config.go | 47 ++- signer/config_test.go | 18 +- signer/cosigner.go | 6 + signer/cosigner_key.go | 112 ++++-- signer/cosigner_key_shares.go | 41 +- signer/cosigner_key_test.go | 12 +- signer/local_cosigner.go | 216 ++++++++--- signer/local_cosigner_test.go | 40 +- signer/raft_store_test.go | 3 +- signer/remote_cosigner.go | 14 + signer/remote_signer.go | 8 +- signer/single_signer_validator.go | 42 +- signer/single_signer_validator_test.go | 9 +- .../rsa_keys.json} | 2 - signer/testdata/testdata.go | 8 + signer/threshold_signer_soft.go | 8 +- signer/threshold_validator.go | 85 +++-- signer/threshold_validator_test.go | 361 ++++-------------- test/horcrux_test.go | 2 +- test/setup.go | 2 +- test/signer.go | 13 +- test/validator.go | 107 ++++-- 38 files changed, 1353 insertions(+), 656 deletions(-) create mode 100644 cmd/horcrux/cmd/migrate.go create mode 100644 cmd/horcrux/cmd/migrate_test.go create mode 100644 cmd/horcrux/cmd/testdata/config-migrated.yaml create mode 100644 cmd/horcrux/cmd/testdata/config-v2.yaml create mode 100644 cmd/horcrux/cmd/testdata/cosigner-key-migrated-ed25519.json create mode 100644 cmd/horcrux/cmd/testdata/cosigner-key-migrated-rsa.json create mode 100644 cmd/horcrux/cmd/testdata/cosigner-key-v2.json create mode 100644 cmd/horcrux/cmd/testdata/testdata.go rename signer/{fixtures/cosigner-key.json => testdata/rsa_keys.json} (97%) create mode 100644 signer/testdata/testdata.go diff --git a/cmd/horcrux/cmd/config.go b/cmd/horcrux/cmd/config.go index 094b5529..b997dc79 100644 --- a/cmd/horcrux/cmd/config.go +++ b/cmd/horcrux/cmd/config.go @@ -18,6 +18,7 @@ func configCmd() *cobra.Command { } cmd.AddCommand(initCmd()) + cmd.AddCommand(migrateCmd()) return cmd } @@ -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 { @@ -84,7 +85,7 @@ func initCmd() *cobra.Command { } cfg = signer.Config{ - PrivValKeyFile: keyFile, + PrivValKeyDir: keyDir, CosignerConfig: &signer.CosignerConfig{ Threshold: threshold, Shares: len(peers) + 1, @@ -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 @@ -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") diff --git a/cmd/horcrux/cmd/cosigner.go b/cmd/horcrux/cmd/cosigner.go index 17b417dc..6a1f6359 100644 --- a/cmd/horcrux/cmd/cosigner.go +++ b/cmd/horcrux/cmd/cosigner.go @@ -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 } @@ -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 } @@ -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) } @@ -161,7 +168,7 @@ func startCosignerCmd() *cobra.Command { localCosigner := signer.NewLocalCosigner( &config, - key, + key.ID, key.RSAKey, peers, cosignerConfig.P2PListen, @@ -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, @@ -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 { diff --git a/cmd/horcrux/cmd/key2shares.go b/cmd/horcrux/cmd/key2shares.go index d8ffa229..8f052dbf 100644 --- a/cmd/horcrux/cmd/key2shares.go +++ b/cmd/horcrux/cmd/key2shares.go @@ -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 } diff --git a/cmd/horcrux/cmd/key2shares_test.go b/cmd/horcrux/cmd/key2shares_test.go index ee498bb1..5b244453 100644 --- a/cmd/horcrux/cmd/key2shares_test.go +++ b/cmd/horcrux/cmd/key2shares_test.go @@ -10,6 +10,8 @@ import ( "github.com/stretchr/testify/require" ) +const testChainID = "test" + func TestKey2Shares(t *testing.T) { tmp := t.TempDir() @@ -24,42 +26,110 @@ func TestKey2Shares(t *testing.T) { expectErr bool }{ { - name: "valid threshold and shares", - args: []string{privValidatorKeyFile, "2", "3"}, + name: "valid threshold and shares", + args: []string{ + "--chain-id", testChainID, + "--key-file", privValidatorKeyFile, + "--threshold", "2", + "--shares", "3", + }, expectErr: false, }, { - name: "valid threshold and shares 2", - args: []string{privValidatorKeyFile, "3", "5"}, + name: "valid threshold and shares 2", + args: []string{ + "--chain-id", testChainID, + "--key-file", privValidatorKeyFile, + "--threshold", "3", + "--shares", "5", + }, expectErr: false, }, { - name: "threshold exactly half of shares", - args: []string{privValidatorKeyFile, "2", "4"}, + name: "threshold exactly half of shares", + args: []string{ + "--chain-id", testChainID, + "--key-file", privValidatorKeyFile, + "--threshold", "2", + "--shares", "4", + }, + expectErr: true, + }, + { + name: "threshold less than half of shares", + args: []string{ + "--chain-id", testChainID, + "--key-file", privValidatorKeyFile, + "--threshold", "1", + "--shares", "3", + }, expectErr: true, }, { - name: "threshold less than half of shares", - args: []string{privValidatorKeyFile, "1", "3"}, + name: "threshold exceeds shares", + args: []string{ + "--chain-id", testChainID, + "--key-file", privValidatorKeyFile, + "--threshold", "4", + "--shares", "3", + }, expectErr: true, }, { - name: "threshold exceeds shares", - args: []string{privValidatorKeyFile, "4", "3"}, + name: "non-numeric threshold and shares", + args: []string{ + "--chain-id", testChainID, + "--key-file", privValidatorKeyFile, + "--threshold", "two", + "--shares", "three", + }, expectErr: true, }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + + cmd := rootCmd() + cmd.SetOutput(io.Discard) + args := append([]string{"create-ed25519-shares", "--home", tmp, "--out", tmp}, tc.args...) + cmd.SetArgs(args) + err := cmd.Execute() + if tc.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestRSAShares(t *testing.T) { + tmp := t.TempDir() + + tcs := []struct { + name string + args []string + expectErr bool + }{ + { + name: "valid shares", + args: []string{"--shares", "3"}, + expectErr: false, + }, { - name: "non-numeric threshold and shares", - args: []string{privValidatorKeyFile, "two", "three"}, + name: "invalid shares", + args: []string{"--shares", "0"}, expectErr: true, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - cmd := createCosignerSharesCmd() + cmd := rootCmd() cmd.SetOutput(io.Discard) - cmd.SetArgs(tc.args) + args := append([]string{"create-rsa-shares", "--home", tmp, "--out", tmp}, tc.args...) + cmd.SetArgs(args) err := cmd.Execute() if tc.expectErr { require.Error(t, err) diff --git a/cmd/horcrux/cmd/metrics.go b/cmd/horcrux/cmd/metrics.go index e94ff21f..078d3be5 100644 --- a/cmd/horcrux/cmd/metrics.go +++ b/cmd/horcrux/cmd/metrics.go @@ -4,9 +4,9 @@ import ( "context" "errors" "fmt" + "io" "net/http" "net/http/pprof" - "os" "time" "github.com/armon/go-metrics" @@ -15,8 +15,8 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" ) -func AddPrometheusMetrics(mux *http.ServeMux) { - logger := cometlog.NewTMLogger(cometlog.NewSyncWriter(os.Stdout)).With("module", "metrics") +func AddPrometheusMetrics(mux *http.ServeMux, out io.Writer) { + logger := cometlog.NewTMLogger(cometlog.NewSyncWriter(out)).With("module", "metrics") // Add metrics from raft's implementation of go-metrics cfg := gmprometheus.DefaultPrometheusOpts @@ -36,8 +36,8 @@ func AddPrometheusMetrics(mux *http.ServeMux) { } // EnableDebugAndMetrics - Initialization errors are not fatal, only logged -func EnableDebugAndMetrics(ctx context.Context) { - logger := cometlog.NewTMLogger(cometlog.NewSyncWriter(os.Stdout)).With("module", "debugserver") +func EnableDebugAndMetrics(ctx context.Context, out io.Writer) { + logger := cometlog.NewTMLogger(cometlog.NewSyncWriter(out)).With("module", "debugserver") // Configure Shared Debug HTTP Server for pprof and prometheus if len(config.Config.DebugAddr) == 0 { @@ -59,7 +59,7 @@ func EnableDebugAndMetrics(ctx context.Context) { mux.Handle("/", http.RedirectHandler("/debug/pprof", http.StatusSeeOther)) // Add prometheus metrics - AddPrometheusMetrics(mux) + AddPrometheusMetrics(mux, out) // Configure Debug Server Network Parameters srv := &http.Server{ diff --git a/cmd/horcrux/cmd/migrate.go b/cmd/horcrux/cmd/migrate.go new file mode 100644 index 00000000..3794117d --- /dev/null +++ b/cmd/horcrux/cmd/migrate.go @@ -0,0 +1,217 @@ +package cmd + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + + cometcrypto "github.com/cometbft/cometbft/crypto" + cometcryptoed25519 "github.com/cometbft/cometbft/crypto/ed25519" + cometcryptoencoding "github.com/cometbft/cometbft/crypto/encoding" + cometprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto" + "github.com/spf13/cobra" + "github.com/strangelove-ventures/horcrux/signer" + amino "github.com/tendermint/go-amino" + "gopkg.in/yaml.v2" +) + +type ( + v2Config struct { + ChainID string `json:"chain-id" yaml:"chain-id"` + PrivValKeyFile *string `json:"key-file,omitempty" yaml:"key-file,omitempty"` + } + + v2CosignerKey struct { + PubKey cometcrypto.PubKey `json:"pub_key"` + ShareKey []byte `json:"secret_share"` + RSAKey rsa.PrivateKey `json:"rsa_key"` + ID int `json:"id"` + CosignerKeys []*rsa.PublicKey `json:"rsa_pubs"` + } +) + +func (cosignerKey *v2CosignerKey) UnmarshalJSON(data []byte) error { + type Alias v2CosignerKey + + aux := &struct { + RSAKey []byte `json:"rsa_key"` + PubkeyBytes []byte `json:"pub_key"` + CosignerKeys [][]byte `json:"rsa_pubs"` + *Alias + }{ + Alias: (*Alias)(cosignerKey), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + privateKey, err := x509.ParsePKCS1PrivateKey(aux.RSAKey) + if err != nil { + return err + } + + var pubkey cometcrypto.PubKey + var protoPubkey cometprotocrypto.PublicKey + err = protoPubkey.Unmarshal(aux.PubkeyBytes) + + // Prior to the tendermint protobuf migration, the public key bytes in key files + // were encoded using the go-amino libraries via + // cdc.MarshalBinaryBare(cosignerKey.PubKey) + // + // To support reading the public key bytes from these key files, we fallback to + // amino unmarshalling if the protobuf unmarshalling fails + if err != nil { + var pub cometcryptoed25519.PubKey + codec := amino.NewCodec() + codec.RegisterInterface((*cometcrypto.PubKey)(nil), nil) + codec.RegisterConcrete(cometcryptoed25519.PubKey{}, "tendermint/PubKeyEd25519", nil) + errInner := codec.UnmarshalBinaryBare(aux.PubkeyBytes, &pub) + if errInner != nil { + return err + } + pubkey = pub + } else { + pubkey, err = cometcryptoencoding.PubKeyFromProto(protoPubkey) + if err != nil { + return err + } + } + + // unmarshal the public key bytes for each cosigner + cosignerKey.CosignerKeys = make([]*rsa.PublicKey, 0) + for _, bytes := range aux.CosignerKeys { + cosignerRsaPubkey, err := x509.ParsePKCS1PublicKey(bytes) + if err != nil { + return err + } + cosignerKey.CosignerKeys = append(cosignerKey.CosignerKeys, cosignerRsaPubkey) + } + + cosignerKey.RSAKey = *privateKey + cosignerKey.PubKey = pubkey + return nil +} + +func (cosignerKey *v2CosignerKey) validate() error { + var errs []error + if cosignerKey.PubKey == nil || len(cosignerKey.PubKey.Bytes()) == 0 { + errs = append(errs, fmt.Errorf("pub_key cannot be empty")) + } + if len(cosignerKey.ShareKey) == 0 { + errs = append(errs, fmt.Errorf("secret_share cannot be empty")) + } + if err := cosignerKey.RSAKey.Validate(); err != nil { + errs = append(errs, fmt.Errorf("rsa_key is invalid: %w", err)) + } + if cosignerKey.ID == 0 { + errs = append(errs, fmt.Errorf("id cannot be zero")) + } + if len(cosignerKey.CosignerKeys) == 0 { + errs = append(errs, fmt.Errorf("cosigner keys cannot be empty")) + } + + return errors.Join(errs...) +} + +func migrateCmd() *cobra.Command { + return &cobra.Command{ + Use: "migrate", + Short: "Migrate config and key files from v2 to v3", + SilenceUsage: true, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + + configFile, err := os.ReadFile(config.ConfigFile) + if err != nil { + return err + } + + var legacyConfig v2Config + + if err := yaml.Unmarshal(configFile, &legacyConfig); err != nil { + return fmt.Errorf("failed to read config file as legacy: %w", err) + } + + if legacyConfig.ChainID == "" { + return fmt.Errorf("unable to migrate v2 config without chain-id") + } + + chainID := legacyConfig.ChainID + + var legacyCosignerKeyFile string + + if legacyConfig.PrivValKeyFile != nil && *legacyConfig.PrivValKeyFile != "" { + legacyCosignerKeyFile = *legacyConfig.PrivValKeyFile + dir := filepath.Dir(legacyCosignerKeyFile) + config.Config.PrivValKeyDir = &dir + } else { + legacyCosignerKeyFile = filepath.Join(config.HomeDir, "share.json") + } + + if _, err := os.Stat(legacyCosignerKeyFile); err != nil { + return fmt.Errorf("error loading v2 key file: %w", err) + } + + keyFile, err := os.ReadFile(legacyCosignerKeyFile) + if err != nil { + return err + } + + legacyCosignerKey := new(v2CosignerKey) + + if err := legacyCosignerKey.UnmarshalJSON(keyFile); err != nil { + return fmt.Errorf("failed to read key file as legacy: %w", err) + } + + if err := legacyCosignerKey.validate(); err != nil { + return err + } + + newEd25519Key := signer.CosignerKey{ + PubKey: legacyCosignerKey.PubKey, + ShareKey: legacyCosignerKey.ShareKey, + ID: legacyCosignerKey.ID, + } + + newEd25519KeyBz, err := newEd25519Key.MarshalJSON() + if err != nil { + return fmt.Errorf("failed to marshal new Ed25519 key to json: %w", err) + } + + newEd25519Path := config.KeyFilePathCosigner(chainID) + if err := os.WriteFile(newEd25519Path, newEd25519KeyBz, 0600); err != nil { + return fmt.Errorf("failed to write new Ed25519 key to %s: %w", newEd25519Path, err) + } + + newRSAKey := signer.CosignerKeyRSA{ + RSAKey: legacyCosignerKey.RSAKey, + ID: legacyCosignerKey.ID, + CosignerKeys: legacyCosignerKey.CosignerKeys, + } + + newRSAKeyBz, err := newRSAKey.MarshalJSON() + if err != nil { + return fmt.Errorf("failed to marshal new RSA key to json: %w", err) + } + + newRSAPath := config.KeyFilePathCosignerRSA() + if err := os.WriteFile(newRSAPath, newRSAKeyBz, 0600); err != nil { + return fmt.Errorf("failed to write new RSA key to %s: %w", newRSAPath, err) + } + + if err := config.WriteConfigFile(); err != nil { + return err + } + + if err := os.Remove(legacyCosignerKeyFile); err != nil { + return fmt.Errorf("failed to remove legacy key file (%s): %w", legacyCosignerKeyFile, err) + } + + return nil + }, + } +} diff --git a/cmd/horcrux/cmd/migrate_test.go b/cmd/horcrux/cmd/migrate_test.go new file mode 100644 index 00000000..d571eda8 --- /dev/null +++ b/cmd/horcrux/cmd/migrate_test.go @@ -0,0 +1,118 @@ +package cmd + +import ( + "fmt" + "io" + "os" + "path/filepath" + "testing" + + "github.com/strangelove-ventures/horcrux/cmd/horcrux/cmd/testdata" + "github.com/stretchr/testify/require" +) + +func TestMigrateV2toV3(t *testing.T) { + tmp := t.TempDir() + + configFile := filepath.Join(tmp, "config.yaml") + + err := os.WriteFile(configFile, testdata.ConfigV2, 0600) + require.NoError(t, err) + + keyShareFile := filepath.Join(tmp, "share.json") + + err = os.WriteFile(keyShareFile, testdata.CosignerKeyV2, 0600) + require.NoError(t, err) + + cmd := rootCmd() + cmd.SetOutput(io.Discard) + args := []string{"--home", tmp, "config", "migrate"} + cmd.SetArgs(args) + err = cmd.Execute() + require.NoError(t, err) + + require.NoFileExists(t, keyShareFile) + + newKeyShareFile := filepath.Join(tmp, "test_share.json") + require.FileExists(t, newKeyShareFile) + + newRSAKeyFile := filepath.Join(tmp, "rsa_keys.json") + require.FileExists(t, newRSAKeyFile) + + newKeyShareFileBz, err := os.ReadFile(newKeyShareFile) + require.NoError(t, err) + + require.Equal(t, testdata.CosignerKeyMigratedEd25519, string(newKeyShareFileBz)) + + newRSAKeyFileBz, err := os.ReadFile(newRSAKeyFile) + require.NoError(t, err) + + require.Equal(t, testdata.CosignerKeyMigratedRSA, string(newRSAKeyFileBz)) + + newConfigFileBz, err := os.ReadFile(configFile) + require.NoError(t, err) + + require.Equal(t, testdata.ConfigMigrated, string(newConfigFileBz)) +} + +func appendToFile(file, append string) error { + f, err := os.OpenFile(file, + os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + _, err = f.WriteString(append) + return err +} + +func TestMigrateV2toV3DifferentKeyFilePath(t *testing.T) { + tmp := t.TempDir() + + keyDir := filepath.Join(tmp, "keys") + err := os.Mkdir(keyDir, 0700) + require.NoError(t, err) + + configFile := filepath.Join(tmp, "config.yaml") + + err = os.WriteFile(configFile, testdata.ConfigV2, 0600) + require.NoError(t, err) + + keyShareFile := filepath.Join(keyDir, "share.json") + + err = appendToFile(configFile, fmt.Sprintf("key-file: %s", keyShareFile)) + require.NoError(t, err) + + err = os.WriteFile(keyShareFile, testdata.CosignerKeyV2, 0600) + require.NoError(t, err) + + cmd := rootCmd() + cmd.SetOutput(io.Discard) + args := []string{"--home", tmp, "config", "migrate"} + cmd.SetArgs(args) + err = cmd.Execute() + require.NoError(t, err) + + require.NoFileExists(t, keyShareFile) + + newKeyShareFile := filepath.Join(keyDir, "test_share.json") + require.FileExists(t, newKeyShareFile) + + newRSAKeyFile := filepath.Join(keyDir, "rsa_keys.json") + require.FileExists(t, newRSAKeyFile) + + newKeyShareFileBz, err := os.ReadFile(newKeyShareFile) + require.NoError(t, err) + + require.Equal(t, testdata.CosignerKeyMigratedEd25519, string(newKeyShareFileBz)) + + newRSAKeyFileBz, err := os.ReadFile(newRSAKeyFile) + require.NoError(t, err) + + require.Equal(t, testdata.CosignerKeyMigratedRSA, string(newRSAKeyFileBz)) + + newConfigFileBz, err := os.ReadFile(configFile) + require.NoError(t, err) + + require.Equal(t, fmt.Sprintf("key-dir: %s\n", keyDir)+testdata.ConfigMigrated, string(newConfigFileBz)) +} diff --git a/cmd/horcrux/cmd/root.go b/cmd/horcrux/cmd/root.go index 2aba7e53..e4060f3c 100644 --- a/cmd/horcrux/cmd/root.go +++ b/cmd/horcrux/cmd/root.go @@ -22,7 +22,8 @@ func rootCmd() *cobra.Command { cmd.AddCommand(configCmd()) cmd.AddCommand(cosignerCmd()) - cmd.AddCommand(createCosignerSharesCmd()) + cmd.AddCommand(createCosignerEd25519SharesCmd()) + cmd.AddCommand(createCosignerRSASharesCmd()) cmd.AddCommand(leaderElectionCmd()) cmd.AddCommand(getLeaderCmd()) cmd.AddCommand(signerCmd()) diff --git a/cmd/horcrux/cmd/signer.go b/cmd/horcrux/cmd/signer.go index 346df663..a5366111 100644 --- a/cmd/horcrux/cmd/signer.go +++ b/cmd/horcrux/cmd/signer.go @@ -2,10 +2,8 @@ package cmd import ( "fmt" - "os" cometlog "github.com/cometbft/cometbft/libs/log" - cometservice "github.com/cometbft/cometbft/libs/service" "github.com/spf13/cobra" "github.com/strangelove-ventures/horcrux/signer" ) @@ -40,7 +38,9 @@ func startSignerCmd() *cobra.Command { Args: cobra.NoArgs, SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) (err error) { - fmt.Fprintln(cmd.OutOrStdout(), singleSignerWarning) + out := cmd.OutOrStdout() + + fmt.Fprintln(out, singleSignerWarning) acceptRisk, _ := cmd.Flags().GetBool(flagAcceptRisk) if !acceptRisk { @@ -55,34 +55,19 @@ func startSignerCmd() *cobra.Command { return err } - var ( - // services to stop on shutdown - services []cometservice.Service - logger = cometlog.NewTMLogger(cometlog.NewSyncWriter(os.Stdout)).With("module", "validator") - ) - - _, err = config.KeyFileExistsSingleSigner() - if err != nil { - return err - } - - logger.Info("CometBFT Validator", "mode", "single-signer", - "priv-key", config.Config.PrivValKeyFile, "priv-state-dir", config.StateDir) + logger := cometlog.NewTMLogger(cometlog.NewSyncWriter(out)).With("module", "validator") - pv, err := signer.NewSingleSignerValidator(&config) - if err != nil { - return fmt.Errorf("failed to construct single signer validator: %w", err) - } + logger.Info( + "CometBFT Validator", + "mode", "single-signer", + "priv-state-dir", config.StateDir, + ) - pubkey, err := pv.GetPubKey() - if err != nil { - return fmt.Errorf("failed to get public key: %w", err) - } - logger.Info("Signer", "pubkey", pubkey) + pv := signer.NewSingleSignerValidator(&config) - go EnableDebugAndMetrics(cmd.Context()) + go EnableDebugAndMetrics(cmd.Context(), out) - services, err = signer.StartRemoteSigners(services, logger, pv, config.Config.Nodes()) + services, err := signer.StartRemoteSigners(nil, logger, pv, config.Config.Nodes()) if err != nil { return fmt.Errorf("failed to start remote signer(s): %w", err) } diff --git a/cmd/horcrux/cmd/testdata/config-migrated.yaml b/cmd/horcrux/cmd/testdata/config-migrated.yaml new file mode 100644 index 00000000..74a148a7 --- /dev/null +++ b/cmd/horcrux/cmd/testdata/config-migrated.yaml @@ -0,0 +1,14 @@ +cosigner: + threshold: 2 + shares: 3 + p2p-listen: tcp://127.0.0.1:2222 + peers: + - share-id: 2 + p2p-addr: tcp://127.0.0.1:2223 + - share-id: 3 + p2p-addr: tcp://127.0.0.1:2224 + rpc-timeout: 1000ms +chain-nodes: +- priv-val-addr: tcp://127.0.0.1:1234 +- priv-val-addr: tcp://127.0.0.1:2345 +- priv-val-addr: tcp://127.0.0.1:3456 diff --git a/cmd/horcrux/cmd/testdata/config-v2.yaml b/cmd/horcrux/cmd/testdata/config-v2.yaml new file mode 100644 index 00000000..09fbfae8 --- /dev/null +++ b/cmd/horcrux/cmd/testdata/config-v2.yaml @@ -0,0 +1,15 @@ +chain-id: test +cosigner: + threshold: 2 + shares: 3 + p2p-listen: tcp://127.0.0.1:2222 + peers: + - share-id: 2 + p2p-addr: tcp://127.0.0.1:2223 + - share-id: 3 + p2p-addr: tcp://127.0.0.1:2224 + rpc-timeout: 1000ms +chain-nodes: +- priv-val-addr: tcp://127.0.0.1:1234 +- priv-val-addr: tcp://127.0.0.1:2345 +- priv-val-addr: tcp://127.0.0.1:3456 diff --git a/cmd/horcrux/cmd/testdata/cosigner-key-migrated-ed25519.json b/cmd/horcrux/cmd/testdata/cosigner-key-migrated-ed25519.json new file mode 100644 index 00000000..ec612235 --- /dev/null +++ b/cmd/horcrux/cmd/testdata/cosigner-key-migrated-ed25519.json @@ -0,0 +1 @@ +{"pub_key":"CiBRLhKCqDU6Wufhj0TZK6jNO8LreArc+CKHKYitdTyYUg==","secret_share":"q0zuUQpplAfBJxoA2e2O1H7BdBgopzhPmL8mIWP8AQw=","id":3} \ No newline at end of file diff --git a/cmd/horcrux/cmd/testdata/cosigner-key-migrated-rsa.json b/cmd/horcrux/cmd/testdata/cosigner-key-migrated-rsa.json new file mode 100644 index 00000000..4b5c6732 --- /dev/null +++ b/cmd/horcrux/cmd/testdata/cosigner-key-migrated-rsa.json @@ -0,0 +1 @@ +{"rsa_key":"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","rsa_pubs":["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"],"id":3} \ No newline at end of file diff --git a/cmd/horcrux/cmd/testdata/cosigner-key-v2.json b/cmd/horcrux/cmd/testdata/cosigner-key-v2.json new file mode 100644 index 00000000..e8a06b1d --- /dev/null +++ b/cmd/horcrux/cmd/testdata/cosigner-key-v2.json @@ -0,0 +1,11 @@ +{ + "rsa_key": "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", + "pub_key": "CiBRLhKCqDU6Wufhj0TZK6jNO8LreArc+CKHKYitdTyYUg==", + "rsa_pubs": [ + "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" + ], + "secret_share": "q0zuUQpplAfBJxoA2e2O1H7BdBgopzhPmL8mIWP8AQw=", + "id": 3 +} \ No newline at end of file diff --git a/cmd/horcrux/cmd/testdata/testdata.go b/cmd/horcrux/cmd/testdata/testdata.go new file mode 100644 index 00000000..775c1734 --- /dev/null +++ b/cmd/horcrux/cmd/testdata/testdata.go @@ -0,0 +1,20 @@ +package testdata + +import ( + _ "embed" // required to embed files +) + +//go:embed config-migrated.yaml +var ConfigMigrated string + +//go:embed config-v2.yaml +var ConfigV2 []byte + +//go:embed cosigner-key-migrated-ed25519.json +var CosignerKeyMigratedEd25519 string + +//go:embed cosigner-key-migrated-rsa.json +var CosignerKeyMigratedRSA string + +//go:embed cosigner-key-v2.json +var CosignerKeyV2 []byte diff --git a/docs/migrating.md b/docs/migrating.md index 219ad602..591e58c0 100644 --- a/docs/migrating.md +++ b/docs/migrating.md @@ -68,15 +68,15 @@ After that is done, initialize the configuration for each node using the `horcru ```bash # Run this command on the signer-1 VM # signer-1 connects to sentry-1 -$ horcrux config init {my_chain_id} "tcp://10.168.0.1:1234" -c -p "tcp://10.168.1.2:2222|2,tcp://10.168.1.3:2222|3" -l "tcp://10.168.1.1:2222" -t 2 --timeout 1500ms +$ horcrux config init "tcp://10.168.0.1:1234" -c -p "tcp://10.168.1.2:2222|2,tcp://10.168.1.3:2222|3" -l "tcp://10.168.1.1:2222" -t 2 # Run this command on the signer-2 VM # signer-2 connects to sentry-2 -$ horcrux config init {my_chain_id} "tcp://10.168.0.2:1234" -c -p "tcp://10.168.1.1:2222|1,tcp://10.168.1.3:2222|3" -l "tcp://10.168.1.2:2222" -t 2 --timeout 1500ms +$ horcrux config init "tcp://10.168.0.2:1234" -c -p "tcp://10.168.1.1:2222|1,tcp://10.168.1.3:2222|3" -l "tcp://10.168.1.2:2222" -t 2 # Run this command on the signer-3 VM # signer-3 connects to sentry-3 -$ horcrux config init {my_chain_id} "tcp://10.168.0.3:1234" -c -p "tcp://10.168.1.1:2222|1,tcp://10.168.1.2:2222|2" -l "tcp://10.168.1.3:2222" -t 2 --timeout 1500ms +$ horcrux config init "tcp://10.168.0.3:1234" -c -p "tcp://10.168.1.1:2222|1,tcp://10.168.1.2:2222|2" -l "tcp://10.168.1.3:2222" -t 2 ``` > **Note** @@ -97,27 +97,41 @@ $ horcrux config init {my_chain_id} "tcp://10.168.0.3:1234" -c -p "tcp://10.168. > **CAUTION:** **The security of any key material is outside the scope of this guide. The suggested procedure here is not necessarily the one you will use. We aim to make this guide easy to understand, not necessarily the most secure. The tooling here is all written in go and can be compiled and used in an airgapped setup if needed. Please open issues if you have questions about how to fit `horcrux` into your infra.** -On some computer that contains your `priv_validator_key.json` create a folder to split the key through the following command. This may take a moment o complete: +On some computer that contains your `priv_validator_key.json` create a folder to split the key through the following command. This may take a moment to complete: ```bash $ ls priv_validator_key.json -$ horcrux create-shares priv_validator_key.json 2 3 -Created Share 1 -Created Share 2 -Created Share 3 +$ horcrux create-ed25519-shares --chain-id cosmoshub-4 --key-file priv_validator_key.json --threshold 2 --shares 3 +Created Ed25519 Share cosigner_1/cosmoshub-4_share.json +Created Ed25519 Share cosigner_2/cosmoshub-4_share.json +Created Ed25519 Share cosigner_3/cosmoshub-4_share.json -$ ls -priv_validator_key.json -private_share_1.json -private_share_2.json -private_share_3.json +$ horcrux create-rsa-shares --shares 3 +Created RSA Share cosigner_1/rsa_keys.json +Created RSA Share cosigner_2/rsa_keys.json +Created RSA Share cosigner_3/rsa_keys.json + +$ ls -R +.: +cosigner_1 cosigner_2 cosigner_3 priv_validator_key.json + +./cosigner_1: +cosmoshub-4_share.json rsa_keys.json + +./cosigner_2: +cosmoshub-4_share.json rsa_keys.json + +./cosigner_3: +cosmoshub-4_share.json rsa_keys.json ``` -The shares need to be moved their co-responding signer nodes at `~/.horcrux/share.json`. It is very important to make sure the share id (in `private_share_.json`) is on the corresponding cosigner node otherwise your signer cluster won't communicate properly and will not sign blocks. If you have named your nodes with their index as the signer index, as in this guide, this operation should be easy to check. +The files need to be moved their corresponding signer nodes in the `~/.horcrux/` directory. It is important to make sure the files for the cosigner `{id}` (in `cosigner_{id}`) are placed on the corresponding cosigner node. If not, the cluster will not produce valid signatures. If you have named your nodes with their index as the signer index, as in this guide, this operation should be easy to check. + +At the end of this step, each of your horcrux nodes will have a `~/.horcrux/{chain-id}_share.json` file with the contents matching the appropriate `cosigner_{id}/share.json` file corresponding to the node number. Additionally, each of your horcrux nodes will have a `~/.horcrux/rsa_keys.json` file with the contents matching the appropriate `cosigner_{id}/rsa_keys.json` file corresponding to the node number. -At the end of this step, each of your horcrux nodes will have a `~/.horcrux/share.json` file with the contents matching the appropriate `private_share_.json` file corresponding to the node number. +If you will be signing for multiple chains with this single horcrux cluster, repeat the `horcrux create-ed25519-shares` command with the `priv_validator_key.json` for each additional chain ID, and again place on the corresponding horcrux nodes. ### 4. Halt your validator node and supply signer state data `horcrux` nodes diff --git a/signer/config.go b/signer/config.go index d806e026..b9744ded 100644 --- a/signer/config.go +++ b/signer/config.go @@ -28,7 +28,7 @@ type NodeConfig struct { // Config maps to the on-disk JSON format type Config struct { - PrivValKeyFile *string `json:"key-file,omitempty" yaml:"key-file,omitempty"` + PrivValKeyDir *string `json:"key-dir,omitempty" yaml:"key-dir,omitempty"` CosignerConfig *CosignerConfig `json:"cosigner,omitempty" yaml:"cosigner,omitempty"` ChainNodes ChainNodes `json:"chain-nodes,omitempty" yaml:"chain-nodes,omitempty"` DebugAddr string `json:"debug-addr,omitempty" yaml:"debug-addr,omitempty"` @@ -100,25 +100,35 @@ type RuntimeConfig struct { Config Config } -func (c RuntimeConfig) cachedKeyFile() string { - if c.Config.PrivValKeyFile != nil { - return *c.Config.PrivValKeyFile +func (c RuntimeConfig) cachedKeyDirectory() string { + if c.Config.PrivValKeyDir != nil { + return *c.Config.PrivValKeyDir } return "" } -func (c RuntimeConfig) KeyFilePathSingleSigner() string { - if kf := c.cachedKeyFile(); kf != "" { - return kf +func (c RuntimeConfig) KeyFilePathSingleSigner(chainID string) string { + keyDir := c.HomeDir + if kd := c.cachedKeyDirectory(); kd != "" { + keyDir = kd } - return filepath.Join(c.HomeDir, "priv_validator_key.json") + return filepath.Join(keyDir, fmt.Sprintf("%s_priv_validator_key.json", chainID)) } -func (c RuntimeConfig) KeyFilePathCosigner() string { - if kf := c.cachedKeyFile(); kf != "" { - return kf +func (c RuntimeConfig) KeyFilePathCosigner(chainID string) string { + keyDir := c.HomeDir + if kd := c.cachedKeyDirectory(); kd != "" { + keyDir = kd } - return filepath.Join(c.HomeDir, "share.json") + return filepath.Join(keyDir, fmt.Sprintf("%s_share.json", chainID)) +} + +func (c RuntimeConfig) KeyFilePathCosignerRSA() string { + keyDir := c.HomeDir + if kd := c.cachedKeyDirectory(); kd != "" { + keyDir = kd + } + return filepath.Join(keyDir, "rsa_keys.json") } func (c RuntimeConfig) PrivValStateFile(chainID string) string { @@ -148,13 +158,18 @@ func fileExists(file string) error { return nil } -func (c RuntimeConfig) KeyFileExistsSingleSigner() (string, error) { - keyFile := c.KeyFilePathSingleSigner() +func (c RuntimeConfig) KeyFileExistsSingleSigner(chainID string) (string, error) { + keyFile := c.KeyFilePathSingleSigner(chainID) + return keyFile, fileExists(keyFile) +} + +func (c RuntimeConfig) KeyFileExistsCosigner(chainID string) (string, error) { + keyFile := c.KeyFilePathCosigner(chainID) return keyFile, fileExists(keyFile) } -func (c RuntimeConfig) KeyFileExistsCosigner() (string, error) { - keyFile := c.KeyFilePathCosigner() +func (c RuntimeConfig) KeyFileExistsCosignerRSA() (string, error) { + keyFile := c.KeyFilePathCosignerRSA() return keyFile, fileExists(keyFile) } diff --git a/signer/config_test.go b/signer/config_test.go index 18787fed..0beff6c9 100644 --- a/signer/config_test.go +++ b/signer/config_test.go @@ -13,6 +13,8 @@ import ( "github.com/stretchr/testify/require" ) +const testChainID = "test" + func TestNodes(t *testing.T) { c := signer.Config{ ChainNodes: signer.ChainNodes{ @@ -304,8 +306,12 @@ func TestRuntimeConfigKeyFilePath(t *testing.T) { HomeDir: dir, } - require.Equal(t, filepath.Join(dir, "share.json"), c.KeyFilePathCosigner()) - require.Equal(t, filepath.Join(dir, "priv_validator_key.json"), c.KeyFilePathSingleSigner()) + require.Equal(t, filepath.Join(dir, fmt.Sprintf("%s_share.json", testChainID)), c.KeyFilePathCosigner(testChainID)) + require.Equal( + t, + filepath.Join(dir, fmt.Sprintf("%s_priv_validator_key.json", testChainID)), + c.KeyFilePathSingleSigner(testChainID), + ) } func TestRuntimeConfigPrivValStateFile(t *testing.T) { @@ -389,7 +395,7 @@ func TestRuntimeConfigKeyFileExists(t *testing.T) { } // Test cosigner - keyFile, err := c.KeyFileExistsCosigner() + keyFile, err := c.KeyFileExistsCosigner(testChainID) require.Error(t, err) require.Equal(t, fmt.Errorf( @@ -405,11 +411,11 @@ func TestRuntimeConfigKeyFileExists(t *testing.T) { err = os.WriteFile(keyFile, []byte{}, 0600) require.NoError(t, err) - _, err = c.KeyFileExistsCosigner() + _, err = c.KeyFileExistsCosigner(testChainID) require.NoError(t, err) // Test single signer - keyFile, err = c.KeyFileExistsSingleSigner() + keyFile, err = c.KeyFileExistsSingleSigner(testChainID) require.Error(t, err) require.Equal(t, fmt.Errorf( @@ -425,7 +431,7 @@ func TestRuntimeConfigKeyFileExists(t *testing.T) { err = os.WriteFile(keyFile, []byte{}, 0600) require.NoError(t, err) - _, err = c.KeyFileExistsSingleSigner() + _, err = c.KeyFileExistsSingleSigner(testChainID) require.NoError(t, err) } diff --git a/signer/cosigner.go b/signer/cosigner.go index a8bd04bb..035d46d4 100644 --- a/signer/cosigner.go +++ b/signer/cosigner.go @@ -3,6 +3,7 @@ package signer import ( "time" + cometcrypto "github.com/cometbft/cometbft/crypto" "github.com/strangelove-ventures/horcrux/signer/proto" ) @@ -137,6 +138,11 @@ type Cosigner interface { // Get the P2P URL (GRPC and Raft) GetAddress() string + // Get the combined public key + GetPubKey(chainID string) (cometcrypto.PubKey, error) + + VerifySignature(chainID string, payload, signature []byte) bool + // Get ephemeral secret part for all peers GetEphemeralSecretParts(chainID string, hrst HRSTKey) (*CosignerEphemeralSecretPartsResponse, error) diff --git a/signer/cosigner_key.go b/signer/cosigner_key.go index e46b3ebe..9ab228eb 100644 --- a/signer/cosigner_key.go +++ b/signer/cosigner_key.go @@ -13,26 +13,16 @@ import ( amino "github.com/tendermint/go-amino" ) -// CosignerKey is a single key for an m-of-n threshold signer. +// CosignerKey is a single Ed255219 key shard for an m-of-n threshold signer. type CosignerKey struct { - PubKey cometcrypto.PubKey `json:"pub_key"` - ShareKey []byte `json:"secret_share"` - RSAKey rsa.PrivateKey `json:"rsa_key"` - ID int `json:"id"` - CosignerKeys []*rsa.PublicKey `json:"rsa_pubs"` + PubKey cometcrypto.PubKey `json:"pub_key"` + ShareKey []byte `json:"secret_share"` + ID int `json:"id"` } func (cosignerKey *CosignerKey) MarshalJSON() ([]byte, error) { type Alias CosignerKey - // marshal our private key and all public keys - privateBytes := x509.MarshalPKCS1PrivateKey(&cosignerKey.RSAKey) - rsaPubKeysBytes := make([][]byte, 0) - for _, pubKey := range cosignerKey.CosignerKeys { - publicBytes := x509.MarshalPKCS1PublicKey(pubKey) - rsaPubKeysBytes = append(rsaPubKeysBytes, publicBytes) - } - protoPubkey, err := cometcryptoencoding.PubKeyToProto(cosignerKey.PubKey) if err != nil { return nil, err @@ -44,15 +34,11 @@ func (cosignerKey *CosignerKey) MarshalJSON() ([]byte, error) { } return json.Marshal(&struct { - RSAKey []byte `json:"rsa_key"` - Pubkey []byte `json:"pub_key"` - CosignerKeys [][]byte `json:"rsa_pubs"` + Pubkey []byte `json:"pub_key"` *Alias }{ - Pubkey: protoBytes, - RSAKey: privateBytes, - CosignerKeys: rsaPubKeysBytes, - Alias: (*Alias)(cosignerKey), + Pubkey: protoBytes, + Alias: (*Alias)(cosignerKey), }) } @@ -60,9 +46,7 @@ func (cosignerKey *CosignerKey) UnmarshalJSON(data []byte) error { type Alias CosignerKey aux := &struct { - RSAKey []byte `json:"rsa_key"` - PubkeyBytes []byte `json:"pub_key"` - CosignerKeys [][]byte `json:"rsa_pubs"` + PubkeyBytes []byte `json:"pub_key"` *Alias }{ Alias: (*Alias)(cosignerKey), @@ -70,14 +54,10 @@ func (cosignerKey *CosignerKey) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &aux); err != nil { return err } - privateKey, err := x509.ParsePKCS1PrivateKey(aux.RSAKey) - if err != nil { - return err - } var pubkey cometcrypto.PubKey var protoPubkey cometprotocrypto.PublicKey - err = protoPubkey.Unmarshal(aux.PubkeyBytes) + err := protoPubkey.Unmarshal(aux.PubkeyBytes) // Prior to the tendermint protobuf migration, the public key bytes in key files // were encoded using the go-amino libraries via @@ -102,6 +82,73 @@ func (cosignerKey *CosignerKey) UnmarshalJSON(data []byte) error { } } + cosignerKey.PubKey = pubkey + return nil +} + +// LoadCosignerKey loads a CosignerKey from file. +func LoadCosignerKey(file string) (CosignerKey, error) { + pvKey := CosignerKey{} + keyJSONBytes, err := os.ReadFile(file) + if err != nil { + return pvKey, err + } + + err = json.Unmarshal(keyJSONBytes, &pvKey) + if err != nil { + return pvKey, err + } + + return pvKey, nil +} + +// CosignerKey is a single RSA key for an m-of-n threshold signer. +type CosignerKeyRSA struct { + RSAKey rsa.PrivateKey `json:"rsa_key"` + ID int `json:"id"` + CosignerKeys []*rsa.PublicKey `json:"rsa_pubs"` +} + +func (cosignerKey *CosignerKeyRSA) MarshalJSON() ([]byte, error) { + type Alias CosignerKeyRSA + + // marshal our private key and all public keys + privateBytes := x509.MarshalPKCS1PrivateKey(&cosignerKey.RSAKey) + rsaPubKeysBytes := make([][]byte, 0) + for _, pubKey := range cosignerKey.CosignerKeys { + publicBytes := x509.MarshalPKCS1PublicKey(pubKey) + rsaPubKeysBytes = append(rsaPubKeysBytes, publicBytes) + } + + return json.Marshal(&struct { + RSAKey []byte `json:"rsa_key"` + CosignerKeys [][]byte `json:"rsa_pubs"` + *Alias + }{ + RSAKey: privateBytes, + CosignerKeys: rsaPubKeysBytes, + Alias: (*Alias)(cosignerKey), + }) +} + +func (cosignerKey *CosignerKeyRSA) UnmarshalJSON(data []byte) error { + type Alias CosignerKeyRSA + + aux := &struct { + RSAKey []byte `json:"rsa_key"` + CosignerKeys [][]byte `json:"rsa_pubs"` + *Alias + }{ + Alias: (*Alias)(cosignerKey), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + privateKey, err := x509.ParsePKCS1PrivateKey(aux.RSAKey) + if err != nil { + return err + } + // unmarshal the public key bytes for each cosigner cosignerKey.CosignerKeys = make([]*rsa.PublicKey, 0) for _, bytes := range aux.CosignerKeys { @@ -113,13 +160,12 @@ func (cosignerKey *CosignerKey) UnmarshalJSON(data []byte) error { } cosignerKey.RSAKey = *privateKey - cosignerKey.PubKey = pubkey return nil } -// LoadCosignerKey loads a CosignerKey from file. -func LoadCosignerKey(file string) (CosignerKey, error) { - pvKey := CosignerKey{} +// LoadCosignerKeyRSA loads a CosignerKeyRSA from file. +func LoadCosignerKeyRSA(file string) (CosignerKeyRSA, error) { + pvKey := CosignerKeyRSA{} keyJSONBytes, err := os.ReadFile(file) if err != nil { return pvKey, err diff --git a/signer/cosigner_key_shares.go b/signer/cosigner_key_shares.go index a488bd2a..7575fb76 100644 --- a/signer/cosigner_key_shares.go +++ b/signer/cosigner_key_shares.go @@ -12,7 +12,7 @@ import ( ) // CreateCosignerSharesFromFile creates cosigner key objects from a priv_validator_key.json file -func CreateCosignerSharesFromFile(priv string, threshold, shares int64) ([]CosignerKey, error) { +func CreateCosignerSharesFromFile(priv string, threshold, shares uint8) ([]CosignerKey, error) { pv, err := ReadPrivValidatorFile(priv) if err != nil { return nil, err @@ -21,22 +21,32 @@ func CreateCosignerSharesFromFile(priv string, threshold, shares int64) ([]Cosig } // CreateCosignerShares creates cosigner key objects from a privval.FilePVKey -func CreateCosignerShares(pv privval.FilePVKey, threshold, shares int64) (out []CosignerKey, err error) { +func CreateCosignerShares(pv privval.FilePVKey, threshold, shares uint8) (out []CosignerKey, err error) { privshares := tsed25519.DealShares(tsed25519.ExpandSecret(pv.PrivKey.Bytes()[:32]), uint8(threshold), uint8(shares)) - rsaKeys, pubKeys, err := makeRSAKeys(len(privshares)) + for idx, share := range privshares { + out = append(out, CosignerKey{ + PubKey: pv.PubKey, + ShareKey: share, + ID: idx + 1, + }) + } + return out, nil +} + +// CreateCosignerShares creates cosigner key objects from a privval.FilePVKey +func CreateCosignerSharesRSA(shares int) (out []CosignerKeyRSA, err error) { + rsaKeys, pubKeys, err := makeRSAKeys(shares) if err != nil { return nil, err } - for idx, share := range privshares { - out = append(out, CosignerKey{ - PubKey: pv.PubKey, - ShareKey: share, - ID: idx + 1, - RSAKey: *rsaKeys[idx], + for i, key := range rsaKeys { + out = append(out, CosignerKeyRSA{ + ID: i + 1, + RSAKey: *key, CosignerKeys: pubKeys, }) } - return + return out, nil } // ReadPrivValidatorFile reads in a privval.FilePVKey from a given file @@ -57,7 +67,16 @@ func WriteCosignerShareFile(cosigner CosignerKey, file string) error { if err != nil { return err } - return os.WriteFile(file, jsonBytes, 0644) //nolint + return os.WriteFile(file, jsonBytes, 0600) +} + +// WriteCosignerShareRSAFile writes a cosigner RSA key to a given file name +func WriteCosignerShareRSAFile(cosigner CosignerKeyRSA, file string) error { + jsonBytes, err := json.Marshal(&cosigner) + if err != nil { + return err + } + return os.WriteFile(file, jsonBytes, 0600) } func makeRSAKeys(num int) (rsaKeys []*rsa.PrivateKey, pubKeys []*rsa.PublicKey, err error) { diff --git a/signer/cosigner_key_test.go b/signer/cosigner_key_test.go index aaf472a3..b7c462f1 100644 --- a/signer/cosigner_key_test.go +++ b/signer/cosigner_key_test.go @@ -1,13 +1,23 @@ package signer import ( + "os" + "path/filepath" "testing" + "github.com/strangelove-ventures/horcrux/signer/testdata" "github.com/stretchr/testify/require" ) func TestLoadCosignerKey(t *testing.T) { - key, err := LoadCosignerKey("./fixtures/cosigner-key.json") + tmp := t.TempDir() + + rsaKeyFile := filepath.Join(tmp, "rsa_keys.json") + + err := os.WriteFile(rsaKeyFile, testdata.RSAKeys, 0600) + require.NoError(t, err) + + key, err := LoadCosignerKeyRSA(rsaKeyFile) require.NoError(t, err) require.Equal(t, key.ID, 3) diff --git a/signer/local_cosigner.go b/signer/local_cosigner.go index 93c202a8..61e0c894 100644 --- a/signer/local_cosigner.go +++ b/signer/local_cosigner.go @@ -11,12 +11,15 @@ import ( "sync" "time" + cometcrypto "github.com/cometbft/cometbft/crypto" cometcryptoed25519 "github.com/cometbft/cometbft/crypto/ed25519" cometjson "github.com/cometbft/cometbft/libs/json" "gitlab.com/unit410/edwards25519" tsed25519 "gitlab.com/unit410/threshold-ed25519/pkg" ) +var _ Cosigner = &LocalCosigner{} + type LastSignStateWrapper struct { // Signing is thread safe - lastSignStateMutex is used for putting locks so only one goroutine can r/w to the function mu sync.Mutex @@ -34,6 +37,9 @@ type ChainState struct { // incremented whenever we are asked to sign a share lastSignState *SignState + pubKeyBytes []byte + key CosignerKey + // Height, Round, Step -> metadata hrsMeta map[HRSTKey]HrsMetadata } @@ -89,13 +95,12 @@ type CosignerGetEphemeralSecretPartRequest struct { type LocalCosigner struct { config *RuntimeConfig - pubKeyBytes []byte - key CosignerKey - rsaKey rsa.PrivateKey - total uint8 - threshold uint8 + id int + rsaKey rsa.PrivateKey + total uint8 + threshold uint8 - chainState map[string]*ChainState + chainState sync.Map peers map[int]CosignerPeer @@ -109,9 +114,19 @@ type LocalCosigner struct { // The disk write is scheduled in a separate goroutine which will perform an atomic write. // pendingDiskWG is used upon termination in pendingDiskWG to ensure all writes have completed. func (cosigner *LocalCosigner) SaveLastSignedState(chainID string, signState SignStateConsensus) error { - cosigner.chainState[chainID].mu.Lock() - defer cosigner.chainState[chainID].mu.Unlock() - return cosigner.chainState[chainID].lastSignState.Save( + cs, ok := cosigner.chainState.Load(chainID) + if !ok { + return fmt.Errorf("failed to load chain state for %s", chainID) + } + + ccs, ok := cs.(*ChainState) + if !ok { + return fmt.Errorf("expected: (*ChainState), actual: (%T)", cs) + } + + ccs.mu.Lock() + defer ccs.mu.Unlock() + return ccs.lastSignState.Save( signState, &cosigner.pendingDiskWG, ) @@ -125,7 +140,7 @@ func (cosigner *LocalCosigner) waitForSignStatesToFlushToDisk() { func NewLocalCosigner( config *RuntimeConfig, - cosignerKey CosignerKey, + id int, rsaKey rsa.PrivateKey, peers []CosignerPeer, address string, @@ -133,30 +148,26 @@ func NewLocalCosigner( threshold uint8, ) *LocalCosigner { cosigner := &LocalCosigner{ - config: config, - key: cosignerKey, - rsaKey: rsaKey, - chainState: make(map[string]*ChainState), - peers: make(map[int]CosignerPeer), - total: total, - threshold: threshold, - address: address, + config: config, + id: id, + rsaKey: rsaKey, + peers: make(map[int]CosignerPeer), + total: total, + threshold: threshold, + address: address, } for _, peer := range peers { cosigner.peers[peer.ID] = peer } - // cache the public key bytes for signing operations - cosigner.pubKeyBytes = cosigner.key.PubKey.(cometcryptoed25519.PubKey)[:] - return cosigner } // GetID returns the id of the cosigner // Implements Cosigner interface func (cosigner *LocalCosigner) GetID() int { - return cosigner.key.ID + return cosigner.id } // GetAddress returns the RPC URL of the cosigner @@ -165,19 +176,71 @@ func (cosigner *LocalCosigner) GetAddress() string { return cosigner.address } +// GetPubKey returns public key of the validator. +// Implements Cosigner interface +func (cosigner *LocalCosigner) GetPubKey(chainID string) (cometcrypto.PubKey, error) { + if err := cosigner.LoadSignStateIfNecessary(chainID); err != nil { + return nil, err + } + + cs, ok := cosigner.chainState.Load(chainID) + if !ok { + return nil, fmt.Errorf("failed to load chain state for %s", chainID) + } + + ccs, ok := cs.(*ChainState) + if !ok { + return nil, fmt.Errorf("expected: (*ChainState), actual: (%T)", cs) + } + + return ccs.key.PubKey, nil +} + +// VerifySignature validates a signed payload against the public key. +// Implements Cosigner interface +func (cosigner *LocalCosigner) VerifySignature(chainID string, payload, signature []byte) bool { + if err := cosigner.LoadSignStateIfNecessary(chainID); err != nil { + return false + } + + cs, ok := cosigner.chainState.Load(chainID) + if !ok { + return false + } + + ccs, ok := cs.(*ChainState) + if !ok { + return false + } + + return ccs.key.PubKey.VerifySignature(payload, signature) +} + // Sign the sign request using the cosigner's share // Return the signed bytes or an error // Implements Cosigner interface func (cosigner *LocalCosigner) sign(req CosignerSignRequest) (CosignerSignResponse, error) { chainID := req.ChainID + + res := CosignerSignResponse{} + + cs, ok := cosigner.chainState.Load(chainID) + if !ok { + return res, fmt.Errorf("failed to load chain state for %s", chainID) + } + + ccs, ok := cs.(*ChainState) + if !ok { + return res, fmt.Errorf("expected: (*ChainState), actual: (%T)", cs) + } + // This function has multiple exit points. Only start time can be guaranteed metricsTimeKeeper.SetPreviousLocalSignStart(time.Now()) - cosigner.chainState[chainID].mu.Lock() - defer cosigner.chainState[chainID].mu.Unlock() + ccs.mu.Lock() + defer ccs.mu.Unlock() - res := CosignerSignResponse{} - lss := cosigner.chainState[chainID].lastSignState + lss := ccs.lastSignState hrst, err := UnpackHRST(req.SignBytes) if err != nil { @@ -203,7 +266,7 @@ func (cosigner *LocalCosigner) sign(req CosignerSignRequest) (CosignerSignRespon // same HRS, and only differ by timestamp - ok to sign again } - meta, ok := cosigner.chainState[chainID].hrsMeta[hrst] + meta, ok := ccs.hrsMeta[hrst] if !ok { return res, errors.New("no metadata at HRS") } @@ -237,10 +300,10 @@ func (cosigner *LocalCosigner) sign(req CosignerSignRequest) (CosignerSignRespon } sig := tsed25519.SignWithShare( - req.SignBytes, cosigner.key.ShareKey, ephemeralShare, cosigner.pubKeyBytes, ephemeralPublic) + req.SignBytes, ccs.key.ShareKey, ephemeralShare, ccs.pubKeyBytes, ephemeralPublic) - cosigner.chainState[chainID].lastSignState.EphemeralPublic = ephemeralPublic - err = cosigner.chainState[chainID].lastSignState.Save(SignStateConsensus{ + ccs.lastSignState.EphemeralPublic = ephemeralPublic + err = ccs.lastSignState.Save(SignStateConsensus{ Height: hrst.Height, Round: hrst.Round, Step: hrst.Step, @@ -254,11 +317,11 @@ func (cosigner *LocalCosigner) sign(req CosignerSignRequest) (CosignerSignRespon } } - for existingKey := range cosigner.chainState[chainID].hrsMeta { + for existingKey := range ccs.hrsMeta { // delete any HRS lower than our signed level // we will not be providing parts for any lower HRS if existingKey.Less(hrst) { - delete(cosigner.chainState[chainID].hrsMeta, existingKey) + delete(ccs.hrsMeta, existingKey) } } @@ -274,6 +337,16 @@ func (cosigner *LocalCosigner) sign(req CosignerSignRequest) (CosignerSignRespon func (cosigner *LocalCosigner) dealShares(req CosignerGetEphemeralSecretPartRequest) (HrsMetadata, error) { chainID := req.ChainID + cs, ok := cosigner.chainState.Load(chainID) + if !ok { + return HrsMetadata{}, fmt.Errorf("failed to load chain state for %s", chainID) + } + + ccs, ok := cs.(*ChainState) + if !ok { + return HrsMetadata{}, fmt.Errorf("expected: (*ChainState), actual: (%T)", cs) + } + hrsKey := HRSTKey{ Height: req.Height, Round: req.Round, @@ -281,7 +354,7 @@ func (cosigner *LocalCosigner) dealShares(req CosignerGetEphemeralSecretPartRequ Timestamp: req.Timestamp.UnixNano(), } - meta, ok := cosigner.chainState[chainID].hrsMeta[hrsKey] + meta, ok := ccs.hrsMeta[hrsKey] if ok { return meta, nil @@ -301,14 +374,18 @@ func (cosigner *LocalCosigner) dealShares(req CosignerGetEphemeralSecretPartRequ // !! dealt shares need to be saved because dealing produces different shares each time! meta.DealtShares = tsed25519.DealShares(meta.Secret, cosigner.threshold, cosigner.total) - cosigner.chainState[chainID].hrsMeta[hrsKey] = meta + ccs.hrsMeta[hrsKey] = meta return meta, nil } func (cosigner *LocalCosigner) LoadSignStateIfNecessary(chainID string) error { - if _, ok := cosigner.chainState[chainID]; ok { + if chainID == "" { + return fmt.Errorf("chain id cannot be empty") + } + + if _, ok := cosigner.chainState.Load(chainID); ok { return nil } @@ -317,10 +394,27 @@ func (cosigner *LocalCosigner) LoadSignStateIfNecessary(chainID string) error { return err } - cosigner.chainState[chainID] = &ChainState{ + keyFile, err := cosigner.config.KeyFileExistsCosigner(chainID) + if err != nil { + return err + } + + key, err := LoadCosignerKey(keyFile) + if err != nil { + return fmt.Errorf("error reading cosigner key: %s", err) + } + + if key.ID != cosigner.id { + return fmt.Errorf("key shard ID (%d) in (%s) does not match cosigner ID (%d)", key.ID, keyFile, cosigner.id) + } + + cosigner.chainState.Store(chainID, &ChainState{ lastSignState: shareSignState, hrsMeta: make(map[HRSTKey]HrsMetadata), - } + // cache the public key bytes for signing operations + key: key, + pubKeyBytes: key.PubKey.(cometcryptoed25519.PubKey)[:], + }) return nil } @@ -339,8 +433,10 @@ func (cosigner *LocalCosigner) GetEphemeralSecretParts( EncryptedSecrets: make([]CosignerEphemeralSecretPart, 0, len(cosigner.peers)-1), } + id := cosigner.GetID() + for _, peer := range cosigner.peers { - if peer.ID == cosigner.GetID() { + if peer.ID == id { continue } secretPart, err := cosigner.getEphemeralSecretPart(CosignerGetEphemeralSecretPartRequest{ @@ -369,9 +465,19 @@ func (cosigner *LocalCosigner) getEphemeralSecretPart( chainID := req.ChainID res := CosignerEphemeralSecretPart{} + cs, ok := cosigner.chainState.Load(chainID) + if !ok { + return res, fmt.Errorf("failed to load chain state for %s", chainID) + } + + ccs, ok := cs.(*ChainState) + if !ok { + return res, fmt.Errorf("expected: (*ChainState), actual: (%T)", cs) + } + // protects the meta map - cosigner.chainState[chainID].mu.Lock() - defer cosigner.chainState[chainID].mu.Unlock() + ccs.mu.Lock() + defer ccs.mu.Unlock() hrst := HRSTKey{ Height: req.Height, @@ -380,7 +486,7 @@ func (cosigner *LocalCosigner) getEphemeralSecretPart( Timestamp: req.Timestamp.UnixNano(), } - meta, ok := cosigner.chainState[chainID].hrsMeta[hrst] + meta, ok := ccs.hrsMeta[hrst] // generate metadata placeholder if !ok { newMeta, err := cosigner.dealShares(CosignerGetEphemeralSecretPartRequest{ @@ -396,14 +502,16 @@ func (cosigner *LocalCosigner) getEphemeralSecretPart( } meta = newMeta - cosigner.chainState[chainID].hrsMeta[hrst] = meta + ccs.hrsMeta[hrst] = meta } ourEphPublicKey := tsed25519.ScalarMultiplyBase(meta.Secret) + id := ccs.key.ID + // set our values - meta.Peers[cosigner.key.ID-1].Share = meta.DealtShares[cosigner.key.ID-1] - meta.Peers[cosigner.key.ID-1].EphemeralSecretPublicKey = ourEphPublicKey + meta.Peers[id-1].Share = meta.DealtShares[id-1] + meta.Peers[id-1].EphemeralSecretPublicKey = ourEphPublicKey // grab the peer info for the ID being requested peer, ok := cosigner.peers[req.ID] @@ -419,7 +527,7 @@ func (cosigner *LocalCosigner) getEphemeralSecretPart( return res, err } - res.SourceID = cosigner.key.ID + res.SourceID = id res.SourceEphemeralSecretPublicKey = ourEphPublicKey res.EncryptedSharePart = encrypted @@ -450,6 +558,16 @@ func (cosigner *LocalCosigner) getEphemeralSecretPart( func (cosigner *LocalCosigner) setEphemeralSecretPart(req CosignerSetEphemeralSecretPartRequest) error { chainID := req.ChainID + cs, ok := cosigner.chainState.Load(chainID) + if !ok { + return fmt.Errorf("failed to load chain state for %s", chainID) + } + + ccs, ok := cs.(*ChainState) + if !ok { + return fmt.Errorf("expected: (*ChainState), actual: (%T)", cs) + } + // Verify the source signature if req.SourceSig == nil { return errors.New("SourceSig field is required") @@ -480,8 +598,8 @@ func (cosigner *LocalCosigner) setEphemeralSecretPart(req CosignerSetEphemeralSe } // protects the meta map - cosigner.chainState[chainID].mu.Lock() - defer cosigner.chainState[chainID].mu.Unlock() + ccs.mu.Lock() + defer ccs.mu.Unlock() hrst := HRSTKey{ Height: req.Height, @@ -490,7 +608,7 @@ func (cosigner *LocalCosigner) setEphemeralSecretPart(req CosignerSetEphemeralSe Timestamp: req.Timestamp.UnixNano(), } - meta, ok := cosigner.chainState[chainID].hrsMeta[hrst] + meta, ok := ccs.hrsMeta[hrst] // generate metadata placeholder if !ok { newMeta, err := cosigner.dealShares(CosignerGetEphemeralSecretPartRequest{ @@ -505,7 +623,7 @@ func (cosigner *LocalCosigner) setEphemeralSecretPart(req CosignerSetEphemeralSe } meta = newMeta - cosigner.chainState[chainID].hrsMeta[hrst] = meta + ccs.hrsMeta[hrst] = meta } // decrypt share diff --git a/signer/local_cosigner_test.go b/signer/local_cosigner_test.go index 911b8f37..f607cfd8 100644 --- a/signer/local_cosigner_test.go +++ b/signer/local_cosigner_test.go @@ -15,12 +15,15 @@ import ( tsed25519 "gitlab.com/unit410/threshold-ed25519/pkg" ) -const testChainID = "test" +const ( + testChainID = "chain-1" + testChainID2 = "chain-2" + bitSize = 4096 +) func TestLocalCosignerGetID(t *testing.T) { dummyPub := cometcryptoed25519.PubKey{} - bitSize := 4096 rsaKey, err := rsa.GenerateKey(rand.Reader, bitSize) require.NoError(t, err) @@ -32,7 +35,7 @@ func TestLocalCosignerGetID(t *testing.T) { cosigner := NewLocalCosigner( &RuntimeConfig{}, - key, + key.ID, *rsaKey, []CosignerPeer{{ ID: 1, @@ -42,7 +45,8 @@ func TestLocalCosignerGetID(t *testing.T) { 0, 0, ) - require.Equal(t, cosigner.GetID(), 1) + + require.Equal(t, 1, cosigner.GetID()) } func TestLocalCosignerSign2of2(t *testing.T) { @@ -51,7 +55,6 @@ func TestLocalCosignerSign2of2(t *testing.T) { total := uint8(2) threshold := uint8(2) - bitSize := 4096 rsaKey1, err := rsa.GenerateKey(rand.Reader, bitSize) require.NoError(t, err) @@ -94,24 +97,43 @@ func TestLocalCosignerSign2of2(t *testing.T) { require.NoError(t, err) cosigner1 := NewLocalCosigner( - &RuntimeConfig{StateDir: cosigner1Dir}, - key1, + &RuntimeConfig{ + HomeDir: cosigner1Dir, + StateDir: cosigner1Dir, + }, + key1.ID, *rsaKey1, peers, "", total, threshold, ) + + key1Bz, err := key1.MarshalJSON() + require.NoError(t, err) + err = os.WriteFile(cosigner1.config.KeyFilePathCosigner(testChainID), key1Bz, 0600) + require.NoError(t, err) + defer cosigner1.waitForSignStatesToFlushToDisk() + cosigner2 := NewLocalCosigner( - &RuntimeConfig{StateDir: cosigner2Dir}, - key2, + &RuntimeConfig{ + HomeDir: cosigner2Dir, + StateDir: cosigner2Dir, + }, + key2.ID, *rsaKey2, peers, "", total, threshold, ) + + key2Bz, err := key2.MarshalJSON() + require.NoError(t, err) + err = os.WriteFile(cosigner2.config.KeyFilePathCosigner(testChainID), key2Bz, 0600) + require.NoError(t, err) + defer cosigner2.waitForSignStatesToFlushToDisk() err = cosigner1.LoadSignStateIfNecessary(testChainID) diff --git a/signer/raft_store_test.go b/signer/raft_store_test.go index 51f34ba7..1c4e0eaf 100644 --- a/signer/raft_store_test.go +++ b/signer/raft_store_test.go @@ -19,7 +19,6 @@ func Test_StoreInMemOpenSingleNode(t *testing.T) { dummyPub := cometcryptoed25519.PubKey{} - bitSize := 4096 rsaKey, err := rsa.GenerateKey(rand.Reader, bitSize) require.NoError(t, err) @@ -31,7 +30,7 @@ func Test_StoreInMemOpenSingleNode(t *testing.T) { cosigner := NewLocalCosigner( &RuntimeConfig{}, - key, + key.ID, *rsaKey, []CosignerPeer{{ ID: 1, diff --git a/signer/remote_cosigner.go b/signer/remote_cosigner.go index 5d9b288a..aaa04b05 100644 --- a/signer/remote_cosigner.go +++ b/signer/remote_cosigner.go @@ -2,9 +2,11 @@ package signer import ( "context" + "fmt" "net/url" "time" + cometcrypto "github.com/cometbft/cometbft/crypto" "github.com/strangelove-ventures/horcrux/signer/proto" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -46,6 +48,18 @@ func (cosigner *RemoteCosigner) GetAddress() string { return cosigner.address } +// GetPubKey returns public key of the validator. +// Implements Cosigner interface +func (cosigner *RemoteCosigner) GetPubKey(_ string) (cometcrypto.PubKey, error) { + return nil, fmt.Errorf("unexpected call to RemoteCosigner.GetPubKey") +} + +// VerifySignature validates a signed payload against the public key. +// Implements Cosigner interface +func (cosigner *RemoteCosigner) VerifySignature(_ string, _, _ []byte) bool { + return false +} + func (cosigner *RemoteCosigner) getGRPCClient() (proto.CosignerGRPCClient, *grpc.ClientConn, error) { var grpcAddress string url, err := url.Parse(cosigner.address) diff --git a/signer/remote_signer.go b/signer/remote_signer.go index 817e8ce7..e604d0c4 100644 --- a/signer/remote_signer.go +++ b/signer/remote_signer.go @@ -5,6 +5,7 @@ import ( "net" "time" + cometcrypto "github.com/cometbft/cometbft/crypto" cometcryptoed25519 "github.com/cometbft/cometbft/crypto/ed25519" cometcryptoencoding "github.com/cometbft/cometbft/crypto/encoding" cometlog "github.com/cometbft/cometbft/libs/log" @@ -14,13 +15,14 @@ import ( cometprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto" cometprotoprivval "github.com/cometbft/cometbft/proto/tendermint/privval" cometproto "github.com/cometbft/cometbft/proto/tendermint/types" - comet "github.com/cometbft/cometbft/types" ) // PrivValidator is a wrapper for tendermint PrivValidator, // with additional Stop method for safe shutdown. type PrivValidator interface { - comet.PrivValidator + SignVote(chainID string, vote *cometproto.Vote) error + SignProposal(chainID string, proposal *cometproto.Proposal) error + GetPubKey(chainID string) (cometcrypto.PubKey, error) Stop() } @@ -307,7 +309,7 @@ func (rs *ReconnRemoteSigner) handlePubKeyRequest(chainID string) cometprotopriv Error: nil, }} - pubKey, err := rs.privVal.GetPubKey() + pubKey, err := rs.privVal.GetPubKey(chainID) if err != nil { rs.Logger.Error( "Failed to get Pub Key", diff --git a/signer/single_signer_validator.go b/signer/single_signer_validator.go index b90031b8..b3a6ebb4 100644 --- a/signer/single_signer_validator.go +++ b/signer/single_signer_validator.go @@ -6,7 +6,6 @@ import ( "sync" cometcrypto "github.com/cometbft/cometbft/crypto" - cometjson "github.com/cometbft/cometbft/libs/json" cometprivval "github.com/cometbft/cometbft/privval" cometproto "github.com/cometbft/cometbft/proto/tendermint/types" ) @@ -18,7 +17,6 @@ var _ PrivValidator = &SingleSignerValidator{} type SingleSignerValidator struct { config *RuntimeConfig chainState sync.Map - pubKey cometcrypto.PubKey } // SingleSignerChainState holds the priv validator and associated mutex for a single chain. @@ -33,39 +31,19 @@ type SingleSignerChainState struct { // NewSingleSignerValidator constructs a validator for single-sign mode (not recommended). // NewThresholdValidator is recommended, but single-sign mode can be used for convenience. -func NewSingleSignerValidator(config *RuntimeConfig) (*SingleSignerValidator, error) { - pv := &SingleSignerValidator{ +func NewSingleSignerValidator(config *RuntimeConfig) *SingleSignerValidator { + return &SingleSignerValidator{ config: config, } - - if err := pv.loadPubKey(); err != nil { - return nil, fmt.Errorf("failed to load priv validator key: %w", err) - } - - return pv, nil } -func (pv *SingleSignerValidator) loadPubKey() error { - keyFile := pv.config.KeyFilePathSingleSigner() - - keyJSONBytes, err := os.ReadFile(keyFile) +// GetPubKey implements types.PrivValidator +func (pv *SingleSignerValidator) GetPubKey(chainID string) (cometcrypto.PubKey, error) { + chainState, err := pv.loadChainStateIfNecessary(chainID) if err != nil { - return err + return nil, err } - pvKey := cometprivval.FilePVKey{} - err = cometjson.Unmarshal(keyJSONBytes, &pvKey) - if err != nil { - return err - } - - pv.pubKey = pvKey.PrivKey.PubKey() - - return nil -} - -// GetPubKey implements types.PrivValidator -func (pv *SingleSignerValidator) GetPubKey() (cometcrypto.PubKey, error) { - return pv.pubKey, nil + return chainState.filePV.GetPubKey() } // SignVote implements types.PrivValidator @@ -96,7 +74,11 @@ func (pv *SingleSignerValidator) loadChainStateIfNecessary(chainID string) (*Sin return cachedChainState.(*SingleSignerChainState), nil } - keyFile := pv.config.KeyFilePathSingleSigner() + keyFile := pv.config.KeyFilePathSingleSigner(chainID) + if _, err := os.Stat(keyFile); err != nil { + return nil, fmt.Errorf("failed to load key file (%s) - %w", keyFile, err) + } + stateFile := pv.config.PrivValStateFile(chainID) var filePV *cometprivval.FilePV if _, err := os.Stat(stateFile); err != nil { diff --git a/signer/single_signer_validator_test.go b/signer/single_signer_validator_test.go index 97b338f4..c1aa8bc2 100644 --- a/signer/single_signer_validator_test.go +++ b/signer/single_signer_validator_test.go @@ -39,12 +39,14 @@ func TestSingleSignerValidator(t *testing.T) { }) require.NoError(t, err) - err = os.WriteFile(runtimeConfig.KeyFilePathSingleSigner(), marshaled, 0600) + err = os.WriteFile(runtimeConfig.KeyFilePathSingleSigner(testChainID), marshaled, 0600) require.NoError(t, err) - validator, err := NewSingleSignerValidator(runtimeConfig) + err = os.WriteFile(runtimeConfig.KeyFilePathSingleSigner("different"), marshaled, 0600) require.NoError(t, err) + validator := NewSingleSignerValidator(runtimeConfig) + proposal := cometproto.Proposal{ Height: 1, Round: 20, @@ -100,8 +102,7 @@ func TestSingleSignerValidator(t *testing.T) { require.NoError(t, err) // reinitialize validator to make sure new runtime will not allow double sign - validator, err = NewSingleSignerValidator(runtimeConfig) - require.NoError(t, err) + validator = NewSingleSignerValidator(runtimeConfig) err = validator.SignProposal(testChainID, &proposal) require.Error(t, err, "double sign!") diff --git a/signer/fixtures/cosigner-key.json b/signer/testdata/rsa_keys.json similarity index 97% rename from signer/fixtures/cosigner-key.json rename to signer/testdata/rsa_keys.json index af4d83f4..25864a2d 100644 --- a/signer/fixtures/cosigner-key.json +++ b/signer/testdata/rsa_keys.json @@ -1,11 +1,9 @@ { "rsa_key": "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", - "pub_key": "CiBRLhKCqDU6Wufhj0TZK6jNO8LreArc+CKHKYitdTyYUg==", "rsa_pubs": [ "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" ], - "secret_share": "q0zuUQpplAfBJxoA2e2O1H7BdBgopzhPmL8mIWP8AQw=", "id": 3 } \ No newline at end of file diff --git a/signer/testdata/testdata.go b/signer/testdata/testdata.go new file mode 100644 index 00000000..c7fa8eea --- /dev/null +++ b/signer/testdata/testdata.go @@ -0,0 +1,8 @@ +package testdata + +import ( + _ "embed" // required to embed files +) + +//go:embed rsa_keys.json +var RSAKeys []byte diff --git a/signer/threshold_signer_soft.go b/signer/threshold_signer_soft.go index 70c46ff0..cf45ea7b 100644 --- a/signer/threshold_signer_soft.go +++ b/signer/threshold_signer_soft.go @@ -21,6 +21,7 @@ import ( type ThresholdSignerSoft struct { pubKeyBytes []byte key CosignerKey + rsaKey CosignerKeyRSA // total signers total uint8 threshold uint8 @@ -32,9 +33,10 @@ type ThresholdSignerSoft struct { // NewThresholdSignerSoft constructs a ThresholdSigner // that signs using the local key share file. -func NewThresholdSignerSoft(key CosignerKey, threshold, total uint8) ThresholdSigner { +func NewThresholdSignerSoft(key CosignerKey, rsaKey CosignerKeyRSA, threshold, total uint8) ThresholdSigner { softSigner := &ThresholdSignerSoft{ key: key, + rsaKey: rsaKey, hrsMeta: make(map[HRSTKey]HrsMetadata), total: total, threshold: threshold, @@ -268,7 +270,7 @@ func (softSigner *ThresholdSignerSoft) GetEphemeralSecretPart( } digest := sha256.Sum256(jsonBytes) - signature, err := rsa.SignPSS(rand.Reader, &softSigner.key.RSAKey, crypto.SHA256, digest[:], nil) + signature, err := rsa.SignPSS(rand.Reader, &softSigner.rsaKey.RSAKey, crypto.SHA256, digest[:], nil) if err != nil { return res, err @@ -344,7 +346,7 @@ func (softSigner *ThresholdSignerSoft) SetEphemeralSecretPart( } // decrypt share - sharePart, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, &softSigner.key.RSAKey, req.EncryptedSharePart, nil) + sharePart, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, &softSigner.rsaKey.RSAKey, req.EncryptedSharePart, nil) if err != nil { return err } diff --git a/signer/threshold_validator.go b/signer/threshold_validator.go index d4291a3c..a5796a76 100644 --- a/signer/threshold_validator.go +++ b/signer/threshold_validator.go @@ -17,14 +17,14 @@ import ( tsed25519 "gitlab.com/unit410/threshold-ed25519/pkg" ) +var _ PrivValidator = &ThresholdValidator{} + type ThresholdValidator struct { config *RuntimeConfig threshold int - pubKey crypto.PubKey - - chainState map[string]ChainSignState + chainState sync.Map // our own cosigner cosigner Cosigner @@ -54,21 +54,18 @@ type ChainSignState struct { func NewThresholdValidator( logger log.Logger, config *RuntimeConfig, - pubKey crypto.PubKey, threshold int, cosigner Cosigner, peers []Cosigner, raftStore *RaftStore, ) *ThresholdValidator { return &ThresholdValidator{ - logger: logger, - config: config, - pubKey: pubKey, - threshold: threshold, - cosigner: cosigner, - peers: peers, - raftStore: raftStore, - chainState: make(map[string]ChainSignState), + logger: logger, + config: config, + threshold: threshold, + cosigner: cosigner, + peers: peers, + raftStore: raftStore, } } @@ -77,9 +74,25 @@ func NewThresholdValidator( // state updates. The disk write is scheduled in a separate goroutine which will perform an atomic write. // pendingDiskWG is used upon termination in pendingDiskWG to ensure all writes have completed. func (pv *ThresholdValidator) SaveLastSignedState(chainID string, signState SignStateConsensus) error { - pv.chainState[chainID].lastSignStateMutex.Lock() - defer pv.chainState[chainID].lastSignStateMutex.Unlock() - return pv.chainState[chainID].lastSignState.Save(signState, &pv.pendingDiskWG) + css := pv.mustLoadChainState(chainID) + + css.lastSignStateMutex.Lock() + defer css.lastSignStateMutex.Unlock() + return css.lastSignState.Save(signState, &pv.pendingDiskWG) +} + +func (pv *ThresholdValidator) mustLoadChainState(chainID string) ChainSignState { + cs, ok := pv.chainState.Load(chainID) + if !ok { + panic(fmt.Errorf("failed to load chain state for %s", chainID)) + } + + css, ok := cs.(ChainSignState) + if !ok { + panic(fmt.Errorf("expected: (ChainSignState), actual: (%T)", cs)) + } + + return css } // SaveLastSignedStateInitiated updates the high watermark height/round/step (HRS) for an initiated @@ -87,9 +100,11 @@ func (pv *ThresholdValidator) SaveLastSignedState(chainID string, signState Sign // state updates. The disk write is scheduled in a separate goroutine which will perform an atomic write. // pendingDiskWG is used upon termination in pendingDiskWG to ensure all writes have completed. func (pv *ThresholdValidator) SaveLastSignedStateInitiated(chainID string, signState SignStateConsensus) error { - pv.chainState[chainID].lastSignStateInitiatedMutex.Lock() - defer pv.chainState[chainID].lastSignStateInitiatedMutex.Unlock() - return pv.chainState[chainID].lastSignStateInitiated.Save(signState, &pv.pendingDiskWG) + css := pv.mustLoadChainState(chainID) + + css.lastSignStateInitiatedMutex.Lock() + defer css.lastSignStateInitiatedMutex.Unlock() + return css.lastSignStateInitiated.Save(signState, &pv.pendingDiskWG) } // Stop safely shuts down the ThresholdValidator. @@ -110,8 +125,8 @@ func (pv *ThresholdValidator) waitForSignStatesToFlushToDisk() { // GetPubKey returns the public key of the validator. // Implements PrivValidator. -func (pv *ThresholdValidator) GetPubKey() (crypto.PubKey, error) { - return pv.pubKey, nil +func (pv *ThresholdValidator) GetPubKey(chainID string) (crypto.PubKey, error) { + return pv.cosigner.GetPubKey(chainID) } // SignVote signs a canonical representation of the vote, along with the @@ -124,6 +139,7 @@ func (pv *ThresholdValidator) SignVote(chainID string, vote *cometproto.Vote) er Timestamp: vote.Timestamp, SignBytes: comet.VoteSignBytes(chainID, vote), } + sig, stamp, err := pv.SignBlock(chainID, block) vote.Signature = sig @@ -142,6 +158,7 @@ func (pv *ThresholdValidator) SignProposal(chainID string, proposal *cometproto. Timestamp: proposal.Timestamp, SignBytes: comet.ProposalSignBytes(chainID, proposal), } + sig, stamp, err := pv.SignBlock(chainID, block) proposal.Signature = sig @@ -175,7 +192,9 @@ type BeyondBlockError struct { func (e *BeyondBlockError) Error() string { return e.msg } func (pv *ThresholdValidator) newBeyondBlockError(chainID string, hrs HRSKey) *BeyondBlockError { - lss := pv.chainState[chainID].lastSignStateInitiated + css := pv.mustLoadChainState(chainID) + + lss := css.lastSignStateInitiated return &BeyondBlockError{ msg: fmt.Sprintf("[%s] Progress already started on block %d.%d.%d, skipping %d.%d.%d", chainID, @@ -319,7 +338,7 @@ func waitUntilCompleteOrTimeout(wg *sync.WaitGroup, timeout time.Duration) bool } func (pv *ThresholdValidator) LoadSignStateIfNecessary(chainID string) error { - if _, ok := pv.chainState[chainID]; ok { + if _, ok := pv.chainState.Load(chainID); ok { return nil } @@ -328,13 +347,13 @@ func (pv *ThresholdValidator) LoadSignStateIfNecessary(chainID string) error { return err } - pv.chainState[chainID] = ChainSignState{ + pv.chainState.Store(chainID, ChainSignState{ lastSignState: signState, lastSignStateInitiated: signState.FreshCache(), lastSignStateMutex: &sync.Mutex{}, lastSignStateInitiatedMutex: &sync.Mutex{}, - } + }) switch cosigner := pv.cosigner.(type) { case *LocalCosigner: @@ -351,9 +370,12 @@ func (pv *ThresholdValidator) getExistingBlockSignature(chainID string, block *B round, step, } - latestBlock, existingSignature := pv.chainState[chainID].lastSignState.GetFromCache( + + css := pv.mustLoadChainState(chainID) + + latestBlock, existingSignature := css.lastSignState.GetFromCache( hrs, - pv.chainState[chainID].lastSignStateMutex, + css.lastSignStateMutex, ) if existingSignature != nil { // If a proposal has already been signed for this HRS, return that @@ -573,7 +595,7 @@ func (pv *ThresholdValidator) SignBlock(chainID string, block *Block) ([]byte, t signature = append(signature, combinedSig...) // verify the combined signature before saving to watermark - if !pv.pubKey.VerifySignature(signBytes, signature) { + if !pv.cosigner.VerifySignature(chainID, signBytes, signature) { totalInvalidSignature.Inc() return nil, stamp, errors.New("combined signature is not valid") } @@ -588,10 +610,13 @@ func (pv *ThresholdValidator) SignBlock(chainID string, block *Block) ([]byte, t SignBytes: signBytes, }, } + + css := pv.mustLoadChainState(chainID) + // Err will be present if newLss is not above high watermark - pv.chainState[chainID].lastSignStateMutex.Lock() - err = pv.chainState[chainID].lastSignState.Save(newLss.SignStateConsensus, &pv.pendingDiskWG) - pv.chainState[chainID].lastSignStateMutex.Unlock() + css.lastSignStateMutex.Lock() + err = css.lastSignState.Save(newLss.SignStateConsensus, &pv.pendingDiskWG) + css.lastSignStateMutex.Unlock() if err != nil { if _, isSameHRSError := err.(*SameHRSError); !isSameHRSError { return nil, stamp, err diff --git a/signer/threshold_validator_test.go b/signer/threshold_validator_test.go index 33787ff5..5cc9bc7a 100644 --- a/signer/threshold_validator_test.go +++ b/signer/threshold_validator_test.go @@ -4,12 +4,14 @@ import ( "bytes" "crypto/rand" "crypto/rsa" + "fmt" "path/filepath" "time" "os" "testing" + cometcrypto "github.com/cometbft/cometbft/crypto" cometcryptoed25519 "github.com/cometbft/cometbft/crypto/ed25519" "github.com/cometbft/cometbft/crypto/tmhash" cometlog "github.com/cometbft/cometbft/libs/log" @@ -20,6 +22,22 @@ import ( tsed25519 "gitlab.com/unit410/threshold-ed25519/pkg" ) +func TestThresholdValidator2of2(t *testing.T) { + testThresholdValidator(t, 2, 2) +} + +func TestThresholdValidator3of3(t *testing.T) { + testThresholdValidator(t, 3, 3) +} + +func TestThresholdValidator2of3(t *testing.T) { + testThresholdValidator(t, 2, 3) +} + +func TestThresholdValidator3of5(t *testing.T) { + testThresholdValidator(t, 3, 5) +} + func getMockRaftStore(cosigner Cosigner, tmpDir string) *RaftStore { return &RaftStore{ NodeID: "1", @@ -33,322 +51,98 @@ func getMockRaftStore(cosigner Cosigner, tmpDir string) *RaftStore { } } -func TestThresholdValidator2of2(t *testing.T) { - total := uint8(2) - threshold := uint8(2) - - tmpDir := t.TempDir() - stateDir := filepath.Join(tmpDir, "state") - - err := os.MkdirAll(stateDir, 0777) - require.NoError(t, err) - - runtimeConfig := &RuntimeConfig{ - HomeDir: tmpDir, - StateDir: filepath.Join(tmpDir, "state"), +func loadKeyForLocalCosigner( + cosigner *LocalCosigner, + pubKey cometcrypto.PubKey, + chainID string, + secretShare []byte, +) error { + key := CosignerKey{ + PubKey: pubKey, + ShareKey: secretShare, + ID: cosigner.GetID(), } - bitSize := 4096 - rsaKey1, err := rsa.GenerateKey(rand.Reader, bitSize) - require.NoError(t, err) - - rsaKey2, err := rsa.GenerateKey(rand.Reader, bitSize) - require.NoError(t, err) - - peers := []CosignerPeer{{ - ID: 1, - PublicKey: rsaKey1.PublicKey, - }, { - ID: 2, - PublicKey: rsaKey2.PublicKey, - }} - - privateKey := cometcryptoed25519.GenPrivKey() - - privKeyBytes := privateKey[:] - secretShares := tsed25519.DealShares(tsed25519.ExpandSecret(privKeyBytes[:32]), threshold, total) - - key1 := CosignerKey{ - PubKey: privateKey.PubKey(), - ShareKey: secretShares[0], - ID: 1, - } - - key2 := CosignerKey{ - PubKey: privateKey.PubKey(), - ShareKey: secretShares[1], - ID: 2, - } - - var cosigner1, cosigner2 Cosigner - - cosigner1 = NewLocalCosigner( - runtimeConfig, - key1, *rsaKey1, - peers, "", total, threshold, - ) - cosigner2 = NewLocalCosigner( - runtimeConfig, - key2, *rsaKey2, - peers, "", total, threshold, - ) - - require.Equal(t, cosigner1.GetID(), 1) - require.Equal(t, cosigner2.GetID(), 2) - - thresholdPeers := []Cosigner{cosigner2} - - raftStore := getMockRaftStore(cosigner1, tmpDir) - - validator := NewThresholdValidator( - cometlog.NewTMLogger(cometlog.NewSyncWriter(os.Stdout)).With("module", "validator"), - runtimeConfig, - privateKey.PubKey(), - int(threshold), - cosigner1, - thresholdPeers, - raftStore, - ) - defer validator.Stop() - - raftStore.SetThresholdValidator(validator) - - _, err = raftStore.Open() - require.NoError(t, err) - - time.Sleep(3 * time.Second) // Ensure there is a leader - - proposal := cometproto.Proposal{ - Height: 1, - Round: 0, - Type: cometproto.ProposalType, + keyBz, err := key.MarshalJSON() + if err != nil { + return err } - signBytes := comet.ProposalSignBytes(testChainID, &proposal) - - err = validator.SignProposal(testChainID, &proposal) - require.NoError(t, err) - - require.True(t, privateKey.PubKey().VerifySignature(signBytes, proposal.Signature)) + return os.WriteFile(cosigner.config.KeyFilePathCosigner(chainID), keyBz, 0600) } -func TestThresholdValidator3of3(t *testing.T) { - total := uint8(3) - threshold := uint8(3) +func testThresholdValidator(t *testing.T, threshold, total uint8) { + rsaKeys := make([]*rsa.PrivateKey, total) + peers := make([]CosignerPeer, total) + cosigners := make([]*LocalCosigner, total) - tmpDir := t.TempDir() - stateDir := filepath.Join(tmpDir, "state") + for i := uint8(0); i < total; i++ { + rsaKey, err := rsa.GenerateKey(rand.Reader, bitSize) + require.NoError(t, err) - err := os.MkdirAll(stateDir, 0777) - require.NoError(t, err) + rsaKeys[i] = rsaKey - runtimeConfig := &RuntimeConfig{ - HomeDir: tmpDir, - StateDir: filepath.Join(tmpDir, "state"), + peers[i] = CosignerPeer{ + ID: int(i) + 1, + PublicKey: rsaKey.PublicKey, + } } - bitSize := 4096 - rsaKey1, err := rsa.GenerateKey(rand.Reader, bitSize) - require.NoError(t, err) - - rsaKey2, err := rsa.GenerateKey(rand.Reader, bitSize) - require.NoError(t, err) - - rsaKey3, err := rsa.GenerateKey(rand.Reader, bitSize) - require.NoError(t, err) - - peers := []CosignerPeer{{ - ID: 1, - PublicKey: rsaKey1.PublicKey, - }, { - ID: 2, - PublicKey: rsaKey2.PublicKey, - }, { - ID: 3, - PublicKey: rsaKey3.PublicKey, - }} - privateKey := cometcryptoed25519.GenPrivKey() - privKeyBytes := privateKey[:] secretShares := tsed25519.DealShares(tsed25519.ExpandSecret(privKeyBytes[:32]), threshold, total) - key1 := CosignerKey{ - PubKey: privateKey.PubKey(), - ShareKey: secretShares[0], - ID: 1, - } - - key2 := CosignerKey{ - PubKey: privateKey.PubKey(), - ShareKey: secretShares[1], - ID: 2, - } - - key3 := CosignerKey{ - PubKey: privateKey.PubKey(), - ShareKey: secretShares[2], - ID: 3, - } - - var cosigner1, cosigner2, cosigner3 Cosigner - - cosigner1 = NewLocalCosigner( - runtimeConfig, - key1, *rsaKey1, - peers, "", total, threshold, - ) - cosigner2 = NewLocalCosigner( - runtimeConfig, - key2, *rsaKey2, - peers, "", total, threshold, - ) - cosigner3 = NewLocalCosigner( - runtimeConfig, - key3, *rsaKey3, - peers, "", total, threshold, - ) - - require.Equal(t, cosigner1.GetID(), 1) - require.Equal(t, cosigner2.GetID(), 2) - require.Equal(t, cosigner3.GetID(), 3) - - thresholdPeers := []Cosigner{cosigner2, cosigner3} - - raftStore := getMockRaftStore(cosigner1, tmpDir) - - validator := NewThresholdValidator( - cometlog.NewTMLogger(cometlog.NewSyncWriter(os.Stdout)).With("module", "validator"), - runtimeConfig, - privateKey.PubKey(), - int(threshold), - cosigner1, - thresholdPeers, - raftStore, - ) - defer validator.Stop() - - raftStore.SetThresholdValidator(validator) - - _, err = raftStore.Open() - require.NoError(t, err) - - time.Sleep(3 * time.Second) // Ensure there is a leader - - proposal := cometproto.Proposal{ - Height: 1, - Round: 0, - Type: cometproto.ProposalType, - } - - signBytes := comet.ProposalSignBytes(testChainID, &proposal) - - err = validator.SignProposal(testChainID, &proposal) - if err != nil { - t.Logf("%v", err) - } - require.NoError(t, err) - - require.True(t, privateKey.PubKey().VerifySignature(signBytes, proposal.Signature)) -} - -func TestThresholdValidator2of3(t *testing.T) { - total := uint8(3) - threshold := uint8(2) - tmpDir := t.TempDir() - stateDir := filepath.Join(tmpDir, "state") - err := os.MkdirAll(stateDir, 0777) - require.NoError(t, err) + for i, peer := range peers { + cosignerDir := filepath.Join(tmpDir, fmt.Sprintf("cosigner_%d", peer.ID)) + err := os.MkdirAll(cosignerDir, 0777) + require.NoError(t, err) - runtimeConfig := &RuntimeConfig{ - HomeDir: tmpDir, - StateDir: filepath.Join(tmpDir, "state"), - } + cosignerConfig := &RuntimeConfig{ + HomeDir: cosignerDir, + StateDir: cosignerDir, + } - bitSize := 4096 - rsaKey1, err := rsa.GenerateKey(rand.Reader, bitSize) - require.NoError(t, err) + cosigner := NewLocalCosigner( + cosignerConfig, + peer.ID, *rsaKeys[i], + peers, "", total, threshold, + ) + require.NoError(t, err) - rsaKey2, err := rsa.GenerateKey(rand.Reader, bitSize) - require.NoError(t, err) - - rsaKey3, err := rsa.GenerateKey(rand.Reader, bitSize) - require.NoError(t, err) + cosigners[i] = cosigner - peers := []CosignerPeer{{ - ID: 1, - PublicKey: rsaKey1.PublicKey, - }, { - ID: 2, - PublicKey: rsaKey2.PublicKey, - }, { - ID: 3, - PublicKey: rsaKey3.PublicKey, - }} - - privateKey := cometcryptoed25519.GenPrivKey() + err = loadKeyForLocalCosigner(cosigner, privateKey.PubKey(), testChainID, secretShares[i]) + require.NoError(t, err) - privKeyBytes := privateKey[:] - secretShares := tsed25519.DealShares(tsed25519.ExpandSecret(privKeyBytes[:32]), threshold, total) - - key1 := CosignerKey{ - PubKey: privateKey.PubKey(), - ShareKey: secretShares[0], - ID: 1, + err = loadKeyForLocalCosigner(cosigner, privateKey.PubKey(), testChainID2, secretShares[i]) + require.NoError(t, err) } - key2 := CosignerKey{ - PubKey: privateKey.PubKey(), - ShareKey: secretShares[1], - ID: 2, - } + thresholdPeers := make([]Cosigner, 0, threshold-1) - key3 := CosignerKey{ - PubKey: privateKey.PubKey(), - ShareKey: secretShares[2], - ID: 3, - } - - var cosigner1, cosigner2, cosigner3 Cosigner - - cosigner1 = NewLocalCosigner( - runtimeConfig, - key1, *rsaKey1, - peers, "", total, threshold, - ) - cosigner2 = NewLocalCosigner( - runtimeConfig, - key2, *rsaKey2, - peers, "", total, threshold, - ) - cosigner3 = NewLocalCosigner( - runtimeConfig, - key3, *rsaKey3, - peers, "", total, threshold, - ) + for i, cosigner := range cosigners { + require.Equal(t, i+1, cosigner.GetID()) - require.Equal(t, cosigner1.GetID(), 1) - require.Equal(t, cosigner2.GetID(), 2) - require.Equal(t, cosigner3.GetID(), 3) - - thresholdPeers := []Cosigner{cosigner2, cosigner3} + if i != 0 && len(thresholdPeers) != int(threshold)-1 { + thresholdPeers = append(thresholdPeers, cosigner) + } + } - raftStore := getMockRaftStore(cosigner1, tmpDir) + raftStore := getMockRaftStore(cosigners[0], tmpDir) validator := NewThresholdValidator( cometlog.NewTMLogger(cometlog.NewSyncWriter(os.Stdout)).With("module", "validator"), - runtimeConfig, - privateKey.PubKey(), + cosigners[0].config, int(threshold), - cosigner1, + cosigners[0], thresholdPeers, raftStore, ) defer validator.Stop() - err = validator.LoadSignStateIfNecessary(testChainID) + err := validator.LoadSignStateIfNecessary(testChainID) require.NoError(t, err) raftStore.SetThresholdValidator(validator) @@ -416,16 +210,15 @@ func TestThresholdValidator2of3(t *testing.T) { require.Error(t, err, "double sign!") // lower LSS should sign for different chain ID - err = validator.SignProposal("different", &proposal) + err = validator.SignProposal(testChainID2, &proposal) require.NoError(t, err) // reinitialize validator to make sure new runtime will not allow double sign newValidator := NewThresholdValidator( cometlog.NewTMLogger(cometlog.NewSyncWriter(os.Stdout)).With("module", "validator"), - runtimeConfig, - privateKey.PubKey(), + cosigners[0].config, int(threshold), - cosigner1, + cosigners[0], thresholdPeers, raftStore, ) diff --git a/test/horcrux_test.go b/test/horcrux_test.go index 88fadc25..211df1c7 100644 --- a/test/horcrux_test.go +++ b/test/horcrux_test.go @@ -17,7 +17,7 @@ func testChainSingleNodeAndHorcrux( t *testing.T, totalValidators int, // total number of validators on chain (one horcrux + single node for the rest) totalSigners int, // total number of signers for the single horcrux validator - threshold int, // key shard threshold, and therefore how many horcrux signers must participate to sign a block + threshold uint8, // key shard threshold, and therefore how many horcrux signers must participate to sign a block totalSentries int, // number of sentry nodes for the single horcrux validator sentriesPerSigner int, // how many sentries should each horcrux signer connect to (min: 1, max: totalSentries) ) { diff --git a/test/setup.go b/test/setup.go index bfbae41e..520ebb31 100644 --- a/test/setup.go +++ b/test/setup.go @@ -74,7 +74,7 @@ func Genesis( bech32Prefix = chain.Bech32Prefix } - pubKey, err := signer.PubKey(bech32Prefix, v.PubKey) + pubKey, err := signer.PubKey(bech32Prefix, v.PubKeys[chain.ChainID]) if err != nil { return err } diff --git a/test/signer.go b/test/signer.go index cf4d755c..c723b240 100644 --- a/test/signer.go +++ b/test/signer.go @@ -94,7 +94,14 @@ func StartSingleSignerContainers( return err } - err = os.WriteFile(filepath.Join(testSigners[0].Dir(), "priv_validator_key.json"), pvFile, 0600) + err = os.WriteFile( + filepath.Join( + testSigners[0].Dir(), + fmt.Sprintf("%s_priv_validator_key.json", validator.ChainID), + ), + pvFile, + 0600, + ) if err != nil { return err } @@ -128,7 +135,7 @@ func StartSingleSignerContainers( func StartCosignerContainers( signers Signers, sentries Nodes, - threshold, + threshold uint8, sentriesPerSigner int, ) error { eg := new(errgroup.Group) @@ -382,7 +389,7 @@ func (ts *Signer) InitSingleSignerConfig(ctx context.Context, listenNodes Nodes) // InitCosignerConfig creates and runs a container to init a signer nodes config files // blocks until the container exits func (ts *Signer) InitCosignerConfig( - ctx context.Context, listenNodes Nodes, peers Signers, skip, threshold int) error { + ctx context.Context, listenNodes Nodes, peers Signers, skip int, threshold uint8) error { return ts.ExecHorcruxCmd(ctx, "config", "init", listenNodes.ListenAddrs(), "--cosigner", diff --git a/test/validator.go b/test/validator.go index 5e04a804..968f66b3 100644 --- a/test/validator.go +++ b/test/validator.go @@ -13,14 +13,13 @@ import ( ) type Validator struct { - Index int - Sentries Nodes - Signers Signers - tl Logger - Home string - PubKey cometcrypto.PubKey - PrivKeyShares []signer.CosignerKey - Threshold int + Index int + Sentries Nodes + Signers Signers + tl Logger + Home string + PubKeys map[string]cometcrypto.PubKey + Threshold uint8 } func NewHorcruxValidator( @@ -30,10 +29,11 @@ func NewHorcruxValidator( home string, index int, numSigners int, - threshold int, + threshold uint8, chains ...*ChainType, ) (*Validator, error) { var sentries Nodes + chainIDs := make([]string, 0, len(chains)) for _, chain := range chains { sentries = append(sentries, MakeNodes( @@ -47,6 +47,7 @@ func NewHorcruxValidator( tl, )..., ) + chainIDs = append(chainIDs, chain.ChainID) } testValidator := &Validator{ Index: index, @@ -55,8 +56,9 @@ func NewHorcruxValidator( tl: tl, Home: home, Threshold: threshold, + PubKeys: make(map[string]cometcrypto.PubKey), } - if err := testValidator.genPrivKeyAndShares(); err != nil { + if err := testValidator.genPrivKeyAndShares(nil, chainIDs...); err != nil { return nil, err } return testValidator, nil @@ -71,7 +73,7 @@ func NewHorcruxValidatorWithPrivValKey( index int, numSentries int, numSigners int, - threshold int, + threshold uint8, chainType *ChainType, privValKey privval.FilePVKey, ) (*Validator, error) { @@ -82,8 +84,9 @@ func NewHorcruxValidatorWithPrivValKey( tl: tl, Home: home, Threshold: threshold, + PubKeys: make(map[string]cometcrypto.PubKey), } - if err := testValidator.generateShares(privValKey); err != nil { + if err := testValidator.genPrivKeyAndShares(&privValKey, chainID); err != nil { return nil, err } return testValidator, nil @@ -99,31 +102,71 @@ func (tv *Validator) Dir() string { return filepath.Join(tv.Home, tv.Name()) } -// Generate Ed25519 Private Key -func (tv *Validator) genPrivKeyAndShares() error { - privKey := cometcryptoed25519.GenPrivKey() - pubKey := privKey.PubKey() - filePVKey := privval.FilePVKey{ - Address: pubKey.Address(), - PubKey: pubKey, - PrivKey: privKey, - } - return tv.generateShares(filePVKey) -} - -func (tv *Validator) generateShares(filePVKey privval.FilePVKey) error { - tv.PubKey = filePVKey.PubKey - shares, err := signer.CreateCosignerShares(filePVKey, int64(tv.Threshold), int64(len(tv.Signers))) +func (tv *Validator) genRSAShares() error { + rsaShares, err := signer.CreateCosignerSharesRSA(len(tv.Signers)) if err != nil { return err } - tv.PrivKeyShares = shares + for i, s := range tv.Signers { - tv.tl.Logf("{%s} -> Writing Key Share To File... ", s.Name()) + tv.tl.Logf("{%s} -> Writing RSA Key Share To File... ", s.Name()) if err := os.MkdirAll(s.Dir(), 0700); err != nil { return err } - privateFilename := filepath.Join(s.Dir(), "share.json") + + cosignerFilename := filepath.Join(s.Dir(), "rsa_keys.json") + if err := signer.WriteCosignerShareRSAFile(rsaShares[i], cosignerFilename); err != nil { + return err + } + } + + return nil +} + +// genPrivKeyAndShares generates cosigner RSA shares. +// If existingKey is nil, generates Ed25519 key shares, otherwise shards existing key. +func (tv *Validator) genPrivKeyAndShares(existingKey *privval.FilePVKey, chainIDs ...string) error { + if err := tv.genRSAShares(); err != nil { + return err + } + + for _, chainID := range chainIDs { + if err := tv.genEd25519Shares(existingKey, chainID); err != nil { + return err + } + } + + return nil +} + +func (tv *Validator) genEd25519Shares( + existingKey *privval.FilePVKey, + chainID string, +) error { + var key privval.FilePVKey + if existingKey != nil { + key = *existingKey + } else { + privKey := cometcryptoed25519.GenPrivKey() + pubKey := privKey.PubKey() + key = privval.FilePVKey{ + Address: pubKey.Address(), + PubKey: pubKey, + PrivKey: privKey, + } + } + + tv.PubKeys[chainID] = key.PubKey + + shares, err := signer.CreateCosignerShares(key, tv.Threshold, uint8(len(tv.Signers))) + if err != nil { + return err + } + + for i, s := range tv.Signers { + tv.tl.Logf("{%s} -> Writing Ed25519 Key Share To File... ", s.Name()) + + privateFilename := filepath.Join(s.Dir(), fmt.Sprintf("%s_share.json", chainID)) if err := signer.WriteCosignerShareFile(shares[i], privateFilename); err != nil { return err } @@ -141,7 +184,7 @@ func (tv *Validator) StartHorcruxCluster( func (tv *Validator) WaitForConsecutiveBlocks(chainID string, blocks int64) error { for _, n := range tv.Sentries { if n.ChainID == chainID { - return n.WaitForConsecutiveBlocks(blocks, tv.PubKey.Address()) + return n.WaitForConsecutiveBlocks(blocks, tv.PubKeys[chainID].Address()) } } return fmt.Errorf("no sentry found with chain id: %s", chainID) @@ -150,7 +193,7 @@ func (tv *Validator) WaitForConsecutiveBlocks(chainID string, blocks int64) erro func (tv *Validator) EnsureNotSlashed(chainID string) error { for _, n := range tv.Sentries { if n.ChainID == chainID { - return n.EnsureNotSlashed(tv.PubKey.Address()) + return n.EnsureNotSlashed(tv.PubKeys[chainID].Address()) } } return fmt.Errorf("no sentry found with chain id: %s", chainID)