diff --git a/.gitignore b/.gitignore index eea6fcf2..56fdbca0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,7 @@ build data private_share_* +*_shard.json +rsa_keys.json priv_validator_key.json dist/ \ No newline at end of file diff --git a/Makefile b/Makefile index 77d210d9..5391dc07 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ build-linux: @GOOS=linux GOARCH=amd64 go build --mod readonly $(BUILD_FLAGS) -o ./build/horcrux ./cmd/horcrux test: - @go test -race -timeout 20m -mod readonly -v ./... + @go test -race -timeout 30m -mod readonly -v ./... test-short: @go test -mod readonly -run TestDownedSigners2of3 -v ./... diff --git a/client/address.go b/client/address.go index 94315824..3a611021 100644 --- a/client/address.go +++ b/client/address.go @@ -9,7 +9,7 @@ import ( func SanitizeAddress(address string) (string, error) { u, err := url.Parse(address) if err != nil { - return "", fmt.Errorf("error parsing peer URL: %w", err) + return "", fmt.Errorf("error parsing URL: %w", err) } return u.Host, nil diff --git a/cmd/horcrux/cmd/address.go b/cmd/horcrux/cmd/address.go new file mode 100644 index 00000000..8a633d7f --- /dev/null +++ b/cmd/horcrux/cmd/address.go @@ -0,0 +1,109 @@ +package cmd + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strings" + + "github.com/cometbft/cometbft/crypto" + cometprivval "github.com/cometbft/cometbft/privval" + "github.com/cosmos/cosmos-sdk/types/bech32" + "github.com/spf13/cobra" + "github.com/strangelove-ventures/horcrux/signer" +) + +type AddressCmdOutput struct { + HexAddress string + PubKey string + ValConsAddress string + ValConsPubAddress string +} + +func addressCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "address chain-id [bech32]", + Short: "Get public key hex address and valcons address", + Example: `horcrux cosigner address cosmos`, + SilenceUsage: true, + Args: cobra.RangeArgs(1, 2), + RunE: func(cmd *cobra.Command, args []string) error { + + var pubKey crypto.PubKey + + switch config.Config.SignMode { + case signer.SignModeThreshold: + err := config.Config.ValidateThresholdModeConfig() + if err != nil { + return err + } + + keyFile, err := config.KeyFileExistsCosigner(args[0]) + if err != nil { + return err + } + + key, err := signer.LoadCosignerEd25519Key(keyFile) + if err != nil { + return fmt.Errorf("error reading cosigner key: %s", err) + } + + pubKey = key.PubKey + case signer.SignModeSingle: + err := config.Config.ValidateSingleSignerConfig() + if err != nil { + return err + } + keyFile, err := config.KeyFileExistsSingleSigner(args[0]) + if err != nil { + return err + } + + filePV := cometprivval.LoadFilePVEmptyState(keyFile, "") + pubKey = filePV.Key.PubKey + default: + panic(fmt.Errorf("unexpected sign mode: %s", config.Config.SignMode)) + } + + pubKeyAddress := pubKey.Address() + + pubKeyJSON, err := signer.PubKey("", pubKey) + if err != nil { + return err + } + + output := AddressCmdOutput{ + HexAddress: strings.ToUpper(hex.EncodeToString(pubKeyAddress)), + PubKey: pubKeyJSON, + } + + 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[1], pubKey) + if err != nil { + return err + } + output.ValConsPubAddress = pubKeyBech32 + } else { + bech32Hint := "Pass bech32 base prefix as argument to generate (e.g. cosmos)" + output.ValConsAddress = bech32Hint + output.ValConsPubAddress = bech32Hint + } + + jsonOut, err := json.Marshal(output) + if err != nil { + return err + } + + fmt.Println(string(jsonOut)) + + return nil + }, + } + + return cmd +} diff --git a/cmd/horcrux/cmd/config.go b/cmd/horcrux/cmd/config.go index b997dc79..a5d8b00c 100644 --- a/cmd/horcrux/cmd/config.go +++ b/cmd/horcrux/cmd/config.go @@ -1,16 +1,24 @@ package cmd import ( - "errors" "fmt" - "net" - "net/url" "os" "github.com/spf13/cobra" "github.com/strangelove-ventures/horcrux/signer" ) +const ( + flagSignMode = "mode" + flagNode = "node" + flagCosigner = "cosigner" + flagDebugAddr = "debug-addr" + flagKeyDir = "key-dir" + flagRaftTimeout = "raft-timeout" + flagGRPCTimeout = "grpc-timeout" + flagOverwrite = "overwrite" +) + func configCmd() *cobra.Command { cmd := &cobra.Command{ Use: "config", @@ -31,18 +39,18 @@ func initCmd() *cobra.Command { Long: "initialize configuration file, use flags for cosigner configuration.\n\n" + "[chain-nodes] is a comma separated array of chain node addresses i.e.\n" + "tcp://chain-node-1:1234,tcp://chain-node-2:1234", - Args: cobra.RangeArgs(0, 1), + Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) (err error) { - var cn signer.ChainNodes - if len(args) == 1 { - cn, err = signer.ChainNodesFromArg(args[0]) - if err != nil { - return err - } + cmdFlags := cmd.Flags() + + nodes, _ := cmdFlags.GetStringSlice(flagNode) + + cn, err := signer.ChainNodesFromFlag(nodes) + if err != nil { + return err } - cmdFlags := cmd.Flags() - overwrite, _ := cmdFlags.GetBool("overwrite") + overwrite, _ := cmdFlags.GetBool(flagOverwrite) if _, err := os.Stat(config.ConfigFile); !os.IsNotExist(err) && !overwrite { return fmt.Errorf("%s already exists. Provide the -o flag to overwrite the existing config", @@ -51,57 +59,43 @@ func initCmd() *cobra.Command { var cfg signer.Config - cs, _ := cmdFlags.GetBool("cosigner") - keyDirFlag, _ := cmdFlags.GetString("key-dir") + signMode, _ := cmdFlags.GetString(flagSignMode) + keyDirFlag, _ := cmdFlags.GetString(flagKeyDir) var keyDir *string if keyDirFlag != "" { keyDir = &keyDirFlag } debugAddr, _ := cmdFlags.GetString("debug-addr") - if cs { - // Cosigner Config - p, _ := cmdFlags.GetStringSlice("peers") - threshold, _ := cmdFlags.GetInt("threshold") - timeout, _ := cmdFlags.GetString("timeout") - peers, err := signer.PeersFromFlag(p) + if signMode == string(signer.SignModeThreshold) { + // Threshold Mode Config + cosignersFlag, _ := cmdFlags.GetStringSlice(flagCosigner) + threshold, _ := cmdFlags.GetInt(flagThreshold) + raftTimeout, _ := cmdFlags.GetString(flagRaftTimeout) + grpcTimeout, _ := cmdFlags.GetString(flagGRPCTimeout) + cosigners, err := signer.CosignersFromFlag(cosignersFlag) if err != nil { return err } - listen, _ := cmdFlags.GetString("listen") - if listen == "" { - return errors.New("must input at least one node") - } - url, err := url.Parse(listen) - if err != nil { - return fmt.Errorf("error parsing listen address: %s, %v", listen, err) - } - host, _, err := net.SplitHostPort(url.Host) - if err != nil { - return err - } - if host == "0.0.0.0" { - return errors.New("host cannot be 0.0.0.0, must be reachable from other peers") - } - cfg = signer.Config{ + SignMode: signer.SignModeThreshold, PrivValKeyDir: keyDir, - CosignerConfig: &signer.CosignerConfig{ - Threshold: threshold, - Shares: len(peers) + 1, - P2PListen: listen, - Peers: peers, - Timeout: timeout, + ThresholdModeConfig: &signer.ThresholdModeConfig{ + Threshold: threshold, + Cosigners: cosigners, + GRPCTimeout: grpcTimeout, + RaftTimeout: raftTimeout, }, ChainNodes: cn, DebugAddr: debugAddr, } - if err = cfg.ValidateCosignerConfig(); err != nil { + if err = cfg.ValidateThresholdModeConfig(); err != nil { return err } } else { // Single Signer Config cfg = signer.Config{ + SignMode: signer.SignModeSingle, PrivValKeyDir: keyDir, ChainNodes: cn, DebugAddr: debugAddr, @@ -128,16 +122,23 @@ func initCmd() *cobra.Command { return nil }, } - cmd.Flags().BoolP("cosigner", "c", false, "set to initialize a cosigner node, requires --peers and --threshold") - cmd.Flags().StringSliceP("peers", "p", []string{}, - "cosigner peer addresses in format tcp://{addr}:{port}|{share-id} \n"+ - "(i.e. \"tcp://node-1:2222|2,tcp://node-2:2222|3\")") - 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("key-dir", "k", "", "priv val key directory") - cmd.Flags().String("timeout", "1500ms", "configure cosigner rpc server timeout value, \n"+ + cmd.Flags().StringP(flagSignMode, "m", string(signer.SignModeThreshold), + `sign mode, "threshold" (recommended) or "single" (unsupported). threshold mode requires --cosigners and --threshold`, + ) + cmd.Flags().StringSliceP(flagNode, "n", []string{}, "chain nodes in format tcp://{p2p-addr}:{port}") + cmd.Flags().StringSliceP(flagCosigner, "c", []string{}, + "cosigners in format tcp://{p2p-addr}:{port}|{shard-id} \n"+ + "(i.e. \"tcp://node-1:2222|1,tcp://node-2:2222|2,tcp://node-3:2222|3\")") + cmd.Flags().IntP(flagThreshold, "t", 0, "number of shards required for threshold signature") + cmd.Flags().StringP( + flagDebugAddr, "d", "", + "listen address for debug server and prometheus metrics in format localhost:8543", + ) + cmd.Flags().StringP(flagKeyDir, "k", "", "key directory if other than home directory") + cmd.Flags().String(flagRaftTimeout, "1500ms", "cosigner raft timeout value, \n"+ + "accepts valid duration strings for Go's time.ParseDuration() e.g. 1s, 1000ms, 1.5m") + cmd.Flags().String(flagGRPCTimeout, "1500ms", "cosigner grpc 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") + cmd.Flags().BoolP(flagOverwrite, "o", false, "overwrite an existing config.yaml") return cmd } diff --git a/cmd/horcrux/cmd/config_test.go b/cmd/horcrux/cmd/config_test.go index 81bd0825..da47a495 100644 --- a/cmd/horcrux/cmd/config_test.go +++ b/cmd/horcrux/cmd/config_test.go @@ -12,49 +12,130 @@ import ( func TestConfigInitCmd(t *testing.T) { tmpHome := t.TempDir() tcs := []struct { - name string - home string - args []string - expectErr bool + name string + home string + args []string + expectErr string + expectConfig string }{ { - name: "valid init", - home: tmpHome + "_valid_init", + name: "valid init threshold", + home: tmpHome + "_valid_init_threshold", args: []string{ - "tcp://10.168.0.1:1234", - "-c", - "-p", "tcp://10.168.1.2:2222|2,tcp://10.168.1.3:2222|3", + "-n", "tcp://10.168.0.1:1234", + "-n", "tcp://10.168.0.2:1234", + "-c", "tcp://10.168.1.1:2222", + "-c", "tcp://10.168.1.2:2222", + "-c", "tcp://10.168.1.3:2222", "-t", "2", - "-l", "tcp://10.168.1.1:2222", - "--timeout", "1500ms", + "--raft-timeout", "1500ms", + "--grpc-timeout", "1500ms", }, - expectErr: false, + expectConfig: `signMode: threshold +thresholdMode: + threshold: 2 + cosigners: + - shardID: 1 + p2pAddr: tcp://10.168.1.1:2222 + - shardID: 2 + p2pAddr: tcp://10.168.1.2:2222 + - shardID: 3 + p2pAddr: tcp://10.168.1.3:2222 + grpcTimeout: 1500ms + raftTimeout: 1500ms +chainNodes: +- privValAddr: tcp://10.168.0.1:1234 +- privValAddr: tcp://10.168.0.2:1234 +`, }, { - name: "invalid chain-nodes", - home: tmpHome + "_invalid_chain-nodes", + name: "valid init single signer", + home: tmpHome + "_valid_init_single", args: []string{ - "://10.168.0.1:1234", // Missing/malformed protocol scheme - "-c", - "-p", "tcp://10.168.1.2:2222|2,tcp://10.168.1.3:2222|3", + "-m", "single", + "-n", "tcp://10.168.0.1:1234", + "-n", "tcp://10.168.0.2:1234", + }, + expectConfig: `signMode: single +chainNodes: +- privValAddr: tcp://10.168.0.1:1234 +- privValAddr: tcp://10.168.0.2:1234 +`, + }, + { + name: "invalid chain-node", + home: tmpHome + "_invalid_chain-node", + args: []string{ + "-n", "://10.168.0.1:1234", + "-n", "tcp://10.168.0.2:1234", + "-c", "tcp://10.168.1.1:2222", + "-c", "tcp://10.168.1.2:2222", + "-c", "tcp://10.168.1.3:2222", "-t", "2", - "-l", "tcp://10.168.1.1:2222", - "--timeout", "1500ms", + "--raft-timeout", "1500ms", + "--grpc-timeout", "1500ms", }, - expectErr: true, + expectErr: `parse "://10.168.0.1:1234": missing protocol scheme`, }, { - name: "invalid peer-nodes", - home: tmpHome + "_invalid_peer-nodes", + name: "invalid cosigner node", + home: tmpHome + "_invalid_cosigner-node", args: []string{ - "tcp://10.168.0.1:1234", - "-c", - "-p", "tcp://10.168.1.2:2222,tcp://10.168.1.3:2222", // Missing share IDs + "-n", "tcp://10.168.0.1:1234", + "-n", "tcp://10.168.0.2:1234", + "-c", "://10.168.1.1:2222", + "-c", "tcp://10.168.1.2:2222", + "-c", "tcp://10.168.1.3:2222", "-t", "2", - "-l", "tcp://10.168.1.1:2222", - "--timeout", "1500ms", + "--raft-timeout", "1500ms", + "--grpc-timeout", "1500ms", }, - expectErr: true, + expectErr: `failed to parse cosigner (shard ID: 1) p2p address: parse "://10.168.1.1:2222": missing protocol scheme`, + }, + { + name: "invalid threshold", + home: tmpHome + "_invalid_threshold", + args: []string{ + "-n", "tcp://10.168.0.1:1234", + "-n", "tcp://10.168.0.2:1234", + "-c", "tcp://10.168.1.1:2222", + "-c", "tcp://10.168.1.2:2222", + "-c", "tcp://10.168.1.3:2222", + "-t", "1", + "--raft-timeout", "1500ms", + "--grpc-timeout", "1500ms", + }, + expectErr: "threshold (1) must be greater than number of shards (3) / 2", + }, + { + name: "invalid raft timeout", + home: tmpHome + "_invalid_raft-timeout", + args: []string{ + "-n", "tcp://10.168.0.1:1234", + "-n", "tcp://10.168.0.2:1234", + "-c", "tcp://10.168.1.1:2222", + "-c", "tcp://10.168.1.2:2222", + "-c", "tcp://10.168.1.3:2222", + "-t", "2", + "--raft-timeout", "1500", + "--grpc-timeout", "1500ms", + }, + expectErr: `invalid raftTimeout: time: missing unit in duration "1500"`, + }, + { + name: "invalid grpc timeout", + home: tmpHome + "_invalid_grpc-timeout", + args: []string{ + "-n", "tcp://10.168.0.1:1234", + "-n", "tcp://10.168.0.2:1234", + "-c", "tcp://10.168.1.1:2222", + "-c", "tcp://10.168.1.2:2222", + "-c", "tcp://10.168.1.3:2222", + "-t", "2", + "--raft-timeout", "1500ms", + "--grpc-timeout", "1500", + }, + expectErr: `invalid grpcTimeout: time: missing unit in duration "1500"`, }, } @@ -72,10 +153,16 @@ func TestConfigInitCmd(t *testing.T) { cmd.SetArgs(args) err = cmd.Execute() - if tc.expectErr { + if tc.expectErr != "" { require.Error(t, err) + require.EqualError(t, err, tc.expectErr) } else { require.NoError(t, err) + + actualConfig, err := os.ReadFile(filepath.Join(tmpConfig, "config.yaml")) + require.NoError(t, err) + + require.Equal(t, tc.expectConfig, string(actualConfig)) } }) } diff --git a/cmd/horcrux/cmd/cosigner.go b/cmd/horcrux/cmd/cosigner.go deleted file mode 100644 index 6a1f6359..00000000 --- a/cmd/horcrux/cmd/cosigner.go +++ /dev/null @@ -1,225 +0,0 @@ -package cmd - -import ( - "encoding/hex" - "encoding/json" - "fmt" - "os" - "path/filepath" - "strings" - "time" - - cometlog "github.com/cometbft/cometbft/libs/log" - cometservice "github.com/cometbft/cometbft/libs/service" - "github.com/cosmos/cosmos-sdk/types/bech32" - "github.com/spf13/cobra" - "github.com/strangelove-ventures/horcrux/signer" -) - -func cosignerCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "cosigner", - Short: "Threshold mpc signer for TM based nodes", - } - - cmd.AddCommand(startCosignerCmd()) - cmd.AddCommand(addressCmd()) - - return cmd -} - -type AddressCmdOutput struct { - HexAddress string - PubKey string - ValConsAddress string - ValConsPubAddress string -} - -func addressCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "address chain-id [bech32]", - Short: "Get public key hex address and valcons address", - Example: `horcrux cosigner address cosmos`, - SilenceUsage: true, - 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(args[0]) - if err != nil { - return err - } - - key, err := signer.LoadCosignerKey(keyFile) - if err != nil { - return fmt.Errorf("error reading cosigner key: %s", err) - } - - pubKey := key.PubKey - pubKeyAddress := pubKey.Address() - - pubKeyJSON, err := signer.PubKey("", pubKey) - if err != nil { - return err - } - - output := AddressCmdOutput{ - HexAddress: strings.ToUpper(hex.EncodeToString(pubKeyAddress)), - PubKey: pubKeyJSON, - } - - 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[1], pubKey) - if err != nil { - return err - } - output.ValConsPubAddress = pubKeyBech32 - } else { - bech32Hint := "Pass bech32 base prefix as argument to generate (e.g. cosmos)" - output.ValConsAddress = bech32Hint - output.ValConsPubAddress = bech32Hint - } - - jsonOut, err := json.Marshal(output) - if err != nil { - return err - } - - fmt.Println(string(jsonOut)) - - return nil - }, - } - - return cmd -} - -func startCosignerCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "start", - Short: "Start cosigner process", - Args: cobra.NoArgs, - SilenceUsage: true, - RunE: func(cmd *cobra.Command, args []string) error { - if err := signer.RequireNotRunning(config.PidFile); err != nil { - return err - } - - if err := config.Config.ValidateCosignerConfig(); err != nil { - return err - } - - 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.KeyFileExistsCosignerRSA() - if err != nil { - return err - } - - logger.Info( - "CometBFT Validator", - "mode", "threshold", - "priv-state-dir", config.StateDir, - ) - - key, err := signer.LoadCosignerKeyRSA(keyFile) - if err != nil { - return fmt.Errorf("error reading cosigner key (%s): %w", keyFile, err) - } - - cosigners := []signer.Cosigner{} - - // add ourselves as a peer so localcosigner can handle GetEphSecPart requests - peers := []signer.CosignerPeer{{ - ID: key.ID, - PublicKey: key.RSAKey.PublicKey, - }} - - cosignerConfig := config.Config.CosignerConfig - - for _, cosignerParams := range cosignerConfig.Peers { - cosigner := signer.NewRemoteCosigner(cosignerParams.ShareID, cosignerParams.P2PAddr) - cosigners = append(cosigners, cosigner) - - pubKey := key.CosignerKeys[cosignerParams.ShareID-1] - peers = append(peers, signer.CosignerPeer{ - ID: cosigner.GetID(), - PublicKey: *pubKey, - }) - } - - total := len(cosignerConfig.Peers) + 1 - - localCosigner := signer.NewLocalCosigner( - &config, - key.ID, - key.RSAKey, - peers, - cosignerConfig.P2PListen, - uint8(total), - uint8(cosignerConfig.Threshold), - ) - - timeout, err := time.ParseDuration(cosignerConfig.Timeout) - if err != nil { - return fmt.Errorf("error parsing configured timeout: %s. %w", cosignerConfig.Timeout, err) - } - - raftDir := filepath.Join(config.HomeDir, "raft") - if err := os.MkdirAll(raftDir, 0700); err != nil { - return fmt.Errorf("error creating raft directory: %w", err) - } - - // RAFT node ID is the cosigner ID - nodeID := fmt.Sprint(key.ID) - - // Start RAFT store listener - raftStore := signer.NewRaftStore(nodeID, - raftDir, cosignerConfig.P2PListen, timeout, logger, localCosigner, cosigners) - if err := raftStore.Start(); err != nil { - return fmt.Errorf("error starting raft store: %w", err) - } - services := []cometservice.Service{raftStore} - - val := signer.NewThresholdValidator( - logger, - &config, - cosignerConfig.Threshold, - localCosigner, - cosigners, - raftStore, - ) - - raftStore.SetThresholdValidator(val) - - go EnableDebugAndMetrics(cmd.Context(), out) - - services, err = signer.StartRemoteSigners(services, logger, val, config.Config.Nodes()) - if err != nil { - return fmt.Errorf("failed to start remote signer(s): %w", err) - } - - signer.WaitAndTerminate(logger, services, config.PidFile) - - return nil - }, - } - - return cmd -} diff --git a/cmd/horcrux/cmd/leader_election.go b/cmd/horcrux/cmd/leader_election.go index c99349e9..7b5a7d80 100644 --- a/cmd/horcrux/cmd/leader_election.go +++ b/cmd/horcrux/cmd/leader_election.go @@ -9,6 +9,7 @@ import ( grpcretry "github.com/grpc-ecosystem/go-grpc-middleware/retry" "github.com/spf13/cobra" "github.com/strangelove-ventures/horcrux/client" + "github.com/strangelove-ventures/horcrux/signer" "github.com/strangelove-ventures/horcrux/signer/proto" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -26,12 +27,12 @@ To choose a specific leader, pass that leader's ID as an argument. horcrux elect 2 # elect specific leader`, SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) (err error) { - if config.Config.CosignerConfig == nil { - return fmt.Errorf("cosigner configuration is not present in config file") + if config.Config.ThresholdModeConfig == nil { + return fmt.Errorf("threshold mode configuration is not present in config file") } - if len(config.Config.CosignerConfig.Peers) == 0 { - return fmt.Errorf("cosigner configuration has no peers") + if len(config.Config.ThresholdModeConfig.Cosigners) == 0 { + return fmt.Errorf("threshold mode configuration has no cosigners") } serviceConfig := `{"healthCheckConfig": {"serviceName": "Leader"}, "loadBalancingConfig": [ { "round_robin": {} } ]}` @@ -40,7 +41,7 @@ horcrux elect 2 # elect specific leader`, grpcretry.WithMax(5), } - grpcAddress, err := config.Config.CosignerConfig.LeaderElectMultiAddress() + grpcAddress, err := config.Config.ThresholdModeConfig.LeaderElectMultiAddress() if err != nil { return err } @@ -93,12 +94,35 @@ func getLeaderCmd() *cobra.Command { Example: `horcrux leader`, SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) (err error) { - if config.Config.CosignerConfig == nil { - return fmt.Errorf("cosigner configuration is not present in config file") + thresholdCfg := config.Config.ThresholdModeConfig + if thresholdCfg == nil { + return fmt.Errorf("threshold mode configuration is not present in config file") } - if len(config.Config.CosignerConfig.Peers) == 0 { - return fmt.Errorf("cosigner configuration has no peers") + if len(thresholdCfg.Cosigners) == 0 { + return fmt.Errorf("threshold mode configuration has no cosigners") + } + + keyFile, err := config.KeyFileExistsCosignerRSA() + if err != nil { + return err + } + + key, err := signer.LoadCosignerRSAKey(keyFile) + if err != nil { + return fmt.Errorf("error reading cosigner key (%s): %w", keyFile, err) + } + + var p2pListen string + + for _, c := range thresholdCfg.Cosigners { + if c.ShardID == key.ID { + p2pListen = c.P2PAddr + } + } + + if p2pListen == "" { + return fmt.Errorf("cosigner config does not exist for our shard ID %d", key.ID) } retryOpts := []grpcretry.CallOption{ @@ -106,7 +130,7 @@ func getLeaderCmd() *cobra.Command { grpcretry.WithMax(5), } - grpcAddress, err := client.SanitizeAddress(config.Config.CosignerConfig.P2PListen) + grpcAddress, err := client.SanitizeAddress(p2pListen) if err != nil { return err } diff --git a/cmd/horcrux/cmd/migrate.go b/cmd/horcrux/cmd/migrate.go index 3794117d..e9e9de0c 100644 --- a/cmd/horcrux/cmd/migrate.go +++ b/cmd/horcrux/cmd/migrate.go @@ -19,31 +19,76 @@ import ( "gopkg.in/yaml.v2" ) +func legacyConfig() (*v2Config, error) { + configFile, err := os.ReadFile(config.ConfigFile) + if err != nil { + return nil, err + } + + legacyConfig := new(v2Config) + + if err := yaml.Unmarshal(configFile, &legacyConfig); err != nil { + return nil, fmt.Errorf("failed to read config file as legacy: %w", err) + } + + if err := legacyConfig.validate(); err != nil { + return nil, err + } + + return legacyConfig, nil +} + type ( v2Config struct { - ChainID string `json:"chain-id" yaml:"chain-id"` - PrivValKeyFile *string `json:"key-file,omitempty" yaml:"key-file,omitempty"` + ChainID string `json:"chain-id" yaml:"chain-id"` + PrivValKeyFile *string `json:"key-file,omitempty" yaml:"key-file,omitempty"` + Cosigner *v2CosignerConfig `json:"cosigner" yaml:"cosigner"` + ChainNodes []v2ChainNodeConfig `json:"chain-nodes,omitempty" yaml:"chain-nodes,omitempty"` + DebugAddr string `json:"debug-addr,omitempty" yaml:"debug-addr,omitempty"` + } + + v2CosignerConfig struct { + Threshold int `json:"threshold" yaml:"threshold"` + Shares int `json:"shares" yaml:"shares"` + P2PListen string `json:"p2p-listen" yaml:"p2p-listen"` + Peers []struct { + ShareID int `json:"share-id" yaml:"share-id"` + P2PAddr string `json:"p2p-addr" yaml:"p2p-addr"` + } `json:"peers" yaml:"peers"` + Timeout string `json:"rpc-timeout" yaml:"rpc-timeout"` + } + + v2ChainNodeConfig struct { + PrivValAddr string `json:"priv-val-addr" yaml:"priv-val-addr"` } 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"` + PubKey cometcrypto.PubKey `json:"pub_key"` + ShareKey []byte `json:"secret_share"` + RSAKey rsa.PrivateKey `json:"rsa_key"` + ID int `json:"id"` + RSAPubs []*rsa.PublicKey `json:"rsa_pubs"` } ) -func (cosignerKey *v2CosignerKey) UnmarshalJSON(data []byte) error { +func (c *v2Config) validate() error { + if c.ChainID == "" { + return fmt.Errorf("chain-id is empty") + } + + return nil +} + +func (key *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"` + RSAKey []byte `json:"rsa_key"` + PubkeyBytes []byte `json:"pub_key"` + RSAPubs [][]byte `json:"rsa_pubs"` *Alias }{ - Alias: (*Alias)(cosignerKey), + Alias: (*Alias)(key), } if err := json.Unmarshal(data, &aux); err != nil { return err @@ -59,7 +104,7 @@ func (cosignerKey *v2CosignerKey) UnmarshalJSON(data []byte) error { // 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) + // cdc.MarshalBinaryBare(CosignerEd25519Key.PubKey) // // To support reading the public key bytes from these key files, we fallback to // amino unmarshalling if the protobuf unmarshalling fails @@ -81,35 +126,35 @@ func (cosignerKey *v2CosignerKey) UnmarshalJSON(data []byte) error { } // unmarshal the public key bytes for each cosigner - cosignerKey.CosignerKeys = make([]*rsa.PublicKey, 0) - for _, bytes := range aux.CosignerKeys { + key.RSAPubs = make([]*rsa.PublicKey, 0) + for _, bytes := range aux.RSAPubs { cosignerRsaPubkey, err := x509.ParsePKCS1PublicKey(bytes) if err != nil { return err } - cosignerKey.CosignerKeys = append(cosignerKey.CosignerKeys, cosignerRsaPubkey) + key.RSAPubs = append(key.RSAPubs, cosignerRsaPubkey) } - cosignerKey.RSAKey = *privateKey - cosignerKey.PubKey = pubkey + key.RSAKey = *privateKey + key.PubKey = pubkey return nil } -func (cosignerKey *v2CosignerKey) validate() error { +func (key *v2CosignerKey) validate() error { var errs []error - if cosignerKey.PubKey == nil || len(cosignerKey.PubKey.Bytes()) == 0 { + if key.PubKey == nil || len(key.PubKey.Bytes()) == 0 { errs = append(errs, fmt.Errorf("pub_key cannot be empty")) } - if len(cosignerKey.ShareKey) == 0 { + if len(key.ShareKey) == 0 { errs = append(errs, fmt.Errorf("secret_share cannot be empty")) } - if err := cosignerKey.RSAKey.Validate(); err != nil { + if err := key.RSAKey.Validate(); err != nil { errs = append(errs, fmt.Errorf("rsa_key is invalid: %w", err)) } - if cosignerKey.ID == 0 { + if key.ID == 0 { errs = append(errs, fmt.Errorf("id cannot be zero")) } - if len(cosignerKey.CosignerKeys) == 0 { + if len(key.RSAPubs) == 0 { errs = append(errs, fmt.Errorf("cosigner keys cannot be empty")) } @@ -118,34 +163,38 @@ func (cosignerKey *v2CosignerKey) validate() error { func migrateCmd() *cobra.Command { return &cobra.Command{ - Use: "migrate", + Use: "migrate [chain-id]", Short: "Migrate config and key files from v2 to v3", SilenceUsage: true, - Args: cobra.NoArgs, + Args: cobra.RangeArgs(0, 1), RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true - configFile, err := os.ReadFile(config.ConfigFile) - if err != nil { - return err + legacyCfg, legacyCfgErr := legacyConfig() + if legacyCfgErr != nil { + fmt.Fprintf( + cmd.OutOrStderr(), + "failed to load legacy config: %v, proceeding to attempt key migration", + legacyCfgErr, + ) } - var legacyConfig v2Config + var chainID string - if err := yaml.Unmarshal(configFile, &legacyConfig); err != nil { - return fmt.Errorf("failed to read config file as legacy: %w", err) - } + if len(args) == 1 { + chainID = args[0] + } else { + if legacyCfgErr != nil { + return fmt.Errorf("unable to migrate v2 config without chain-id. please provide [chain-id] argument") + } - if legacyConfig.ChainID == "" { - return fmt.Errorf("unable to migrate v2 config without chain-id") + chainID = legacyCfg.ChainID } - chainID := legacyConfig.ChainID - var legacyCosignerKeyFile string - if legacyConfig.PrivValKeyFile != nil && *legacyConfig.PrivValKeyFile != "" { - legacyCosignerKeyFile = *legacyConfig.PrivValKeyFile + if legacyCfgErr == nil && legacyCfg.PrivValKeyFile != nil && *legacyCfg.PrivValKeyFile != "" { + legacyCosignerKeyFile = *legacyCfg.PrivValKeyFile dir := filepath.Dir(legacyCosignerKeyFile) config.Config.PrivValKeyDir = &dir } else { @@ -171,10 +220,10 @@ func migrateCmd() *cobra.Command { return err } - newEd25519Key := signer.CosignerKey{ - PubKey: legacyCosignerKey.PubKey, - ShareKey: legacyCosignerKey.ShareKey, - ID: legacyCosignerKey.ID, + newEd25519Key := signer.CosignerEd25519Key{ + PubKey: legacyCosignerKey.PubKey, + PrivateShard: legacyCosignerKey.ShareKey, + ID: legacyCosignerKey.ID, } newEd25519KeyBz, err := newEd25519Key.MarshalJSON() @@ -187,10 +236,10 @@ func migrateCmd() *cobra.Command { 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, + newRSAKey := signer.CosignerRSAKey{ + RSAKey: legacyCosignerKey.RSAKey, + ID: legacyCosignerKey.ID, + RSAPubs: legacyCosignerKey.RSAPubs, } newRSAKeyBz, err := newRSAKey.MarshalJSON() @@ -203,8 +252,56 @@ func migrateCmd() *cobra.Command { return fmt.Errorf("failed to write new RSA key to %s: %w", newRSAPath, err) } - if err := config.WriteConfigFile(); err != nil { - return err + // only attempt config migration if legacy config exists + if legacyCfgErr == nil { + var migratedNodes signer.ChainNodes + + for _, n := range legacyCfg.ChainNodes { + migratedNodes = append(migratedNodes, signer.ChainNode{ + PrivValAddr: n.PrivValAddr, + }) + } + + config.Config.ChainNodes = migratedNodes + config.Config.DebugAddr = legacyCfg.DebugAddr + + signMode := signer.SignModeSingle + + if legacyCfg.Cosigner != nil { + signMode = signer.SignModeThreshold + + var migratedCosigners signer.CosignersConfig + + if legacyCfg.Cosigner.P2PListen != "" { + migratedCosigners = append( + migratedCosigners, + signer.CosignerConfig{ + ShardID: legacyCosignerKey.ID, + P2PAddr: legacyCfg.Cosigner.P2PListen, + }, + ) + } + + for _, c := range legacyCfg.Cosigner.Peers { + migratedCosigners = append(migratedCosigners, signer.CosignerConfig{ + ShardID: c.ShareID, + P2PAddr: c.P2PAddr, + }) + } + + config.Config.ThresholdModeConfig = &signer.ThresholdModeConfig{ + Threshold: legacyCfg.Cosigner.Threshold, + Cosigners: migratedCosigners, + GRPCTimeout: legacyCfg.Cosigner.Timeout, + RaftTimeout: legacyCfg.Cosigner.Timeout, + } + } + + config.Config.SignMode = signMode + + if err := config.WriteConfigFile(); err != nil { + return err + } } if err := os.Remove(legacyCosignerKeyFile); err != nil { diff --git a/cmd/horcrux/cmd/migrate_test.go b/cmd/horcrux/cmd/migrate_test.go index d571eda8..5aee2134 100644 --- a/cmd/horcrux/cmd/migrate_test.go +++ b/cmd/horcrux/cmd/migrate_test.go @@ -33,21 +33,21 @@ func TestMigrateV2toV3(t *testing.T) { require.NoFileExists(t, keyShareFile) - newKeyShareFile := filepath.Join(tmp, "test_share.json") - require.FileExists(t, newKeyShareFile) + newKeyShardFile := filepath.Join(tmp, "test_shard.json") + require.FileExists(t, newKeyShardFile) newRSAKeyFile := filepath.Join(tmp, "rsa_keys.json") require.FileExists(t, newRSAKeyFile) - newKeyShareFileBz, err := os.ReadFile(newKeyShareFile) + newKeyShardFileBz, err := os.ReadFile(newKeyShardFile) require.NoError(t, err) - require.Equal(t, testdata.CosignerKeyMigratedEd25519, string(newKeyShareFileBz)) + require.Equal(t, testdata.CosignerEd25519KeyMigrated, string(newKeyShardFileBz)) newRSAKeyFileBz, err := os.ReadFile(newRSAKeyFile) require.NoError(t, err) - require.Equal(t, testdata.CosignerKeyMigratedRSA, string(newRSAKeyFileBz)) + require.Equal(t, testdata.CosignerRSAKeyMigrated, string(newRSAKeyFileBz)) newConfigFileBz, err := os.ReadFile(configFile) require.NoError(t, err) @@ -95,24 +95,138 @@ func TestMigrateV2toV3DifferentKeyFilePath(t *testing.T) { require.NoFileExists(t, keyShareFile) - newKeyShareFile := filepath.Join(keyDir, "test_share.json") - require.FileExists(t, newKeyShareFile) + newKeyShardFile := filepath.Join(keyDir, "test_shard.json") + require.FileExists(t, newKeyShardFile) newRSAKeyFile := filepath.Join(keyDir, "rsa_keys.json") require.FileExists(t, newRSAKeyFile) - newKeyShareFileBz, err := os.ReadFile(newKeyShareFile) + newKeyShardFileBz, err := os.ReadFile(newKeyShardFile) require.NoError(t, err) - require.Equal(t, testdata.CosignerKeyMigratedEd25519, string(newKeyShareFileBz)) + require.Equal(t, testdata.CosignerEd25519KeyMigrated, string(newKeyShardFileBz)) newRSAKeyFileBz, err := os.ReadFile(newRSAKeyFile) require.NoError(t, err) - require.Equal(t, testdata.CosignerKeyMigratedRSA, string(newRSAKeyFileBz)) + require.Equal(t, testdata.CosignerRSAKeyMigrated, 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)) + require.Equal(t, fmt.Sprintf("keyDir: %s\n", keyDir)+testdata.ConfigMigrated, string(newConfigFileBz)) +} + +// Should migrate keys only if config has already been migrated +func TestMigrateV2toV3KeysOnly(t *testing.T) { + tmp := t.TempDir() + + 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", "test"} + cmd.SetArgs(args) + err = cmd.Execute() + require.NoError(t, err) + + require.NoFileExists(t, keyShareFile) + + newKeyShardFile := filepath.Join(tmp, "test_shard.json") + require.FileExists(t, newKeyShardFile) + + newRSAKeyFile := filepath.Join(tmp, "rsa_keys.json") + require.FileExists(t, newRSAKeyFile) + + newKeyShardFileBz, err := os.ReadFile(newKeyShardFile) + require.NoError(t, err) + + require.Equal(t, testdata.CosignerEd25519KeyMigrated, string(newKeyShardFileBz)) + + newRSAKeyFileBz, err := os.ReadFile(newRSAKeyFile) + require.NoError(t, err) + + require.Equal(t, testdata.CosignerRSAKeyMigrated, string(newRSAKeyFileBz)) +} + +// Should not modify config that is already in v3 format +func TestMigrateV2toV3ConfigAlreadyMigrated(t *testing.T) { + tmp := t.TempDir() + + configFile := filepath.Join(tmp, "config.yaml") + + err := os.WriteFile(configFile, []byte(testdata.ConfigMigrated), 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", "test"} + cmd.SetArgs(args) + err = cmd.Execute() + require.NoError(t, err) + + require.NoFileExists(t, keyShareFile) + + newKeyShardFile := filepath.Join(tmp, "test_shard.json") + require.FileExists(t, newKeyShardFile) + + newRSAKeyFile := filepath.Join(tmp, "rsa_keys.json") + require.FileExists(t, newRSAKeyFile) + + newKeyShardFileBz, err := os.ReadFile(newKeyShardFile) + require.NoError(t, err) + + require.Equal(t, testdata.CosignerEd25519KeyMigrated, string(newKeyShardFileBz)) + + newRSAKeyFileBz, err := os.ReadFile(newRSAKeyFile) + require.NoError(t, err) + + require.Equal(t, testdata.CosignerRSAKeyMigrated, string(newRSAKeyFileBz)) + + newConfigFileBz, err := os.ReadFile(configFile) + require.NoError(t, err) + + require.Equal(t, testdata.ConfigMigrated, string(newConfigFileBz)) +} + +// Should not modify config or keys that are already in v3 format +func TestMigrateV2toV3AlreadyMigrated(t *testing.T) { + tmp := t.TempDir() + + configFile := filepath.Join(tmp, "config.yaml") + + err := os.WriteFile(configFile, []byte(testdata.ConfigMigrated), 0600) + require.NoError(t, err) + + ed25519KeyShardFile := filepath.Join(tmp, "test_shard.json") + + err = os.WriteFile(ed25519KeyShardFile, []byte(testdata.CosignerEd25519KeyMigrated), 0600) + require.NoError(t, err) + + rsaKeyShardFile := filepath.Join(tmp, "rsa_keys.json") + + err = os.WriteFile(rsaKeyShardFile, []byte(testdata.CosignerRSAKeyMigrated), 0600) + require.NoError(t, err) + + cmd := rootCmd() + cmd.SetOutput(io.Discard) + args := []string{"--home", tmp, "config", "migrate", "test"} + cmd.SetArgs(args) + err = cmd.Execute() + require.Error(t, err) + require.EqualError( + t, err, + fmt.Sprintf( + "error loading v2 key file: stat %s: no such file or directory", + filepath.Join(tmp, "share.json"), + ), + ) } diff --git a/cmd/horcrux/cmd/root.go b/cmd/horcrux/cmd/root.go index e4060f3c..eff962d6 100644 --- a/cmd/horcrux/cmd/root.go +++ b/cmd/horcrux/cmd/root.go @@ -17,16 +17,16 @@ var config signer.RuntimeConfig func rootCmd() *cobra.Command { cmd := &cobra.Command{ Use: "horcrux", - Short: "A tendermint remote signer with both single signer and threshold signer modes", + Short: "A tendermint remote signer with both threshold signer and single signer modes", } cmd.AddCommand(configCmd()) - cmd.AddCommand(cosignerCmd()) - cmd.AddCommand(createCosignerEd25519SharesCmd()) - cmd.AddCommand(createCosignerRSASharesCmd()) + cmd.AddCommand(startCmd()) + cmd.AddCommand(addressCmd()) + cmd.AddCommand(createCosignerEd25519ShardsCmd()) + cmd.AddCommand(createCosignerRSAShardsCmd()) cmd.AddCommand(leaderElectionCmd()) cmd.AddCommand(getLeaderCmd()) - cmd.AddCommand(signerCmd()) cmd.AddCommand(stateCmd()) cmd.AddCommand(versionCmd()) diff --git a/cmd/horcrux/cmd/key2shares.go b/cmd/horcrux/cmd/shards.go similarity index 67% rename from cmd/horcrux/cmd/key2shares.go rename to cmd/horcrux/cmd/shards.go index 8f052dbf..e527dd58 100644 --- a/cmd/horcrux/cmd/key2shares.go +++ b/cmd/horcrux/cmd/shards.go @@ -46,7 +46,7 @@ func createCosignerDirectoryIfNecessary(out string, id int) (string, error) { const ( flagOutputDir = "out" flagThreshold = "threshold" - flagShares = "shares" + flagShards = "shards" flagKeyFile = "key-file" flagChainID = "chain-id" ) @@ -55,31 +55,31 @@ 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 addShardsFlag(cmd *cobra.Command) { + cmd.Flags().Uint8(flagShards, 0, "total key shards") } func addShardFlags(cmd *cobra.Command) { - addShareFlag(cmd) - cmd.Flags().Uint8(flagThreshold, 0, "threshold number of shares required to successfully sign") + addShardsFlag(cmd) + cmd.Flags().Uint8(flagThreshold, 0, "threshold number of shards 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 createCosignerEd25519SharesCmd() *cobra.Command { +// createCosignerEd25519ShardsCmd is a cobra command for creating +// cosigner shards from a full priv validator key. +func createCosignerEd25519ShardsCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "create-ed25519-shares chain-id priv-validator-key-file threshold shares", - Aliases: []string{"shard", "shares"}, - Args: cobra.NoArgs, - Short: "Create cosigner Ed25519 shares", + Use: "create-ed25519-shards chain-id priv-validator-key-file threshold shards", + Args: cobra.NoArgs, + Short: "Create cosigner Ed25519 shards", RunE: func(cmd *cobra.Command, args []string) (err error) { flags := cmd.Flags() chainID, _ := flags.GetString(flagChainID) keyFile, _ := flags.GetString(flagKeyFile) threshold, _ := flags.GetUint8(flagThreshold) - shares, _ := flags.GetUint8(flagShares) + shards, _ := flags.GetUint8(flagShards) var errs []error @@ -95,31 +95,31 @@ func createCosignerEd25519SharesCmd() *cobra.Command { 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 shards == 0 { + errs = append(errs, fmt.Errorf("shards 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 { + if threshold > shards { errs = append(errs, fmt.Errorf( - "threshold cannot be greater than total shares, got [threshold](%d) > [shares](%d)", - threshold, shares, + "threshold cannot be greater than total shards, got [threshold](%d) > [shards](%d)", + threshold, shards, )) } - 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 threshold <= shards/2 { + errs = append(errs, fmt.Errorf("threshold must be greater than total shards "+ + "divided by 2, got [threshold](%d) <= [shards](%d) / 2", threshold, shards)) } if len(errs) > 0 { return errors.Join(errs...) } - csKeys, err := signer.CreateCosignerSharesFromFile(keyFile, threshold, shares) + csKeys, err := signer.CreateCosignerEd25519ShardsFromFile(keyFile, threshold, shards) if err != nil { return err } @@ -139,11 +139,11 @@ func createCosignerEd25519SharesCmd() *cobra.Command { if err != nil { return err } - filename := filepath.Join(dir, fmt.Sprintf("%s_share.json", chainID)) - if err = signer.WriteCosignerShareFile(c, filename); err != nil { + filename := filepath.Join(dir, fmt.Sprintf("%s_shard.json", chainID)) + if err = signer.WriteCosignerEd25519ShardFile(c, filename); err != nil { return err } - fmt.Fprintf(cmd.OutOrStdout(), "Created Ed25519 Share %s\n", filename) + fmt.Fprintf(cmd.OutOrStdout(), "Created Ed25519 Shard %s\n", filename) } return nil }, @@ -153,22 +153,21 @@ func createCosignerEd25519SharesCmd() *cobra.Command { return cmd } -// CreateCosignerSharesCmd is a cobra command for creating cosigner shares from a priv validator -func createCosignerRSASharesCmd() *cobra.Command { +// createCosignerRSAShardsCmd is a cobra command for creating cosigner shards from a priv validator +func createCosignerRSAShardsCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "create-rsa-shares shares", - Aliases: []string{"shard", "shares"}, - Args: cobra.NoArgs, - Short: "Create cosigner RSA shares", + Use: "create-rsa-shards shards", + Args: cobra.NoArgs, + Short: "Create cosigner RSA shards", RunE: func(cmd *cobra.Command, args []string) (err error) { - shares, _ := cmd.Flags().GetUint8(flagShares) + shards, _ := cmd.Flags().GetUint8(flagShards) - if shares <= 0 { - return fmt.Errorf("shares must be greater than zero (%d): %w", shares, err) + if shards <= 0 { + return fmt.Errorf("shards must be greater than zero (%d): %w", shards, err) } - csKeys, err := signer.CreateCosignerSharesRSA(int(shares)) + csKeys, err := signer.CreateCosignerRSAShards(int(shards)) if err != nil { return err } @@ -189,15 +188,15 @@ func createCosignerRSASharesCmd() *cobra.Command { return err } filename := filepath.Join(dir, "rsa_keys.json") - if err = signer.WriteCosignerShareRSAFile(c, filename); err != nil { + if err = signer.WriteCosignerRSAShardFile(c, filename); err != nil { return err } - fmt.Fprintf(cmd.OutOrStdout(), "Created RSA Share %s\n", filename) + fmt.Fprintf(cmd.OutOrStdout(), "Created RSA Shard %s\n", filename) } return nil }, } - addShareFlag(cmd) + addShardsFlag(cmd) addOutputDirFlag(cmd) return cmd } diff --git a/cmd/horcrux/cmd/key2shares_test.go b/cmd/horcrux/cmd/shards_test.go similarity index 75% rename from cmd/horcrux/cmd/key2shares_test.go rename to cmd/horcrux/cmd/shards_test.go index 5b244453..a640376c 100644 --- a/cmd/horcrux/cmd/key2shares_test.go +++ b/cmd/horcrux/cmd/shards_test.go @@ -12,7 +12,7 @@ import ( const testChainID = "test" -func TestKey2Shares(t *testing.T) { +func TestEd25519Shards(t *testing.T) { tmp := t.TempDir() privValidatorKeyFile := filepath.Join(tmp, "priv_validator_key.json") @@ -26,62 +26,62 @@ func TestKey2Shares(t *testing.T) { expectErr bool }{ { - name: "valid threshold and shares", + name: "valid threshold and shards", args: []string{ "--chain-id", testChainID, "--key-file", privValidatorKeyFile, "--threshold", "2", - "--shares", "3", + "--shards", "3", }, expectErr: false, }, { - name: "valid threshold and shares 2", + name: "valid threshold and shards 2", args: []string{ "--chain-id", testChainID, "--key-file", privValidatorKeyFile, "--threshold", "3", - "--shares", "5", + "--shards", "5", }, expectErr: false, }, { - name: "threshold exactly half of shares", + name: "threshold exactly half of shards", args: []string{ "--chain-id", testChainID, "--key-file", privValidatorKeyFile, "--threshold", "2", - "--shares", "4", + "--shards", "4", }, expectErr: true, }, { - name: "threshold less than half of shares", + name: "threshold less than half of shards", args: []string{ "--chain-id", testChainID, "--key-file", privValidatorKeyFile, "--threshold", "1", - "--shares", "3", + "--shards", "3", }, expectErr: true, }, { - name: "threshold exceeds shares", + name: "threshold exceeds shards", args: []string{ "--chain-id", testChainID, "--key-file", privValidatorKeyFile, "--threshold", "4", - "--shares", "3", + "--shards", "3", }, expectErr: true, }, { - name: "non-numeric threshold and shares", + name: "non-numeric threshold and shards", args: []string{ "--chain-id", testChainID, "--key-file", privValidatorKeyFile, "--threshold", "two", - "--shares", "three", + "--shards", "three", }, expectErr: true, }, @@ -92,7 +92,7 @@ func TestKey2Shares(t *testing.T) { cmd := rootCmd() cmd.SetOutput(io.Discard) - args := append([]string{"create-ed25519-shares", "--home", tmp, "--out", tmp}, tc.args...) + args := append([]string{"create-ed25519-shards", "--home", tmp, "--out", tmp}, tc.args...) cmd.SetArgs(args) err := cmd.Execute() if tc.expectErr { @@ -104,7 +104,7 @@ func TestKey2Shares(t *testing.T) { } } -func TestRSAShares(t *testing.T) { +func TestRSAShards(t *testing.T) { tmp := t.TempDir() tcs := []struct { @@ -113,13 +113,13 @@ func TestRSAShares(t *testing.T) { expectErr bool }{ { - name: "valid shares", - args: []string{"--shares", "3"}, + name: "valid shards", + args: []string{"--shards", "3"}, expectErr: false, }, { - name: "invalid shares", - args: []string{"--shares", "0"}, + name: "invalid shards", + args: []string{"--shards", "0"}, expectErr: true, }, } @@ -128,7 +128,7 @@ func TestRSAShares(t *testing.T) { t.Run(tc.name, func(t *testing.T) { cmd := rootCmd() cmd.SetOutput(io.Discard) - args := append([]string{"create-rsa-shares", "--home", tmp, "--out", tmp}, tc.args...) + args := append([]string{"create-rsa-shards", "--home", tmp, "--out", tmp}, tc.args...) cmd.SetArgs(args) err := cmd.Execute() if tc.expectErr { diff --git a/cmd/horcrux/cmd/signer.go b/cmd/horcrux/cmd/signer.go deleted file mode 100644 index a5366111..00000000 --- a/cmd/horcrux/cmd/signer.go +++ /dev/null @@ -1,84 +0,0 @@ -package cmd - -import ( - "fmt" - - cometlog "github.com/cometbft/cometbft/libs/log" - "github.com/spf13/cobra" - "github.com/strangelove-ventures/horcrux/signer" -) - -const ( - flagAcceptRisk = "accept-risk" - - singleSignerWarning = `@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -@ WARNING: SINGLE-SIGNER MODE SHOULD NOT BE USED FOR MAINNET! @ -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -Horcrux single-signer mode does not give the level of improved -key security and fault tolerance that Horcrux MPC/cosigner mode -provides. While it is a simpler deployment configuration, -single-signer should only be used for experimentation -as it is not officially supported by Strangelove.` -) - -func signerCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "signer", - Short: "Remote tx signer for TM based nodes.", - } - cmd.AddCommand(startSignerCmd()) - - return cmd -} - -func startSignerCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "start", - Short: "Start single signer process", - Args: cobra.NoArgs, - SilenceUsage: true, - RunE: func(cmd *cobra.Command, args []string) (err error) { - out := cmd.OutOrStdout() - - fmt.Fprintln(out, singleSignerWarning) - - acceptRisk, _ := cmd.Flags().GetBool(flagAcceptRisk) - if !acceptRisk { - panic(fmt.Errorf("risk not accepted. --accept-risk flag required to run single signer mode")) - } - - if err = signer.RequireNotRunning(config.PidFile); err != nil { - return err - } - - if err := config.Config.ValidateSingleSignerConfig(); err != nil { - return err - } - - logger := cometlog.NewTMLogger(cometlog.NewSyncWriter(out)).With("module", "validator") - - logger.Info( - "CometBFT Validator", - "mode", "single-signer", - "priv-state-dir", config.StateDir, - ) - - pv := signer.NewSingleSignerValidator(&config) - - go EnableDebugAndMetrics(cmd.Context(), out) - - services, err := signer.StartRemoteSigners(nil, logger, pv, config.Config.Nodes()) - if err != nil { - return fmt.Errorf("failed to start remote signer(s): %w", err) - } - - signer.WaitAndTerminate(logger, services, config.PidFile) - - return nil - }, - } - - cmd.Flags().Bool(flagAcceptRisk, false, "Single-signer-mode unsupported. Required to accept risk and proceed.") - - return cmd -} diff --git a/cmd/horcrux/cmd/single_signer.go b/cmd/horcrux/cmd/single_signer.go new file mode 100644 index 00000000..18b4ba1e --- /dev/null +++ b/cmd/horcrux/cmd/single_signer.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "fmt" + "io" + + "github.com/strangelove-ventures/horcrux/signer" +) + +const ( + flagAcceptRisk = "accept-risk" + + singleSignerWarning = `@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +@ WARNING: SINGLE-SIGNER MODE SHOULD NOT BE USED FOR MAINNET! @ +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +Horcrux single-signer mode does not give the level of improved +key security and fault tolerance that Horcrux MPC/cosigner mode +provides. While it is a simpler deployment configuration, +single-signer should only be used for experimentation +as it is not officially supported by Strangelove.` +) + +func NewSingleSignerValidator( + out io.Writer, + acceptRisk bool, +) (*signer.SingleSignerValidator, error) { + fmt.Fprintln(out, singleSignerWarning) + + if !acceptRisk { + panic(fmt.Errorf("risk not accepted. --accept-risk flag required to run single signer mode")) + } + + if err := config.Config.ValidateSingleSignerConfig(); err != nil { + return nil, err + } + + return signer.NewSingleSignerValidator(&config), nil +} diff --git a/cmd/horcrux/cmd/start.go b/cmd/horcrux/cmd/start.go new file mode 100644 index 00000000..f1f302c0 --- /dev/null +++ b/cmd/horcrux/cmd/start.go @@ -0,0 +1,74 @@ +package cmd + +import ( + "fmt" + + cometlog "github.com/cometbft/cometbft/libs/log" + "github.com/cometbft/cometbft/libs/service" + "github.com/spf13/cobra" + "github.com/strangelove-ventures/horcrux/signer" +) + +func startCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "start", + Short: "Start horcrux signer process", + Args: cobra.NoArgs, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + err := signer.RequireNotRunning(config.PidFile) + if err != nil { + return err + } + + out := cmd.OutOrStdout() + + if _, err := legacyConfig(); err == nil { + return fmt.Errorf("this is a legacy config. run `horcrux config migrate` to migrate to the latest format") + } + + logger := cometlog.NewTMLogger(cometlog.NewSyncWriter(out)).With("module", "validator") + + logger.Info( + "Horcrux Validator", + "mode", config.Config.SignMode, + "priv-state-dir", config.StateDir, + ) + + acceptRisk, _ := cmd.Flags().GetBool(flagAcceptRisk) + + var val signer.PrivValidator + var services []service.Service + + switch config.Config.SignMode { + case signer.SignModeThreshold: + services, val, err = NewThresholdValidator(logger) + if err != nil { + return err + } + case signer.SignModeSingle: + val, err = NewSingleSignerValidator(out, acceptRisk) + if err != nil { + return err + } + default: + panic(fmt.Errorf("unexpected sign mode: %s", config.Config.SignMode)) + } + + go EnableDebugAndMetrics(cmd.Context(), out) + + services, err = signer.StartRemoteSigners(services, logger, val, config.Config.Nodes()) + if err != nil { + return fmt.Errorf("failed to start remote signer(s): %w", err) + } + + signer.WaitAndTerminate(logger, services, config.PidFile) + + return nil + }, + } + + cmd.Flags().Bool(flagAcceptRisk, false, "Single-signer-mode unsupported. Required to accept risk and proceed.") + + return cmd +} diff --git a/cmd/horcrux/cmd/state.go b/cmd/horcrux/cmd/state.go index de00c025..f88e4ae6 100644 --- a/cmd/horcrux/cmd/state.go +++ b/cmd/horcrux/cmd/state.go @@ -4,6 +4,7 @@ import ( "bufio" "encoding/base64" "fmt" + "io" "os" "strconv" "strings" @@ -40,7 +41,7 @@ func showStateCmd() *cobra.Command { return &cobra.Command{ Use: "show [chain-id]", Aliases: []string{"s"}, - Short: "Show the priv validator and share sign state for a specific chain-id", + Short: "Show the sign state for a specific chain-id", Args: cobra.ExactArgs(1), SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { @@ -56,15 +57,16 @@ func showStateCmd() *cobra.Command { return err } - share, err := signer.LoadSignState(config.ShareStateFile(chainID)) + cs, err := signer.LoadSignState(config.CosignerStateFile(chainID)) if err != nil { return err } - fmt.Println("Private Validator State:") - printSignState(*pv) - fmt.Println("Share Sign State:") - printSignState(*share) + out := cmd.OutOrStdout() + fmt.Fprintln(out, "Private Validator State:") + printSignState(out, pv) + fmt.Fprintln(out, "Share Sign State:") + printSignState(out, cs) return nil }, } @@ -74,7 +76,7 @@ func setStateCmd() *cobra.Command { return &cobra.Command{ Use: "set [chain-id] [height]", Aliases: []string{"s"}, - Short: "Set the height for both the priv validator and the share sign state", + Short: "Set the height for the sign state of a specific chain-id", Args: cobra.ExactArgs(2), SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { @@ -96,7 +98,7 @@ func setStateCmd() *cobra.Command { return err } - share, err := signer.LoadOrCreateSignState(config.ShareStateFile(chainID)) + cs, err := signer.LoadOrCreateSignState(config.CosignerStateFile(chainID)) if err != nil { return err } @@ -109,7 +111,7 @@ func setStateCmd() *cobra.Command { fmt.Fprintf(cmd.OutOrStdout(), "Setting height %d\n", height) - pv.EphemeralPublic, share.EphemeralPublic = nil, nil + pv.EphemeralPublic, cs.EphemeralPublic = nil, nil signState := signer.SignStateConsensus{ Height: height, Round: 0, @@ -122,7 +124,7 @@ func setStateCmd() *cobra.Command { fmt.Printf("error saving privval sign state") return err } - err = share.Save(signState, nil) + err = cs.Save(signState, nil) if err != nil { fmt.Printf("error saving share sign state") return err @@ -161,7 +163,7 @@ func importStateCmd() *cobra.Command { } // shareStateFile does not exist during default config init, so create if necessary - share, err := signer.LoadOrCreateSignState(config.ShareStateFile(chainID)) + cs, err := signer.LoadOrCreateSignState(config.CosignerStateFile(chainID)) if err != nil { return err } @@ -212,7 +214,7 @@ func importStateCmd() *cobra.Command { fmt.Printf("error saving privval sign state") return err } - err = share.Save(signState, nil) + err = cs.Save(signState, nil) if err != nil { fmt.Printf("error saving share sign state") return err @@ -223,19 +225,19 @@ func importStateCmd() *cobra.Command { } } -func printSignState(ss signer.SignState) { - fmt.Printf(" Height: %v\n"+ +func printSignState(out io.Writer, ss *signer.SignState) { + fmt.Fprintf(out, " Height: %v\n"+ " Round: %v\n"+ " Step: %v\n", ss.Height, ss.Round, ss.Step) if ss.EphemeralPublic != nil { - fmt.Println(" Ephemeral Public Key:", base64.StdEncoding.EncodeToString(ss.EphemeralPublic)) + fmt.Fprintln(out, " Ephemeral Public Key:", base64.StdEncoding.EncodeToString(ss.EphemeralPublic)) } if ss.Signature != nil { - fmt.Println(" Signature:", base64.StdEncoding.EncodeToString(ss.Signature)) + fmt.Fprintln(out, " Signature:", base64.StdEncoding.EncodeToString(ss.Signature)) } if ss.SignBytes != nil { - fmt.Println(" SignBytes:", ss.SignBytes) + fmt.Fprintln(out, " SignBytes:", ss.SignBytes) } } diff --git a/cmd/horcrux/cmd/state_test.go b/cmd/horcrux/cmd/state_test.go index 60d62fb8..006b67cd 100644 --- a/cmd/horcrux/cmd/state_test.go +++ b/cmd/horcrux/cmd/state_test.go @@ -23,12 +23,9 @@ func TestStateSetCmd(t *testing.T) { cmd.SetArgs([]string{ "--home", tmpConfig, "config", "init", - "tcp://10.168.0.1:1234", - "-c", + "-n", "tcp://10.168.0.1:1234", "-t", "2", - "-p", "tcp://10.168.1.2:2222|2,tcp://10.168.1.3:2222|3", - "-l", "tcp://10.168.1.1:2222", - "--timeout", "1500ms", + "-c", "tcp://10.168.1.1:2222,tcp://10.168.1.2:2222,tcp://10.168.1.3:2222", }) err := cmd.Execute() require.NoError(t, err) diff --git a/cmd/horcrux/cmd/testdata/config-migrated.yaml b/cmd/horcrux/cmd/testdata/config-migrated.yaml index 74a148a7..1b4e610d 100644 --- a/cmd/horcrux/cmd/testdata/config-migrated.yaml +++ b/cmd/horcrux/cmd/testdata/config-migrated.yaml @@ -1,14 +1,16 @@ -cosigner: +signMode: threshold +thresholdMode: 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 + cosigners: + - shardID: 3 + p2pAddr: tcp://127.0.0.1:2224 + - shardID: 1 + p2pAddr: tcp://127.0.0.1:2222 + - shardID: 2 + p2pAddr: tcp://127.0.0.1:2223 + grpcTimeout: 1000ms + raftTimeout: 1000ms +chainNodes: +- privValAddr: tcp://127.0.0.1:1234 +- privValAddr: tcp://127.0.0.1:2345 +- privValAddr: tcp://127.0.0.1:3456 diff --git a/cmd/horcrux/cmd/testdata/config-v2.yaml b/cmd/horcrux/cmd/testdata/config-v2.yaml index 09fbfae8..57fdf00c 100644 --- a/cmd/horcrux/cmd/testdata/config-v2.yaml +++ b/cmd/horcrux/cmd/testdata/config-v2.yaml @@ -2,12 +2,12 @@ chain-id: test cosigner: threshold: 2 shares: 3 - p2p-listen: tcp://127.0.0.1:2222 + p2p-listen: tcp://127.0.0.1:2224 peers: + - share-id: 1 + p2p-addr: tcp://127.0.0.1:2222 - 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 diff --git a/cmd/horcrux/cmd/testdata/cosigner-key-migrated-ed25519.json b/cmd/horcrux/cmd/testdata/cosigner-key-migrated-ed25519.json index ec612235..ae8a9382 100644 --- a/cmd/horcrux/cmd/testdata/cosigner-key-migrated-ed25519.json +++ b/cmd/horcrux/cmd/testdata/cosigner-key-migrated-ed25519.json @@ -1 +1 @@ -{"pub_key":"CiBRLhKCqDU6Wufhj0TZK6jNO8LreArc+CKHKYitdTyYUg==","secret_share":"q0zuUQpplAfBJxoA2e2O1H7BdBgopzhPmL8mIWP8AQw=","id":3} \ No newline at end of file +{"pubKey":"CiBRLhKCqDU6Wufhj0TZK6jNO8LreArc+CKHKYitdTyYUg==","privateShard":"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 index 4b5c6732..17bf9860 100644 --- a/cmd/horcrux/cmd/testdata/cosigner-key-migrated-rsa.json +++ b/cmd/horcrux/cmd/testdata/cosigner-key-migrated-rsa.json @@ -1 +1 @@ -{"rsa_key":"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","rsa_pubs":["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"],"id":3} \ No newline at end of file +{"rsaKey":"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","rsaPubs":["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"],"id":3} \ No newline at end of file diff --git a/cmd/horcrux/cmd/testdata/testdata.go b/cmd/horcrux/cmd/testdata/testdata.go index 775c1734..2b7a0449 100644 --- a/cmd/horcrux/cmd/testdata/testdata.go +++ b/cmd/horcrux/cmd/testdata/testdata.go @@ -11,10 +11,10 @@ var ConfigMigrated string var ConfigV2 []byte //go:embed cosigner-key-migrated-ed25519.json -var CosignerKeyMigratedEd25519 string +var CosignerEd25519KeyMigrated string //go:embed cosigner-key-migrated-rsa.json -var CosignerKeyMigratedRSA string +var CosignerRSAKeyMigrated string //go:embed cosigner-key-v2.json var CosignerKeyV2 []byte diff --git a/cmd/horcrux/cmd/threshold.go b/cmd/horcrux/cmd/threshold.go new file mode 100644 index 00000000..de708b15 --- /dev/null +++ b/cmd/horcrux/cmd/threshold.go @@ -0,0 +1,99 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "time" + + cometlog "github.com/cometbft/cometbft/libs/log" + cometservice "github.com/cometbft/cometbft/libs/service" + "github.com/strangelove-ventures/horcrux/signer" +) + +func NewThresholdValidator( + logger cometlog.Logger, +) ([]cometservice.Service, *signer.ThresholdValidator, error) { + if err := config.Config.ValidateThresholdModeConfig(); err != nil { + return nil, nil, err + } + + keyFile, err := config.KeyFileExistsCosignerRSA() + if err != nil { + return nil, nil, err + } + + key, err := signer.LoadCosignerRSAKey(keyFile) + if err != nil { + return nil, nil, fmt.Errorf("error reading cosigner key (%s): %w", keyFile, err) + } + + thresholdCfg := config.Config.ThresholdModeConfig + + remoteCosigners := make([]signer.Cosigner, 0, len(thresholdCfg.Cosigners)-1) + pubKeys := make([]signer.CosignerRSAPubKey, len(thresholdCfg.Cosigners)) + + var p2pListen string + + for i, c := range thresholdCfg.Cosigners { + if c.ShardID != key.ID { + remoteCosigners = append( + remoteCosigners, + signer.NewRemoteCosigner(c.ShardID, c.P2PAddr), + ) + } else { + p2pListen = c.P2PAddr + } + + pubKeys[i] = signer.CosignerRSAPubKey{ + ID: c.ShardID, + PublicKey: *key.RSAPubs[c.ShardID-1], + } + } + + if p2pListen == "" { + return nil, nil, fmt.Errorf("cosigner config does not exist for our shard ID %d", key.ID) + } + + localCosigner := signer.NewLocalCosigner( + &config, + key, + pubKeys, + p2pListen, + uint8(thresholdCfg.Threshold), + ) + + // Validated prior in ValidateThresholdModeConfig + grpcTimeout, _ := time.ParseDuration(thresholdCfg.GRPCTimeout) + raftTimeout, _ := time.ParseDuration(thresholdCfg.RaftTimeout) + + raftDir := filepath.Join(config.HomeDir, "raft") + if err := os.MkdirAll(raftDir, 0700); err != nil { + return nil, nil, fmt.Errorf("error creating raft directory: %w", err) + } + + // RAFT node ID is the cosigner ID + nodeID := fmt.Sprint(key.ID) + + // Start RAFT store listener + raftStore := signer.NewRaftStore(nodeID, + raftDir, p2pListen, raftTimeout, logger, localCosigner, remoteCosigners) + if err := raftStore.Start(); err != nil { + return nil, nil, fmt.Errorf("error starting raft store: %w", err) + } + services := []cometservice.Service{raftStore} + + val := signer.NewThresholdValidator( + logger, + &config, + thresholdCfg.Threshold, + grpcTimeout, + localCosigner, + remoteCosigners, + raftStore, + ) + + raftStore.SetThresholdValidator(val) + + return services, val, nil +} diff --git a/docs/horcrux.service b/docs/horcrux.service index f5428390..43ffd830 100644 --- a/docs/horcrux.service +++ b/docs/horcrux.service @@ -6,7 +6,7 @@ After=network.target Type=simple User=ubuntu WorkingDirectory=/home/ubuntu -ExecStart=/usr/bin/horcrux cosigner start +ExecStart=/usr/bin/horcrux start Restart=on-failure RestartSec=3 LimitNOFILE=4096 diff --git a/docs/metrics.md b/docs/metrics.md index a7090fc3..5c28725b 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -13,20 +13,20 @@ debug-addr: 0.0.0.0:6001 Resulting in a configuration like the following: ``` -chain-id: testnet-1 -cosigner: +thresholdMode: threshold: 2 - shares: 3 - p2p-listen: tcp://localhost:5001 - peers: - - share-id: 2 - p2p-addr: tcp://localhost:5002 - - share-id: 3 - p2p-addr: tcp://localhost:5003 - rpc-timeout: 1500ms -chain-nodes: -- priv-val-addr: tcp://localhost:2300 -debug-addr: 0.0.0.0:6001 + cosigners: + - shardID: 1 + p2pAddr: tcp://localhost:5001 + - shardID: 2 + p2pAddr: tcp://localhost:5002 + - shardID: 3 + p2pAddr: tcp://localhost:5003 + grpcTimeout: 1500ms + raftTimeout: 1500ms +chainNodes: +- privValAddr: tcp://localhost:2300 +debugAddr: 0.0.0.0:6001 ``` ## Prometheus Cautions diff --git a/docs/migrating.md b/docs/migrating.md index 591e58c0..d845776a 100644 --- a/docs/migrating.md +++ b/docs/migrating.md @@ -63,35 +63,29 @@ $ sudo nano /etc/systemd/system/horcrux.service $ sudo systemctl daemon-reload ``` -After that is done, initialize the configuration for each node using the `horcrux` cli. Each node will require a slightly different command. Below are the commands for each of the 3 signer nodes given the private IPs above. Input your own data here: +After that is done, initialize the configuration for the cosigners using the `horcrux` cli. If you would like different cosigners to connect to different sentry node(s), modify the `--node` flag values for each cosigner. ```bash -# Run this command on the signer-1 VM -# signer-1 connects to sentry-1 -$ 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 to generate a config file that can be used on all config nodes. -# Run this command on the signer-2 VM -# signer-2 connects to sentry-2 -$ 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 "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 +$ horcrux config init --node "tcp://10.168.0.1:1234" --node "tcp://10.168.0.2:1234" --node "tcp://10.168.0.3:1234" --cosigner "tcp://10.168.1.1:2222" --cosigner "tcp://10.168.1.2:2222" --cosigner "tcp://10.168.1.3:2222" --threshold 2 --grpc-timeout 1000ms --raft-timeout 1000ms ``` > **Note** -> Note the node address (e.g. "tcp://10.168.0.1:1234") of each command. In this example, each horcrux node is communicating with a corresponding sentry. It is also possible to include a comma separated list of node addresses (e.g. "tcp://chain-node-1:1234,tcp://chain-node-2:1234", etc), allowing all horcrux nodes to communicate with all sentries. - -> **Warning** -> SINGLE-SIGNER MODE SHOULD NOT BE USED FOR MAINNET! Horcrux single-signer mode does not give the level of improved key security and fault tolerance that Horcrux MPC/cosigner mode provides. While it is a simpler deployment configuration, single-signer should only be used for experimentation as it is not officially supported by Strangelove. +> Note the use of multiple `--node` and `--cosigner` flags. In this example, there are 3 sentry(chain) nodes that each horcrux cosigner will connect to. There are 3 horcrux cosigners, with a threshold of 2 cosigners required to sign a valid block signature. #### Flags -- `-c`/`--cosigner`: this flag instructs horcrux to configure the signer for MPC cosigner operations. This is the officially-supported configuration. The signer can also be run in single signer configuration for experimental, non-mainnet deployments. To enable single-signer mode, exclude the `-c`, `-p`, `-t`, and `--timeout` flags. -- `-p`/`--peers`: configures the addresses of the other signer nodes in the config. Two ports are required, the P2P port for RCP traffic, and the Raft port for key-value sharing. Note that each signer also has an index. This index corresponds to the shard of the private key it will sign with. Keeping the node names and the indexes the same helps avoid errors and allows you to work more quickly -- `-l`/`--listen`: configures the listen address for the cosigner, which is used for communication between cosigners, Raft and GRPC. The DNS/IP used for this must be reachable by the other peers, i.e. do not use 0.0.0.0 for the hostname. -- `-k`/`--keyfile`: configures the file path for the private key share file if you would like to use a different path than the default, `~/.horcrux/share.json`. -- `--timeout`: configures the timeout for cosigner-to-cosigner GRPC communication. This value defaults to `1000ms`. If you are running in disconnected data centers (i.e. across amazon AZs or gcp zones) increasing the timeout slightly helps to avoid missed blocks especially around proposals. +- `-c`/`--cosigner`: configures the P2P address and shard ID for cosigner nodes. Keeping the node names and the IDs the same helps avoid errors. The DNS/IP used for all of these must be reachable by the other cosigners, i.e. do not use 0.0.0.0 for the hostname. +- `-n`/`--node`: configures the priv-val interface listen address for the chain sentry nodes. +- `-k`/`--key-dir`: configures the directory for the RSA and Ed25519 private key files if you would like to use a different path than the default, `~/.horcrux`. +- `--grpc-timeout`: configures the timeout for cosigner-to-cosigner GRPC communication. This value defaults to `1000ms`. +- `--raft-timeout`: configures the timeout for cosigner-to-cosigner Raft consensus. This value defaults to `1000ms`. +- `-m`/`--mode`: this flag allows changing the sign mode. By default, horcrux uses `threshold` mode for MPC cosigner operations. This is the officially-supported configuration. The signer can also be run in single signer configuration for experimental, non-mainnet deployments. To enable single-signer mode, use `single` for this flag, exclude the `-c`, `-t`, `--grpc-timeout`, and `--raft-timeout` flags, and pass the `--accept-risk` flag to accept the elevated risk of running in single signer mode. + +> **Warning** +> SINGLE-SIGNER MODE SHOULD NOT BE USED FOR MAINNET! Horcrux single-signer mode does not give the level of improved key security and fault tolerance that Horcrux MPC/cosigner mode provides. While it is a simpler deployment configuration, single-signer should only be used for experimentation as it is not officially supported by Strangelove. ### 3. Split `priv_validator_key.json` and distribute key material @@ -103,35 +97,35 @@ On some computer that contains your `priv_validator_key.json` create a folder to $ ls priv_validator_key.json -$ 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 +$ horcrux create-ed25519-shards --chain-id cosmoshub-4 --key-file priv_validator_key.json --threshold 2 --shards 3 +Created Ed25519 Shard cosigner_1/cosmoshub-4_shard.json +Created Ed25519 Shard cosigner_2/cosmoshub-4_shard.json +Created Ed25519 Shard cosigner_3/cosmoshub-4_shard.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 +$ horcrux create-rsa-shards --shards 3 +Created RSA Shard cosigner_1/rsa_keys.json +Created RSA Shard cosigner_2/rsa_keys.json +Created RSA Shard 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 +cosmoshub-4_shard.json rsa_keys.json ./cosigner_2: -cosmoshub-4_share.json rsa_keys.json +cosmoshub-4_shard.json rsa_keys.json ./cosigner_3: -cosmoshub-4_share.json rsa_keys.json +cosmoshub-4_shard.json rsa_keys.json ``` 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 should have a `~/.horcrux/{chain-id}_shard.json` file with the contents matching the appropriate `cosigner_{id}/{chain-id}_shard.json` file corresponding to the node number. Additionally, each of your horcrux nodes should have a `~/.horcrux/rsa_keys.json` file with the contents matching the appropriate `cosigner_{id}/rsa_keys.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. +If you will be signing for multiple chains with this single horcrux cluster, repeat the `horcrux create-ed25519-shards` 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 @@ -153,7 +147,7 @@ Once the validator has been stopped, you will need the contents of the `$NODE_HO } ``` -You will need to replace the contents of the `~/.horcrux/state/{chain-id}_priv_validator_state.json` and `~/.horcrux/state/{chain-id}_share_sign_state.json` on each signer node with a truncated and slightly modified version of the file. Note the `""` especially on the `"round"` value: +You will need to replace the contents of the `~/.horcrux/state/{chain-id}_priv_validator_state.json` on each signer node with a truncated and slightly modified version of the file. Note the `""` especially on the `"round"` value: ```json { @@ -163,7 +157,7 @@ You will need to replace the contents of the `~/.horcrux/state/{chain-id}_priv_v } ``` -> **NOTE:** This step can be error prone. We will be [adding a feature](https://github.com/strangelove-ventures/horcrux/issues/18) to allow using the CLI to set these values but for now `nano`/`vi`, `cat` and [`jq`](https://stedolan.github.io/jq/) are your friends. +`horcrux state import` can be used to import an existing `priv_validator_state.json` ### 5. Start the signer cluster @@ -188,7 +182,7 @@ I[2021-09-24|02:10:09.027] Retrying module=v The signer will continue retrying attempts to reach the sentries until we turn the sentry `priv_validator` listener on in the next step. Any panic causing errors are likely due to one of the two following issues: - Misnaming or incorrect structure of the files in `~/.horcrux/state`. Double check these if you see errors -- Misnaming or misplacement of the `~/.horcrux/share.json` file +- Misnaming or misplacement of the `~/.horcrux/{chain-id}_shard.json` file > **NOTE:** leaving these logs streaming in seperate terminal windows will enable you to watch the cluster connect to the sentries. @@ -219,6 +213,6 @@ You now can sleep much better at night because you are much less likely to have ### 8. Administration Commands -`horcrux elect` - Elect a new cluster leader. Pass an optional argument with the intended leader ID to elect that cosigner as the new leader, e.g. `horcrux elect 3` to elect cosigner with `ID: 3` as leader +`horcrux elect` - Elect a new cluster leader. Pass an optional argument with the intended leader ID to elect that cosigner as the new leader, e.g. `horcrux elect 3` to elect cosigner with `shardID: 3` as leader. This is an optimistic leader election, it is not guaranteed that the exact requested leader will be elected. -`horcrux cosigner address` - Get the public key address as both hex and optionally the validator consensus bech32 address. To retrieve the valcons bech32 address, pass an optional argument with the chain's bech32 valcons prefix, e.g. `horcrux cosigner address cosmosvalcons` +`horcrux address` - Get the public key address as both hex and optionally the validator consensus bech32 address. To retrieve the valcons bech32 address, pass an optional argument with the chain's bech32 prefix, e.g. `horcrux address cosmos` diff --git a/docs/signing.md b/docs/signing.md index f8595875..20eb55be 100644 --- a/docs/signing.md +++ b/docs/signing.md @@ -34,11 +34,11 @@ Each block sign request (votes and proposals) from any connected sentry node(s), The signer node that is the current elected raft leader will act upon the sign requests by managing the threshold validation process: - Check the requested block against the high watermark file (kept in consensus between the signer nodes) to avoid double signing. -- Request ephemeral nonces for the block signature from each signer node peer. +- Request ephemeral nonces for the block signature from each cosigner node. - Each signer will act upon the request by generating the ephemeral nonce shares for all other signers (encrypted with the destination signer's RSA public key). These shares will be the response to the leader. - The leader will wait until it has received _`t - 1`_ responses. The signer nodes which responded in time, _`blockSigners`_ are the signers that will be included with the leader for signing the block. - The leader will then make a request to each of the _`blockSigners`_ to set the ephemeral nonces for the other signers that are participating in the block signing (_`blockSigners`_ and leader), and produce the signature part from the block data. -- The participant in _`blockSigners`_ will handle this request by decrypting the ephemeral shares with its RSA private key, verify the signatures of the ephemeral share to verify the identity of the source signers, and then save it in memory. After all of the nonces are saved (consensus with the leader and _`blockSigners`_), it will produce its signature piece for the block data and respond to the leader with it. +- The participant in _`blockSigners`_ will handle this request by decrypting the ephemeral shares with its RSA private key, verify the signatures of the ephemeral share to verify the identity of the source signers, and then save it in memory. After all of the nonces are saved (consensus with the leader and _`blockSigners`_), it will sign the block data with it's Ed25519 key shard, and respond with to the leader with its signature piece. - Once the leader receives the signature parts from all of the _`blockSigners`_, it will make a combined signature including its own signature part and those from the _`blockSigners`_ - The leader will verify the combined signature is valid, then update its own high watermark file and also emit the block metadata (height, round, and step), to the rest of the signers through raft in order to update their high watermark files. This gives the cluster consensus on what the last successfully signed block was. - The leader will finally respond with the combined signature for the block, either directly to the requesting sentry if the raft leader was the one who handled the sentry request, or the signer that proxied the request to the leader, which would then respond to the requesting sentry. diff --git a/scripts/spin-up.bash b/scripts/spin-up.bash deleted file mode 100755 index f168f615..00000000 --- a/scripts/spin-up.bash +++ /dev/null @@ -1,200 +0,0 @@ -#!/bin/sh -# USAGE: ./one-chain - -CHAINID=test-chain-id -CHAINDIR=./data -HORCRUX=../build/horcrux - -function setConfigHorcrux() { - cfgdir=$1 - rpc=$2 - p2p=$3 - grpc=$4 - grpcweb=$5 - profile=$6 - privval=$7 - sed -i '' "s#127.0.0.1:26657#0.0.0.0:$rpc#g" $cfgdir/config.toml - sed -i '' "s#0.0.0.0:26656#0.0.0.0:$p2p#g" $cfgdir/config.toml - sed -i '' "s#0.0.0.0:9090#0.0.0.0:$grpc#g" $cfgdir/app.toml - sed -i '' "s#0.0.0.0:9091#0.0.0.0:$grpcweb#g" $cfgdir/app.toml - sed -i '' "s#localhost:6060#localhost:$profile#g" $cfgdir/config.toml - sed -i '' 's/timeout_commit = "5s"/timeout_commit = "3s"/g' $cfgdir/config.toml - sed -i '' 's/timeout_propose = "3s"/timeout_propose = "3s"/g' $cfgdir/config.toml - sed -i '' "s#priv_validator_laddr = \"\"#priv_validator_laddr = \"tcp://0.0.0.0:$privval\"#g" $cfgdir/config.toml - sed -i '' 's#allow_duplicate_ip = false#allow_duplicate_ip = true#g' $cfgdir/config.toml - sed -i '' 's#log_level = "main:info,state:info,statesync:info,*:error"#log_level = "info"#g' $cfgdir/config.toml - sed -i '' 's#addr_book_strict = true#addr_book_strict = false#g' $cfgdir/config.toml - sed -i '' "s#external_address = \"\"#external_address = \"tcp://127.0.0.1:$p2p\"#g" $cfgdir/config.toml -} - -function setConfigValidator() { - cfgdir=$1 - rpc=$2 - p2p=$3 - grpc=$4 - grpcweb=$5 - profile=$6 - sed -i '' "s#127.0.0.1:26657#0.0.0.0:$rpc#g" $cfgdir/config.toml - sed -i '' "s#0.0.0.0:26656#0.0.0.0:$p2p#g" $cfgdir/config.toml - sed -i '' "s#0.0.0.0:9090#0.0.0.0:$grpc#g" $cfgdir/app.toml - sed -i '' "s#0.0.0.0:9091#0.0.0.0:$grpcweb#g" $cfgdir/app.toml - sed -i '' "s#localhost:6060#localhost:$profile#g" $cfgdir/config.toml - sed -i '' 's/timeout_commit = "5s"/timeout_commit = "3s"/g' $cfgdir/config.toml - sed -i '' 's/timeout_propose = "3s"/timeout_propose = "3s"/g' $cfgdir/config.toml - sed -i '' 's#allow_duplicate_ip = false#allow_duplicate_ip = true#g' $cfgdir/config.toml - sed -i '' 's#log_level = "main:info,state:info,statesync:info,*:error"#log_level = "info"#g' $cfgdir/config.toml - sed -i '' 's#addr_book_strict = true#addr_book_strict = false#g' $cfgdir/config.toml - sed -i '' "s#external_address = \"\"#external_address = \"tcp://127.0.0.1:$p2p\"#g" $cfgdir/config.toml -} - -# echo "Creating gaiad instance with home=$CHAINDIR chain-id=$CHAINID..." -# Build genesis file incl account for passed address -coins="100000000000stake,100000000000samoleans" - -# Have different home folders for each node -# Nodes for horcrux -n0dir="$CHAINDIR/$CHAINID/n0" -n1dir="$CHAINDIR/$CHAINID/n1" -n2dir="$CHAINDIR/$CHAINID/n2" -# Other validators -n3dir="$CHAINDIR/$CHAINID/n3" -n4dir="$CHAINDIR/$CHAINID/n4" -n5dir="$CHAINDIR/$CHAINID/n5" - -# Nodes for horcrux -home0="--home $n0dir" -home1="--home $n1dir" -home2="--home $n2dir" -# Other validators -home3="--home $n3dir" -home4="--home $n4dir" -home5="--home $n5dir" - -# Nodes for horcrux -n0cfgDir="$n0dir/config" -n1cfgDir="$n1dir/config" -n2cfgDir="$n2dir/config" -# Other validators -n3cfgDir="$n3dir/config" -n4cfgDir="$n4dir/config" -n5cfgDir="$n5dir/config" - -# Nodes for horcrux -n0cfg="$n0cfgDir/config.toml" -n1cfg="$n1cfgDir/config.toml" -n2cfg="$n2cfgDir/config.toml" -# Other validators -n3cfg="$n3cfgDir/config.toml" -n4cfg="$n4cfgDir/config.toml" -n5cfg="$n5cfgDir/config.toml" -kbt="--keyring-backend="test"" - -# Initialize the 2 home directories -gaiad $home0 --chain-id $CHAINID init n0 &>/dev/null -gaiad $home1 --chain-id $CHAINID init n1 &>/dev/null -gaiad $home2 --chain-id $CHAINID init n2 &>/dev/null -gaiad $home3 --chain-id $CHAINID init n3 &>/dev/null -gaiad $home4 --chain-id $CHAINID init n4 &>/dev/null -gaiad $home5 --chain-id $CHAINID init n5 &>/dev/null - -# Add some keys for funds -# Add horcrux validator key -gaiad $home0 keys add validator $kbt &>/dev/null -# Add other validator keys -gaiad $home3 keys add validator $kbt &>/dev/null -gaiad $home4 keys add validator $kbt &>/dev/null -gaiad $home5 keys add validator $kbt &>/dev/null -# have a key with some extra funds -gaiad $home0 keys add extra $kbt &>/dev/null - -# Add addresses to genesis -# Add all validator addresses keys to node0 genesis -gaiad $home0 add-genesis-account $(gaiad $home0 keys $kbt show validator -a) $coins &>/dev/null -gaiad $home0 add-genesis-account $(gaiad $home3 keys $kbt show validator -a) $coins &>/dev/null -gaiad $home0 add-genesis-account $(gaiad $home4 keys $kbt show validator -a) $coins &>/dev/null -gaiad $home0 add-genesis-account $(gaiad $home5 keys $kbt show validator -a) $coins &>/dev/null -# Add extra funds key to node0 genesis -gaiad $home0 add-genesis-account $(gaiad $home0 keys $kbt show extra -a) $coins &>/dev/null -# Add validator addresses for gentxs to other validators home folders -gaiad $home3 add-genesis-account $(gaiad $home3 keys $kbt show validator -a) $coins &>/dev/null -gaiad $home4 add-genesis-account $(gaiad $home4 keys $kbt show validator -a) $coins &>/dev/null -gaiad $home5 add-genesis-account $(gaiad $home5 keys $kbt show validator -a) $coins &>/dev/null - -# Gentxs for each node -gaiad $home0 gentx validator 100000000000stake $kbt --chain-id $CHAINID &>/dev/null -gaiad $home3 gentx validator 100000000000stake $kbt --chain-id $CHAINID &>/dev/null -gaiad $home4 gentx validator 100000000000stake $kbt --chain-id $CHAINID &>/dev/null -gaiad $home5 gentx validator 100000000000stake $kbt --chain-id $CHAINID &>/dev/null -# Move gentxs to node0 config dir -mv $n3cfgDir/gentx/* $n0cfgDir/gentx &>/dev/null -mv $n4cfgDir/gentx/* $n0cfgDir/gentx &>/dev/null -mv $n5cfgDir/gentx/* $n0cfgDir/gentx &>/dev/null - -# finalize genesis -gaiad $home0 collect-gentxs &>/dev/null - -# Copy genesis over to other nodes -cp $n0cfgDir/genesis.json $n1cfgDir/genesis.json -cp $n0cfgDir/genesis.json $n2cfgDir/genesis.json -cp $n0cfgDir/genesis.json $n3cfgDir/genesis.json -cp $n0cfgDir/genesis.json $n4cfgDir/genesis.json -cp $n0cfgDir/genesis.json $n5cfgDir/genesis.json - -# Set proper defaults and change ports on horcrux nodes -setConfigHorcrux $n0cfgDir 26657 26656 9090 9091 6060 1234 -setConfigHorcrux $n1cfgDir 26658 26655 9092 9093 6061 1235 -setConfigHorcrux $n2cfgDir 26659 26654 9094 9095 6062 1236 -# Set proper defaults and change ports on validator nodes -setConfigValidator $n3cfgDir 26660 26653 9096 9097 6062 -setConfigValidator $n4cfgDir 26661 26652 9098 9099 6063 -setConfigValidator $n5cfgDir 26662 26651 9100 9101 6064 - -# Set peers for all nodes -peer0="$(gaiad $home0 tendermint show-node-id)@127.0.0.1:26656" -peer1="$(gaiad $home1 tendermint show-node-id)@127.0.0.1:26655" -peer2="$(gaiad $home2 tendermint show-node-id)@127.0.0.1:26654" -peer3="$(gaiad $home3 tendermint show-node-id)@127.0.0.1:26653" -peer4="$(gaiad $home4 tendermint show-node-id)@127.0.0.1:26652" -peer5="$(gaiad $home5 tendermint show-node-id)@127.0.0.1:26651" -sed -i '' 's#persistent_peers = ""#persistent_peers = "'$peer1','$peer2','$peer3','$peer4','$peer5'"#g' $n0cfg -sed -i '' 's#persistent_peers = ""#persistent_peers = "'$peer0','$peer2','$peer3','$peer4','$peer5'"#g' $n1cfg -sed -i '' 's#persistent_peers = ""#persistent_peers = "'$peer0','$peer1','$peer3','$peer4','$peer5'"#g' $n2cfg -sed -i '' 's#persistent_peers = ""#persistent_peers = "'$peer0','$peer1','$peer2','$peer4','$peer5'"#g' $n3cfg -sed -i '' 's#persistent_peers = ""#persistent_peers = "'$peer0','$peer1','$peer2','$peer3','$peer5'"#g' $n4cfg -sed -i '' 's#persistent_peers = ""#persistent_peers = "'$peer0','$peer1','$peer2','$peer3','$peer4'"#g' $n5cfg - -# Copy priv validator over from node that signed gentx to the signer -cp $n0cfgDir/priv_validator_key.json $CHAINDIR/priv_validator_key.json -cd $CHAINDIR -$HORCRUX create-shares ./priv_validator_key.json 2 3 -$HORCRUX config init $CHAINID localhost:1234 --home $(pwd)/signer1 --cosigner --peers "tcp://localhost:2223|2,tcp://localhost:2224|3" --threshold 2 --listen "tcp://0.0.0.0:2222" -$HORCRUX config init $CHAINID localhost:1235 --home $(pwd)/signer2 --cosigner --peers "tcp://localhost:2222|1,tcp://localhost:2224|3" --threshold 2 --listen "tcp://0.0.0.0:2223" -$HORCRUX config init $CHAINID localhost:1236 --home $(pwd)/signer3 --cosigner --peers "tcp://localhost:2222|1,tcp://localhost:2223|2" --threshold 2 --listen "tcp://0.0.0.0:2224" -cp ./private_share_1.json ./signer1/share.json -cp ./private_share_2.json ./signer2/share.json -cp ./private_share_3.json ./signer3/share.json -cd .. - -# Start the gaia instances -./build/horcrux --home ./data/signer1 cosigner start > $CHAINDIR/signer1.log 2>&1 & -./build/horcrux --home ./data/signer2 cosigner start > $CHAINDIR/signer2.log 2>&1 & -./build/horcrux --home ./data/signer3 cosigner start > $CHAINDIR/signer3.log 2>&1 & -sleep 5 -gaiad $home0 start --pruning=nothing > $CHAINDIR/$CHAINID.n0.log 2>&1 & -gaiad $home1 start --pruning=nothing > $CHAINDIR/$CHAINID.n1.log 2>&1 & -gaiad $home2 start --pruning=nothing > $CHAINDIR/$CHAINID.n2.log 2>&1 & -gaiad $home3 start --pruning=nothing > $CHAINDIR/$CHAINID.n3.log 2>&1 & -gaiad $home4 start --pruning=nothing > $CHAINDIR/$CHAINID.n4.log 2>&1 & -gaiad $home5 start --pruning=nothing > $CHAINDIR/$CHAINID.n5.log 2>&1 & - -echo -echo "Logs:" -echo " - n0 'tail -f ./data/signer1.log'" -echo " - n1 'tail -f ./data/signer2.log'" -echo " - n2 'tail -f ./data/signer3.log'" -echo " - f0 'tail -f ./data/test-chain-id.n0.log'" -echo " - f1 'tail -f ./data/test-chain-id.n1.log'" -echo " - f1 'tail -f ./data/test-chain-id.n2.log'" -echo " - f1 'tail -f ./data/test-chain-id.n3.log'" -echo " - f1 'tail -f ./data/test-chain-id.n4.log'" -echo " - f1 'tail -f ./data/test-chain-id.n5.log'" diff --git a/signer/config.go b/signer/config.go index b9744ded..9e57660b 100644 --- a/signer/config.go +++ b/signer/config.go @@ -3,11 +3,10 @@ package signer import ( "errors" "fmt" + "net" "net/url" "os" "path/filepath" - "strconv" - "strings" "time" "github.com/cometbft/cometbft/crypto" @@ -22,16 +21,20 @@ import ( "gopkg.in/yaml.v2" ) -type NodeConfig struct { - Address string -} +type SignMode string + +const ( + SignModeThreshold SignMode = "threshold" + SignModeSingle SignMode = "single" +) -// Config maps to the on-disk JSON format +// Config maps to the on-disk yaml format type Config struct { - 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"` + PrivValKeyDir *string `yaml:"keyDir,omitempty"` + SignMode SignMode `yaml:"signMode"` + ThresholdModeConfig *ThresholdModeConfig `yaml:"thresholdMode,omitempty"` + ChainNodes ChainNodes `yaml:"chainNodes,omitempty"` + DebugAddr string `yaml:"debugAddr,omitempty"` } func (c *Config) Nodes() (out []string) { @@ -52,7 +55,7 @@ func (c *Config) MustMarshalYaml() []byte { func (c *Config) ValidateSingleSignerConfig() error { var errs []error if len(c.ChainNodes) == 0 { - errs = append(errs, fmt.Errorf("need to have chain-nodes configured for priv-val connection")) + errs = append(errs, fmt.Errorf("need to have chainNodes configured for priv-val connection")) } if err := c.ChainNodes.Validate(); err != nil { errs = append(errs, err) @@ -60,35 +63,43 @@ func (c *Config) ValidateSingleSignerConfig() error { return errors.Join(errs...) } -func (c *Config) ValidateCosignerConfig() error { +func (c *Config) ValidateThresholdModeConfig() error { var errs []error + if err := c.ValidateSingleSignerConfig(); err != nil { errs = append(errs, err) } - if c.CosignerConfig == nil { + + if c.ThresholdModeConfig == nil { errs = append(errs, fmt.Errorf("cosigner config can't be empty")) - // the rest of the checks depend on non-nil c.CosignerConfig + // the rest of the checks depend on non-nil c.ThresholdModeConfig return errors.Join(errs...) } - if c.CosignerConfig.Threshold <= c.CosignerConfig.Shares/2 { - errs = append(errs, fmt.Errorf("threshold (%d) must be greater than number of shares (%d) / 2", - c.CosignerConfig.Threshold, c.CosignerConfig.Shares)) + + numShards := len(c.ThresholdModeConfig.Cosigners) + + if c.ThresholdModeConfig.Threshold <= numShards/2 { + errs = append(errs, fmt.Errorf("threshold (%d) must be greater than number of shards (%d) / 2", + c.ThresholdModeConfig.Threshold, numShards)) } - if c.CosignerConfig.Shares < c.CosignerConfig.Threshold { - errs = append(errs, fmt.Errorf("number of shares (%d) must be greater or equal to threshold (%d)", - c.CosignerConfig.Shares, c.CosignerConfig.Threshold)) + + if numShards < c.ThresholdModeConfig.Threshold { + errs = append(errs, fmt.Errorf("number of shards (%d) must be greater or equal to threshold (%d)", + numShards, c.ThresholdModeConfig.Threshold)) } - _, err := time.ParseDuration(c.CosignerConfig.Timeout) - if err != nil { - errs = append(errs, fmt.Errorf("invalid --timeout: %w", err)) + if _, err := time.ParseDuration(c.ThresholdModeConfig.RaftTimeout); err != nil { + errs = append(errs, fmt.Errorf("invalid raftTimeout: %w", err)) } - if _, err := url.Parse(c.CosignerConfig.P2PListen); err != nil { - errs = append(errs, fmt.Errorf("failed to parse p2p listen address: %w", err)) + + if _, err := time.ParseDuration(c.ThresholdModeConfig.GRPCTimeout); err != nil { + errs = append(errs, fmt.Errorf("invalid grpcTimeout: %w", err)) } - if err := c.CosignerConfig.Peers.Validate(c.CosignerConfig.Shares); err != nil { + + if err := c.ThresholdModeConfig.Cosigners.Validate(); err != nil { errs = append(errs, err) } + return errors.Join(errs...) } @@ -120,7 +131,7 @@ func (c RuntimeConfig) KeyFilePathCosigner(chainID string) string { if kd := c.cachedKeyDirectory(); kd != "" { keyDir = kd } - return filepath.Join(keyDir, fmt.Sprintf("%s_share.json", chainID)) + return filepath.Join(keyDir, fmt.Sprintf("%s_shard.json", chainID)) } func (c RuntimeConfig) KeyFilePathCosignerRSA() string { @@ -135,7 +146,7 @@ func (c RuntimeConfig) PrivValStateFile(chainID string) string { return filepath.Join(c.StateDir, fmt.Sprintf("%s_priv_validator_state.json", chainID)) } -func (c RuntimeConfig) ShareStateFile(chainID string) string { +func (c RuntimeConfig) CosignerStateFile(chainID string) string { return filepath.Join(c.StateDir, fmt.Sprintf("%s_share_sign_state.json", chainID)) } @@ -173,64 +184,82 @@ func (c RuntimeConfig) KeyFileExistsCosignerRSA() (string, error) { return keyFile, fileExists(keyFile) } -type CosignerConfig struct { - Threshold int `json:"threshold" yaml:"threshold"` - Shares int `json:"shares" yaml:"shares"` - P2PListen string `json:"p2p-listen" yaml:"p2p-listen"` - Peers CosignerPeersConfig `json:"peers" yaml:"peers"` - Timeout string `json:"rpc-timeout" yaml:"rpc-timeout"` +// ThresholdModeConfig is the on disk config format for threshold sign mode. +type ThresholdModeConfig struct { + Threshold int `yaml:"threshold"` + Cosigners CosignersConfig `yaml:"cosigners"` + GRPCTimeout string `yaml:"grpcTimeout"` + RaftTimeout string `yaml:"raftTimeout"` } -func (cfg *CosignerConfig) LeaderElectMultiAddress() (string, error) { - addresses := make([]string, 1+len(cfg.Peers)) - addresses[0] = cfg.P2PListen - for i, peer := range cfg.Peers { - addresses[i+1] = peer.P2PAddr +func (cfg *ThresholdModeConfig) LeaderElectMultiAddress() (string, error) { + addresses := make([]string, len(cfg.Cosigners)) + for i, c := range cfg.Cosigners { + addresses[i] = c.P2PAddr } return client.MultiAddress(addresses) } -type CosignerPeerConfig struct { - ShareID int `json:"share-id" yaml:"share-id"` - P2PAddr string `json:"p2p-addr" yaml:"p2p-addr"` +// CosignerConfig is the on disk format representing a cosigner for threshold sign mode. +type CosignerConfig struct { + ShardID int `yaml:"shardID"` + P2PAddr string `yaml:"p2pAddr"` } -type CosignerPeersConfig []CosignerPeerConfig +type CosignersConfig []CosignerConfig -func (peers CosignerPeersConfig) Validate(shares int) error { +func (cosigners CosignersConfig) Validate() error { + var errs []error // Check IDs to make sure none are duplicated - if dupl := duplicatePeers(peers); len(dupl) != 0 { - return fmt.Errorf("found duplicate share IDs in args: %v", dupl) + if dupl := duplicateCosigners(cosigners); len(dupl) != 0 { + errs = append(errs, fmt.Errorf("found duplicate cosigner shard ID(s) in args: %v", dupl)) } - // Make sure that the peers' IDs match the number of shares. - for _, peer := range peers { - if peer.ShareID < 1 || peer.ShareID > shares { - return fmt.Errorf("peer ID %v in args is out of range, must be between 1 and %v, inclusive", - peer.ShareID, shares) + shards := len(cosigners) + + // Make sure that the cosigner IDs match the number of cosigners. + for _, cosigner := range cosigners { + if cosigner.ShardID < 1 || cosigner.ShardID > shards { + errs = append(errs, fmt.Errorf("cosigner shard ID %d in args is out of range, must be between 1 and %d, inclusive", + cosigner.ShardID, shards)) + } + + url, err := url.Parse(cosigner.P2PAddr) + if err != nil { + errs = append(errs, fmt.Errorf("failed to parse cosigner (shard ID: %d) p2p address: %w", cosigner.ShardID, err)) + continue + } + + host, _, err := net.SplitHostPort(url.Host) + if err != nil { + errs = append(errs, fmt.Errorf("failed to parse cosigner (shard ID: %d) host port: %w", cosigner.ShardID, err)) + continue + } + if host == "0.0.0.0" { + errs = append(errs, fmt.Errorf("host cannot be 0.0.0.0, must be reachable from other cosigners")) } } - // Check that exactly {num-shares}-1 peers are in the peer list, assuming - // the remaining peer ID is the ID the local node is configured with. - if len(peers) != shares-1 { - return fmt.Errorf("incorrect number of peers. expected (%d shares - local node = %d peers)", - shares, shares-1) + // Check that exactly {num-shards} cosigners are in the list + if len(cosigners) != shards { + errs = append(errs, fmt.Errorf("incorrect number of cosigners. expected (%d shards = %d cosigners)", + shards, shards)) } - return nil + + return errors.Join(errs...) } -func duplicatePeers(peers []CosignerPeerConfig) (duplicates map[int][]string) { +func duplicateCosigners(cosigners []CosignerConfig) (duplicates map[int][]string) { idAddrs := make(map[int][]string) - for _, peer := range peers { - // Collect all addresses assigned to each share ID. - idAddrs[peer.ShareID] = append(idAddrs[peer.ShareID], peer.P2PAddr) + for _, cosigner := range cosigners { + // Collect all addresses assigned to each cosigner. + idAddrs[cosigner.ShardID] = append(idAddrs[cosigner.ShardID], cosigner.P2PAddr) } - for shareID, peers := range idAddrs { - if len(peers) == 1 { + for shardID, cosigners := range idAddrs { + if len(cosigners) == 1 { // One address per ID is correct. - delete(idAddrs, shareID) + delete(idAddrs, shardID) } } @@ -243,20 +272,10 @@ func duplicatePeers(peers []CosignerPeerConfig) (duplicates map[int][]string) { return idAddrs } -func PeersFromFlag(peers []string) (out []CosignerPeerConfig, err error) { +func CosignersFromFlag(cosigners []string) (out []CosignerConfig, err error) { var errs []error - for _, p := range peers { - ps := strings.Split(p, "|") - if len(ps) != 2 { - errs = append(errs, fmt.Errorf("invalid peer string %s, expected format: tcp://{addr}:{port}|{share-id}", p)) - continue - } - shareid, err := strconv.ParseInt(ps[1], 10, 64) - if err != nil { - errs = append(errs, fmt.Errorf("failed to parse share ID: %w", err)) - continue - } - out = append(out, CosignerPeerConfig{ShareID: int(shareid), P2PAddr: ps[0]}) + for i, c := range cosigners { + out = append(out, CosignerConfig{ShardID: i + 1, P2PAddr: c}) } if len(errs) > 0 { return nil, errors.Join(errs...) @@ -265,7 +284,7 @@ func PeersFromFlag(peers []string) (out []CosignerPeerConfig, err error) { } type ChainNode struct { - PrivValAddr string `json:"priv-val-addr" yaml:"priv-val-addr"` + PrivValAddr string `json:"privValAddr" yaml:"privValAddr"` } func (cn ChainNode) Validate() error { @@ -285,8 +304,8 @@ func (cns ChainNodes) Validate() error { return errors.Join(errs...) } -func ChainNodesFromArg(arg string) (out ChainNodes, err error) { - for _, n := range strings.Split(arg, ",") { +func ChainNodesFromFlag(nodes []string) (out ChainNodes, err error) { + for _, n := range nodes { cn := ChainNode{PrivValAddr: n} out = append(out, cn) } diff --git a/signer/config_test.go b/signer/config_test.go index 0beff6c9..ce3f4e69 100644 --- a/signer/config_test.go +++ b/signer/config_test.go @@ -6,7 +6,6 @@ import ( "net/url" "os" "path/filepath" - "strconv" "testing" "github.com/strangelove-ventures/horcrux/signer" @@ -54,7 +53,7 @@ func TestValidateSingleSignerConfig(t *testing.T) { config: signer.Config{ ChainNodes: nil, }, - expectErr: fmt.Errorf("need to have chain-nodes configured for priv-val connection"), + expectErr: fmt.Errorf("need to have chainNodes configured for priv-val connection"), }, { name: "invalid node address", @@ -80,7 +79,7 @@ func TestValidateSingleSignerConfig(t *testing.T) { } } -func TestValidateCosignerConfig(t *testing.T) { +func TestValidateThresholdModeConfig(t *testing.T) { type testCase struct { name string config signer.Config @@ -91,18 +90,21 @@ func TestValidateCosignerConfig(t *testing.T) { { name: "valid config", config: signer.Config{ - CosignerConfig: &signer.CosignerConfig{ - Threshold: 2, - Shares: 3, - Timeout: "1000ms", - P2PListen: "tcp://127.0.0.1:2222", - Peers: signer.CosignerPeersConfig{ + ThresholdModeConfig: &signer.ThresholdModeConfig{ + Threshold: 2, + RaftTimeout: "1000ms", + GRPCTimeout: "1000ms", + Cosigners: signer.CosignersConfig{ { - ShareID: 2, + ShardID: 1, P2PAddr: "tcp://127.0.0.1:2223", }, { - ShareID: 3, + ShardID: 2, + P2PAddr: "tcp://127.0.0.1:2223", + }, + { + ShardID: 3, P2PAddr: "tcp://127.0.0.1:2224", }, }, @@ -141,18 +143,21 @@ func TestValidateCosignerConfig(t *testing.T) { { name: "invalid p2p listen", config: signer.Config{ - CosignerConfig: &signer.CosignerConfig{ - Threshold: 2, - Shares: 3, - Timeout: "1000ms", - P2PListen: ":2222", - Peers: signer.CosignerPeersConfig{ + ThresholdModeConfig: &signer.ThresholdModeConfig{ + Threshold: 2, + RaftTimeout: "1000ms", + GRPCTimeout: "1000ms", + Cosigners: signer.CosignersConfig{ + { + ShardID: 1, + P2PAddr: ":2222", + }, { - ShareID: 2, + ShardID: 2, P2PAddr: "tcp://127.0.0.1:2223", }, { - ShareID: 3, + ShardID: 3, P2PAddr: "tcp://127.0.0.1:2224", }, }, @@ -169,23 +174,26 @@ func TestValidateCosignerConfig(t *testing.T) { }, }, }, - expectErr: fmt.Errorf("failed to parse p2p listen address: %w", &url.Error{ + expectErr: fmt.Errorf("failed to parse cosigner (shard ID: 1) p2p address: %w", &url.Error{ Op: "parse", URL: ":2222", Err: fmt.Errorf("missing protocol scheme"), }), }, { - name: "not enough peers", + name: "not enough cosigners", config: signer.Config{ - CosignerConfig: &signer.CosignerConfig{ - Threshold: 2, - Shares: 3, - Timeout: "1000ms", - P2PListen: "tcp://127.0.0.1:2222", - Peers: signer.CosignerPeersConfig{ + ThresholdModeConfig: &signer.ThresholdModeConfig{ + Threshold: 3, + RaftTimeout: "1000ms", + GRPCTimeout: "1000ms", + Cosigners: signer.CosignersConfig{ { - ShareID: 2, + ShardID: 1, + P2PAddr: "tcp://127.0.0.1:2222", + }, + { + ShardID: 2, P2PAddr: "tcp://127.0.0.1:2223", }, }, @@ -202,23 +210,26 @@ func TestValidateCosignerConfig(t *testing.T) { }, }, }, - expectErr: fmt.Errorf("incorrect number of peers. expected (3 shares - local node = 2 peers)"), + expectErr: fmt.Errorf("number of shards (2) must be greater or equal to threshold (3)"), }, { - name: "invalid timeout", + name: "invalid raft timeout", config: signer.Config{ - CosignerConfig: &signer.CosignerConfig{ - Threshold: 2, - Shares: 3, - Timeout: "1000", - P2PListen: "tcp://127.0.0.1:2222", - Peers: signer.CosignerPeersConfig{ + ThresholdModeConfig: &signer.ThresholdModeConfig{ + Threshold: 2, + GRPCTimeout: "1000ms", + RaftTimeout: "1000", + Cosigners: signer.CosignersConfig{ + { + ShardID: 1, + P2PAddr: "tcp://127.0.0.1:2222", + }, { - ShareID: 2, + ShardID: 2, P2PAddr: "tcp://127.0.0.1:2223", }, { - ShareID: 3, + ShardID: 3, P2PAddr: "tcp://127.0.0.1:2224", }, }, @@ -235,46 +246,88 @@ func TestValidateCosignerConfig(t *testing.T) { }, }, }, - expectErr: fmt.Errorf("invalid --timeout: %w", fmt.Errorf("time: missing unit in duration \"1000\"")), + expectErr: fmt.Errorf("invalid raftTimeout: %w", fmt.Errorf("time: missing unit in duration \"1000\"")), + }, + { + name: "invalid grpc timeout", + config: signer.Config{ + ThresholdModeConfig: &signer.ThresholdModeConfig{ + Threshold: 2, + GRPCTimeout: "1000", + RaftTimeout: "1000ms", + Cosigners: signer.CosignersConfig{ + { + ShardID: 1, + P2PAddr: "tcp://127.0.0.1:2222", + }, + { + ShardID: 2, + P2PAddr: "tcp://127.0.0.1:2223", + }, + { + ShardID: 3, + P2PAddr: "tcp://127.0.0.1:2224", + }, + }, + }, + ChainNodes: []signer.ChainNode{ + { + PrivValAddr: "tcp://127.0.0.1:1234", + }, + { + PrivValAddr: "tcp://127.0.0.1:2345", + }, + { + PrivValAddr: "tcp://127.0.0.1:3456", + }, + }, + }, + expectErr: fmt.Errorf("invalid grpcTimeout: %w", fmt.Errorf("time: missing unit in duration \"1000\"")), }, { name: "no nodes configured", config: signer.Config{ - CosignerConfig: &signer.CosignerConfig{ - Threshold: 2, - Shares: 3, - Timeout: "1000ms", - P2PListen: "tcp://127.0.0.1:2222", - Peers: signer.CosignerPeersConfig{ + ThresholdModeConfig: &signer.ThresholdModeConfig{ + Threshold: 2, + RaftTimeout: "1000ms", + GRPCTimeout: "1000ms", + Cosigners: signer.CosignersConfig{ + { + ShardID: 1, + P2PAddr: "tcp://127.0.0.1:2222", + }, { - ShareID: 2, + ShardID: 2, P2PAddr: "tcp://127.0.0.1:2223", }, { - ShareID: 3, + ShardID: 3, P2PAddr: "tcp://127.0.0.1:2224", }, }, }, ChainNodes: nil, }, - expectErr: fmt.Errorf("need to have chain-nodes configured for priv-val connection"), + expectErr: fmt.Errorf("need to have chainNodes configured for priv-val connection"), }, { name: "invalid node address", config: signer.Config{ - CosignerConfig: &signer.CosignerConfig{ - Threshold: 2, - Shares: 3, - Timeout: "1000ms", - P2PListen: "tcp://127.0.0.1:2222", - Peers: signer.CosignerPeersConfig{ + ThresholdModeConfig: &signer.ThresholdModeConfig{ + Threshold: 2, + RaftTimeout: "1000ms", + GRPCTimeout: "1000ms", + Cosigners: signer.CosignersConfig{ + { + ShardID: 1, + P2PAddr: "tcp://127.0.0.1:2222", + }, { - ShareID: 2, + ShardID: 2, P2PAddr: "tcp://127.0.0.1:2223", }, { - ShareID: 3, + ShardID: 3, P2PAddr: "tcp://127.0.0.1:2224", }, }, @@ -290,7 +343,7 @@ func TestValidateCosignerConfig(t *testing.T) { } for _, tc := range testCases { - err := tc.config.ValidateCosignerConfig() + err := tc.config.ValidateThresholdModeConfig() if tc.expectErr == nil { require.NoError(t, err, tc.name) } else { @@ -306,7 +359,7 @@ func TestRuntimeConfigKeyFilePath(t *testing.T) { HomeDir: dir, } - require.Equal(t, filepath.Join(dir, fmt.Sprintf("%s_share.json", testChainID)), c.KeyFilePathCosigner(testChainID)) + require.Equal(t, filepath.Join(dir, fmt.Sprintf("%s_shard.json", testChainID)), c.KeyFilePathCosigner(testChainID)) require.Equal( t, filepath.Join(dir, fmt.Sprintf("%s_priv_validator_key.json", testChainID)), @@ -323,33 +376,28 @@ func TestRuntimeConfigPrivValStateFile(t *testing.T) { require.Equal(t, filepath.Join(dir, "chain-1_priv_validator_state.json"), c.PrivValStateFile("chain-1")) } -func TestRuntimeConfigShareStateFile(t *testing.T) { - dir := t.TempDir() - c := signer.RuntimeConfig{ - StateDir: dir, - } - - require.Equal(t, filepath.Join(dir, "chain-1_share_sign_state.json"), c.ShareStateFile("chain-1")) -} - func TestRuntimeConfigWriteConfigFile(t *testing.T) { dir := t.TempDir() configFile := filepath.Join(dir, "config.yaml") c := signer.RuntimeConfig{ ConfigFile: configFile, Config: signer.Config{ - CosignerConfig: &signer.CosignerConfig{ - Threshold: 2, - Shares: 3, - Timeout: "1000ms", - P2PListen: "tcp://127.0.0.1:2222", - Peers: signer.CosignerPeersConfig{ + SignMode: signer.SignModeThreshold, + ThresholdModeConfig: &signer.ThresholdModeConfig{ + Threshold: 2, + RaftTimeout: "1000ms", + GRPCTimeout: "1000ms", + Cosigners: signer.CosignersConfig{ { - ShareID: 2, + ShardID: 1, + P2PAddr: "tcp://127.0.0.1:2222", + }, + { + ShardID: 2, P2PAddr: "tcp://127.0.0.1:2223", }, { - ShareID: 3, + ShardID: 3, P2PAddr: "tcp://127.0.0.1:2224", }, }, @@ -371,20 +419,22 @@ func TestRuntimeConfigWriteConfigFile(t *testing.T) { require.NoError(t, c.WriteConfigFile()) configYamlBz, err := os.ReadFile(configFile) require.NoError(t, err) - require.Equal(t, `cosigner: + require.Equal(t, `signMode: threshold +thresholdMode: 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 + cosigners: + - shardID: 1 + p2pAddr: tcp://127.0.0.1:2222 + - shardID: 2 + p2pAddr: tcp://127.0.0.1:2223 + - shardID: 3 + p2pAddr: tcp://127.0.0.1:2224 + grpcTimeout: 1000ms + raftTimeout: 1000ms +chainNodes: +- privValAddr: tcp://127.0.0.1:1234 +- privValAddr: tcp://127.0.0.1:2345 +- privValAddr: tcp://127.0.0.1:3456 `, string(configYamlBz)) } @@ -435,19 +485,22 @@ func TestRuntimeConfigKeyFileExists(t *testing.T) { require.NoError(t, err) } -func TestCosignerConfigLeaderElectMultiAddress(t *testing.T) { - c := &signer.CosignerConfig{ - Threshold: 2, - Shares: 3, - Timeout: "1000ms", - P2PListen: "tcp://127.0.0.1:2222", - Peers: signer.CosignerPeersConfig{ +func TestThresholdModeConfigLeaderElectMultiAddress(t *testing.T) { + c := &signer.ThresholdModeConfig{ + Threshold: 2, + RaftTimeout: "1000ms", + GRPCTimeout: "1000ms", + Cosigners: signer.CosignersConfig{ + { + ShardID: 1, + P2PAddr: "tcp://127.0.0.1:2222", + }, { - ShareID: 2, + ShardID: 2, P2PAddr: "tcp://127.0.0.1:2223", }, { - ShareID: 3, + ShardID: 3, P2PAddr: "tcp://127.0.0.1:2224", }, }, @@ -458,78 +511,65 @@ func TestCosignerConfigLeaderElectMultiAddress(t *testing.T) { require.Equal(t, "multi:///127.0.0.1:2222,127.0.0.1:2223,127.0.0.1:2224", multiAddr) } -func TestCosignerPeersConfigValidate(t *testing.T) { +func TestCosignerRSAPubKeysConfigValidate(t *testing.T) { type testCase struct { name string - peers signer.CosignerPeersConfig - shares int + cosigners signer.CosignersConfig expectErr error } testCases := []testCase{ { name: "valid config", - peers: signer.CosignerPeersConfig{ + cosigners: signer.CosignersConfig{ { - ShareID: 2, - P2PAddr: "tcp://127.0.0.1:2223", - }, - { - ShareID: 3, - P2PAddr: "tcp://127.0.0.1:2224", + ShardID: 1, + P2PAddr: "tcp://127.0.0.1:2222", }, - }, - shares: 3, - expectErr: nil, - }, - { - name: "too many peers", - peers: signer.CosignerPeersConfig{ { - ShareID: 2, + ShardID: 2, P2PAddr: "tcp://127.0.0.1:2223", }, { - ShareID: 3, + ShardID: 3, P2PAddr: "tcp://127.0.0.1:2224", }, }, - shares: 2, - expectErr: fmt.Errorf("peer ID 3 in args is out of range, must be between 1 and 2, inclusive"), + expectErr: nil, }, { - name: "too many shares", - peers: signer.CosignerPeersConfig{ + name: "too many cosigners", + cosigners: signer.CosignersConfig{ { - ShareID: 2, + ShardID: 2, P2PAddr: "tcp://127.0.0.1:2223", }, { - ShareID: 3, + ShardID: 3, P2PAddr: "tcp://127.0.0.1:2224", }, }, - shares: 4, - expectErr: fmt.Errorf("incorrect number of peers. expected (4 shares - local node = 3 peers)"), + expectErr: fmt.Errorf("cosigner shard ID 3 in args is out of range, must be between 1 and 2, inclusive"), }, { - name: "duplicate peer", - peers: signer.CosignerPeersConfig{ + name: "duplicate cosigner", + cosigners: signer.CosignersConfig{ { - ShareID: 2, + ShardID: 2, P2PAddr: "tcp://127.0.0.1:2223", }, { - ShareID: 2, + ShardID: 2, P2PAddr: "tcp://127.0.0.1:2223", }, }, - shares: 3, - expectErr: fmt.Errorf("found duplicate share IDs in args: map[2:[tcp://127.0.0.1:2223 tcp://127.0.0.1:2223]]"), + expectErr: fmt.Errorf( + "found duplicate cosigner shard ID(s) in args: map[2:[tcp://127.0.0.1:2223 tcp://127.0.0.1:2223]]", + ), }, } for _, tc := range testCases { - err := tc.peers.Validate(tc.shares) + err := tc.cosigners.Validate() if tc.expectErr == nil { require.NoError(t, err, tc.name) } else { @@ -539,39 +579,23 @@ func TestCosignerPeersConfigValidate(t *testing.T) { } } -func TestPeersFromFlag(t *testing.T) { +func TestCosignersFromFlag(t *testing.T) { type testCase struct { name string - peers []string + cosigners []string expectErr error } testCases := []testCase{ { - name: "valid peers flag", - peers: []string{"tcp://127.0.0.1:2222|1", "tcp://127.0.0.1:2223|2"}, + name: "valid cosigners flag", + cosigners: []string{"tcp://127.0.0.1:2222", "tcp://127.0.0.1:2223"}, expectErr: nil, }, - { - name: "missing peer id", - peers: []string{"tcp://127.0.0.1:2222|1", "tcp://127.0.0.1:2223"}, - expectErr: fmt.Errorf("invalid peer string tcp://127.0.0.1:2223, expected format: tcp://{addr}:{port}|{share-id}"), - }, - { - name: "invalid peer id", - peers: []string{"tcp://127.0.0.1:2222|1", "tcp://127.0.0.1:2223|a"}, - expectErr: fmt.Errorf("failed to parse share ID: %w", - &strconv.NumError{ - Func: "ParseInt", - Num: "a", - Err: fmt.Errorf("invalid syntax"), - }, - ), - }, } for _, tc := range testCases { - _, err := signer.PeersFromFlag(tc.peers) + _, err := signer.CosignersFromFlag(tc.cosigners) if tc.expectErr == nil { require.NoError(t, err, tc.name) } else { diff --git a/signer/cosigner.go b/signer/cosigner.go index 035d46d4..83ff34f1 100644 --- a/signer/cosigner.go +++ b/signer/cosigner.go @@ -143,7 +143,7 @@ type Cosigner interface { VerifySignature(chainID string, payload, signature []byte) bool - // Get ephemeral secret part for all peers + // Get ephemeral secret part for all cosigner shards GetEphemeralSecretParts(chainID string, hrst HRSTKey) (*CosignerEphemeralSecretPartsResponse, error) // Sign the requested bytes diff --git a/signer/cosigner_grpc_server.go b/signer/cosigner_grpc_server.go index 9f643269..c436d0af 100644 --- a/signer/cosigner_grpc_server.go +++ b/signer/cosigner_grpc_server.go @@ -78,13 +78,13 @@ func (rpc *CosignerGRPCServer) TransferLeadership( ) (*proto.CosignerGRPCTransferLeadershipResponse, error) { leaderID := req.GetLeaderID() if leaderID != "" { - for _, peer := range rpc.raftStore.Peers { - thisPeerID := fmt.Sprint(peer.GetID()) - if thisPeerID == leaderID { - peerRaftAddress := p2pURLToRaftAddress(peer.GetAddress()) - fmt.Printf("Transferring leadership to ID: %s - Address: %s\n", thisPeerID, peerRaftAddress) - rpc.raftStore.raft.LeadershipTransferToServer(raft.ServerID(thisPeerID), raft.ServerAddress(peerRaftAddress)) - return &proto.CosignerGRPCTransferLeadershipResponse{LeaderID: thisPeerID, LeaderAddress: peerRaftAddress}, nil + for _, c := range rpc.raftStore.Cosigners { + shardID := fmt.Sprint(c.GetID()) + if shardID == leaderID { + raftAddress := p2pURLToRaftAddress(c.GetAddress()) + fmt.Printf("Transferring leadership to ID: %s - Address: %s\n", shardID, raftAddress) + rpc.raftStore.raft.LeadershipTransferToServer(raft.ServerID(shardID), raft.ServerAddress(raftAddress)) + return &proto.CosignerGRPCTransferLeadershipResponse{LeaderID: shardID, LeaderAddress: raftAddress}, nil } } } diff --git a/signer/cosigner_key.go b/signer/cosigner_key.go index 9ab228eb..76cf1ea5 100644 --- a/signer/cosigner_key.go +++ b/signer/cosigner_key.go @@ -13,17 +13,17 @@ import ( amino "github.com/tendermint/go-amino" ) -// 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"` - ID int `json:"id"` +// CosignerEd25519Key is a single Ed255219 key shard for an m-of-n threshold signer. +type CosignerEd25519Key struct { + PubKey cometcrypto.PubKey `json:"pubKey"` + PrivateShard []byte `json:"privateShard"` + ID int `json:"id"` } -func (cosignerKey *CosignerKey) MarshalJSON() ([]byte, error) { - type Alias CosignerKey +func (key *CosignerEd25519Key) MarshalJSON() ([]byte, error) { + type Alias CosignerEd25519Key - protoPubkey, err := cometcryptoencoding.PubKeyToProto(cosignerKey.PubKey) + protoPubkey, err := cometcryptoencoding.PubKeyToProto(key.PubKey) if err != nil { return nil, err } @@ -34,22 +34,22 @@ func (cosignerKey *CosignerKey) MarshalJSON() ([]byte, error) { } return json.Marshal(&struct { - Pubkey []byte `json:"pub_key"` + PubKey []byte `json:"pubKey"` *Alias }{ - Pubkey: protoBytes, - Alias: (*Alias)(cosignerKey), + PubKey: protoBytes, + Alias: (*Alias)(key), }) } -func (cosignerKey *CosignerKey) UnmarshalJSON(data []byte) error { - type Alias CosignerKey +func (key *CosignerEd25519Key) UnmarshalJSON(data []byte) error { + type Alias CosignerEd25519Key aux := &struct { - PubkeyBytes []byte `json:"pub_key"` + PubkeyBytes []byte `json:"pubKey"` *Alias }{ - Alias: (*Alias)(cosignerKey), + Alias: (*Alias)(key), } if err := json.Unmarshal(data, &aux); err != nil { return err @@ -61,7 +61,7 @@ func (cosignerKey *CosignerKey) UnmarshalJSON(data []byte) error { // 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) + // cdc.MarshalBinaryBare(key.PubKey) // // To support reading the public key bytes from these key files, we fallback to // amino unmarshalling if the protobuf unmarshalling fails @@ -82,13 +82,13 @@ func (cosignerKey *CosignerKey) UnmarshalJSON(data []byte) error { } } - cosignerKey.PubKey = pubkey + key.PubKey = pubkey return nil } -// LoadCosignerKey loads a CosignerKey from file. -func LoadCosignerKey(file string) (CosignerKey, error) { - pvKey := CosignerKey{} +// LoadCosignerEd25519Key loads a CosignerEd25519Key from file. +func LoadCosignerEd25519Key(file string) (CosignerEd25519Key, error) { + pvKey := CosignerEd25519Key{} keyJSONBytes, err := os.ReadFile(file) if err != nil { return pvKey, err @@ -102,44 +102,44 @@ func LoadCosignerKey(file string) (CosignerKey, error) { 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"` +// CosignerEd25519Key is a single RSA key for an m-of-n threshold signer. +type CosignerRSAKey struct { + RSAKey rsa.PrivateKey `json:"rsaKey"` + ID int `json:"id"` + RSAPubs []*rsa.PublicKey `json:"rsaPubs"` } -func (cosignerKey *CosignerKeyRSA) MarshalJSON() ([]byte, error) { - type Alias CosignerKeyRSA +func (key *CosignerRSAKey) MarshalJSON() ([]byte, error) { + type Alias CosignerRSAKey // marshal our private key and all public keys - privateBytes := x509.MarshalPKCS1PrivateKey(&cosignerKey.RSAKey) + privateBytes := x509.MarshalPKCS1PrivateKey(&key.RSAKey) rsaPubKeysBytes := make([][]byte, 0) - for _, pubKey := range cosignerKey.CosignerKeys { + for _, pubKey := range key.RSAPubs { publicBytes := x509.MarshalPKCS1PublicKey(pubKey) rsaPubKeysBytes = append(rsaPubKeysBytes, publicBytes) } return json.Marshal(&struct { - RSAKey []byte `json:"rsa_key"` - CosignerKeys [][]byte `json:"rsa_pubs"` + RSAKey []byte `json:"rsaKey"` + RSAPubs [][]byte `json:"rsaPubs"` *Alias }{ - RSAKey: privateBytes, - CosignerKeys: rsaPubKeysBytes, - Alias: (*Alias)(cosignerKey), + RSAKey: privateBytes, + RSAPubs: rsaPubKeysBytes, + Alias: (*Alias)(key), }) } -func (cosignerKey *CosignerKeyRSA) UnmarshalJSON(data []byte) error { - type Alias CosignerKeyRSA +func (key *CosignerRSAKey) UnmarshalJSON(data []byte) error { + type Alias CosignerRSAKey aux := &struct { - RSAKey []byte `json:"rsa_key"` - CosignerKeys [][]byte `json:"rsa_pubs"` + RSAKey []byte `json:"rsaKey"` + RSAPubs [][]byte `json:"rsaPubs"` *Alias }{ - Alias: (*Alias)(cosignerKey), + Alias: (*Alias)(key), } if err := json.Unmarshal(data, &aux); err != nil { return err @@ -150,22 +150,22 @@ func (cosignerKey *CosignerKeyRSA) UnmarshalJSON(data []byte) error { } // unmarshal the public key bytes for each cosigner - cosignerKey.CosignerKeys = make([]*rsa.PublicKey, 0) - for _, bytes := range aux.CosignerKeys { + key.RSAPubs = make([]*rsa.PublicKey, 0) + for _, bytes := range aux.RSAPubs { cosignerRsaPubkey, err := x509.ParsePKCS1PublicKey(bytes) if err != nil { return err } - cosignerKey.CosignerKeys = append(cosignerKey.CosignerKeys, cosignerRsaPubkey) + key.RSAPubs = append(key.RSAPubs, cosignerRsaPubkey) } - cosignerKey.RSAKey = *privateKey + key.RSAKey = *privateKey return nil } -// LoadCosignerKeyRSA loads a CosignerKeyRSA from file. -func LoadCosignerKeyRSA(file string) (CosignerKeyRSA, error) { - pvKey := CosignerKeyRSA{} +// LoadCosignerRSAKey loads a CosignerRSAKey from file. +func LoadCosignerRSAKey(file string) (CosignerRSAKey, error) { + pvKey := CosignerRSAKey{} 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 7575fb76..66500e81 100644 --- a/signer/cosigner_key_shares.go +++ b/signer/cosigner_key_shares.go @@ -11,39 +11,39 @@ import ( tsed25519 "gitlab.com/unit410/threshold-ed25519/pkg" ) -// CreateCosignerSharesFromFile creates cosigner key objects from a priv_validator_key.json file -func CreateCosignerSharesFromFile(priv string, threshold, shares uint8) ([]CosignerKey, error) { +// CreateCosignerEd25519ShardsFromFile creates CosignerEd25519Key objects from a priv_validator_key.json file +func CreateCosignerEd25519ShardsFromFile(priv string, threshold, shards uint8) ([]CosignerEd25519Key, error) { pv, err := ReadPrivValidatorFile(priv) if err != nil { return nil, err } - return CreateCosignerShares(pv, threshold, shares) + return CreateCosignerEd25519Shards(pv, threshold, shards) } -// CreateCosignerShares creates cosigner key objects from a privval.FilePVKey -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)) - for idx, share := range privshares { - out = append(out, CosignerKey{ - PubKey: pv.PubKey, - ShareKey: share, - ID: idx + 1, +// CreateCosignerEd25519Shards creates CosignerEd25519Key objects from a privval.FilePVKey +func CreateCosignerEd25519Shards(pv privval.FilePVKey, threshold, shards uint8) (out []CosignerEd25519Key, err error) { + privShards := tsed25519.DealShares(tsed25519.ExpandSecret(pv.PrivKey.Bytes()[:32]), threshold, shards) + for i, shard := range privShards { + out = append(out, CosignerEd25519Key{ + PubKey: pv.PubKey, + PrivateShard: shard, + ID: i + 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) +// CreateCosignerRSAShards generate CosignerRSAKey objects +func CreateCosignerRSAShards(shards int) (out []CosignerRSAKey, err error) { + rsaKeys, pubKeys, err := makeRSAKeys(shards) if err != nil { return nil, err } for i, key := range rsaKeys { - out = append(out, CosignerKeyRSA{ - ID: i + 1, - RSAKey: *key, - CosignerKeys: pubKeys, + out = append(out, CosignerRSAKey{ + ID: i + 1, + RSAKey: *key, + RSAPubs: pubKeys, }) } return out, nil @@ -61,8 +61,8 @@ func ReadPrivValidatorFile(priv string) (out privval.FilePVKey, err error) { return } -// WriteCosignerShareFile writes a cosigner key to a given file name -func WriteCosignerShareFile(cosigner CosignerKey, file string) error { +// WriteCosignerEd25519ShardFile writes a cosigner Ed25519 key to a given file name +func WriteCosignerEd25519ShardFile(cosigner CosignerEd25519Key, file string) error { jsonBytes, err := json.Marshal(&cosigner) if err != nil { return err @@ -70,8 +70,8 @@ func WriteCosignerShareFile(cosigner CosignerKey, file string) error { return os.WriteFile(file, jsonBytes, 0600) } -// WriteCosignerShareRSAFile writes a cosigner RSA key to a given file name -func WriteCosignerShareRSAFile(cosigner CosignerKeyRSA, file string) error { +// WriteCosignerRSAShardFile writes a cosigner RSA key to a given file name +func WriteCosignerRSAShardFile(cosigner CosignerRSAKey, file string) error { jsonBytes, err := json.Marshal(&cosigner) if err != nil { return err diff --git a/signer/cosigner_key_test.go b/signer/cosigner_key_test.go index b7c462f1..b06f927c 100644 --- a/signer/cosigner_key_test.go +++ b/signer/cosigner_key_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestLoadCosignerKey(t *testing.T) { +func TestLoadCosignerEd25519Key(t *testing.T) { tmp := t.TempDir() rsaKeyFile := filepath.Join(tmp, "rsa_keys.json") @@ -17,10 +17,10 @@ func TestLoadCosignerKey(t *testing.T) { err := os.WriteFile(rsaKeyFile, testdata.RSAKeys, 0600) require.NoError(t, err) - key, err := LoadCosignerKeyRSA(rsaKeyFile) + key, err := LoadCosignerRSAKey(rsaKeyFile) require.NoError(t, err) require.Equal(t, key.ID, 3) // public key from cosigner pubs array should match public key from our private key - require.Equal(t, &key.RSAKey.PublicKey, key.CosignerKeys[key.ID-1]) + require.Equal(t, &key.RSAKey.PublicKey, key.RSAPubs[key.ID-1]) } diff --git a/signer/grpc_server.go b/signer/grpc_server.go index 6bd3526d..19333eff 100644 --- a/signer/grpc_server.go +++ b/signer/grpc_server.go @@ -48,7 +48,7 @@ func (rpc *GRPCServer) SetEphemeralSecretPartsAndSign( }) if err != nil { rpc.raftStore.logger.Error( - "Failed to sign with share", + "Failed to sign with shard", "chain_id", req.ChainID, "height", req.Hrst.Height, "round", req.Hrst.Round, @@ -58,7 +58,7 @@ func (rpc *GRPCServer) SetEphemeralSecretPartsAndSign( return nil, err } rpc.raftStore.logger.Info( - "Signed with share", + "Signed with shard", "chain_id", req.ChainID, "height", req.Hrst.Height, "round", req.Hrst.Round, @@ -93,13 +93,13 @@ func (rpc *GRPCServer) TransferLeadership( ) (*proto.CosignerGRPCTransferLeadershipResponse, error) { leaderID := req.GetLeaderID() if leaderID != "" { - for _, peer := range rpc.raftStore.Peers { - thisPeerID := fmt.Sprint(peer.GetID()) - if thisPeerID == leaderID { - peerRaftAddress := p2pURLToRaftAddress(peer.GetAddress()) - fmt.Printf("Transferring leadership to ID: %s - Address: %s\n", thisPeerID, peerRaftAddress) - rpc.raftStore.raft.LeadershipTransferToServer(raft.ServerID(thisPeerID), raft.ServerAddress(peerRaftAddress)) - return &proto.CosignerGRPCTransferLeadershipResponse{LeaderID: thisPeerID, LeaderAddress: peerRaftAddress}, nil + for _, c := range rpc.raftStore.Cosigners { + shardID := fmt.Sprint(c.GetID()) + if shardID == leaderID { + raftAddress := p2pURLToRaftAddress(c.GetAddress()) + fmt.Printf("Transferring leadership to ID: %s - Address: %s\n", shardID, raftAddress) + rpc.raftStore.raft.LeadershipTransferToServer(raft.ServerID(shardID), raft.ServerAddress(raftAddress)) + return &proto.CosignerGRPCTransferLeadershipResponse{LeaderID: shardID, LeaderAddress: raftAddress}, nil } } } diff --git a/signer/local_cosigner.go b/signer/local_cosigner.go index 61e0c894..25160857 100644 --- a/signer/local_cosigner.go +++ b/signer/local_cosigner.go @@ -24,8 +24,8 @@ 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 - // lastSignState stores the last sign state for a share we have fully signed - // incremented whenever we are asked to sign a share + // lastSignState stores the last sign state for an HRS we have fully signed + // incremented whenever we are asked to sign an HRS LastSignState *SignState } @@ -33,12 +33,12 @@ type ChainState struct { // Signing is thread safe - lastSignStateMutex is used for putting locks so only one goroutine can r/w to the function mu sync.Mutex - // lastSignState stores the last sign state for a share we have fully signed - // incremented whenever we are asked to sign a share + // lastSignState stores the last sign state for an HRS we have fully signed + // incremented whenever we are asked to sign an HRS lastSignState *SignState pubKeyBytes []byte - key CosignerKey + key CosignerEd25519Key // Height, Round, Step -> metadata hrsMeta map[HRSTKey]HrsMetadata @@ -74,7 +74,7 @@ func (hrst *HRSTKey) Less(other HRSTKey) bool { return false } -type CosignerPeer struct { +type CosignerRSAPubKey struct { ID int PublicKey rsa.PublicKey } @@ -88,24 +88,17 @@ type CosignerGetEphemeralSecretPartRequest struct { Timestamp time.Time } -// LocalCosigner responds to sign requests using their share key +// LocalCosigner responds to sign requests using the key shard // The cosigner maintains a watermark to avoid double-signing // // LocalCosigner signing is thread saafe type LocalCosigner struct { - config *RuntimeConfig - - id int - rsaKey rsa.PrivateKey - total uint8 - threshold uint8 - - chainState sync.Map - - peers map[int]CosignerPeer - - address string - + config *RuntimeConfig + key CosignerRSAKey + threshold uint8 + chainState sync.Map + rsaPubKeys map[int]CosignerRSAPubKey + address string pendingDiskWG sync.WaitGroup } @@ -140,25 +133,21 @@ func (cosigner *LocalCosigner) waitForSignStatesToFlushToDisk() { func NewLocalCosigner( config *RuntimeConfig, - id int, - rsaKey rsa.PrivateKey, - peers []CosignerPeer, + key CosignerRSAKey, + rsaPubKeys []CosignerRSAPubKey, address string, - total uint8, threshold uint8, ) *LocalCosigner { cosigner := &LocalCosigner{ - config: config, - id: id, - rsaKey: rsaKey, - peers: make(map[int]CosignerPeer), - total: total, - threshold: threshold, - address: address, + config: config, + key: key, + rsaPubKeys: make(map[int]CosignerRSAPubKey), + threshold: threshold, + address: address, } - for _, peer := range peers { - cosigner.peers[peer.ID] = peer + for _, pubKey := range rsaPubKeys { + cosigner.rsaPubKeys[pubKey.ID] = pubKey } return cosigner @@ -167,7 +156,7 @@ func NewLocalCosigner( // GetID returns the id of the cosigner // Implements Cosigner interface func (cosigner *LocalCosigner) GetID() int { - return cosigner.id + return cosigner.key.ID } // GetAddress returns the RPC URL of the cosigner @@ -216,7 +205,7 @@ func (cosigner *LocalCosigner) VerifySignature(chainID string, payload, signatur return ccs.key.PubKey.VerifySignature(payload, signature) } -// Sign the sign request using the cosigner's share +// Sign the sign request using the cosigner's shard // Return the signed bytes or an error // Implements Cosigner interface func (cosigner *LocalCosigner) sign(req CosignerSignRequest) (CosignerSignResponse, error) { @@ -275,32 +264,30 @@ func (cosigner *LocalCosigner) sign(req CosignerSignRequest) (CosignerSignRespon publicKeys := make([]tsed25519.Element, 0) // calculate secret and public keys - for _, peer := range meta.Peers { - if len(peer.Share) == 0 { + for _, c := range meta.Cosigners { + if len(c.Share) == 0 { continue } - shareParts = append(shareParts, peer.Share) - publicKeys = append(publicKeys, peer.EphemeralSecretPublicKey) + shareParts = append(shareParts, c.Share) + publicKeys = append(publicKeys, c.EphemeralSecretPublicKey) } ephemeralShare := tsed25519.AddScalars(shareParts) ephemeralPublic := tsed25519.AddElements(publicKeys) // check bounds for ephemeral share to avoid passing out of bounds valids to SignWithShare - { - if len(ephemeralShare) != 32 { - return res, errors.New("ephemeral share is out of bounds") - } + if len(ephemeralShare) != 32 { + return res, errors.New("ephemeral share is out of bounds") + } - var scalarBytes [32]byte - copy(scalarBytes[:], ephemeralShare) - if !edwards25519.ScMinimal(&scalarBytes) { - return res, errors.New("ephemeral share is out of bounds") - } + var scalarBytes [32]byte + copy(scalarBytes[:], ephemeralShare) + if !edwards25519.ScMinimal(&scalarBytes) { + return res, errors.New("ephemeral share is out of bounds") } sig := tsed25519.SignWithShare( - req.SignBytes, ccs.key.ShareKey, ephemeralShare, ccs.pubKeyBytes, ephemeralPublic) + req.SignBytes, ccs.key.PrivateShard, ephemeralShare, ccs.pubKeyBytes, ephemeralPublic) ccs.lastSignState.EphemeralPublic = ephemeralPublic err = ccs.lastSignState.Save(SignStateConsensus{ @@ -365,14 +352,16 @@ func (cosigner *LocalCosigner) dealShares(req CosignerGetEphemeralSecretPartRequ return HrsMetadata{}, err } + total := len(cosigner.rsaPubKeys) + meta = HrsMetadata{ - Secret: secret, - Peers: make([]PeerMetadata, cosigner.total), + Secret: secret, + Cosigners: make([]CosignerMetadata, total), } // split this secret with shamirs // !! dealt shares need to be saved because dealing produces different shares each time! - meta.DealtShares = tsed25519.DealShares(meta.Secret, cosigner.threshold, cosigner.total) + meta.DealtShares = tsed25519.DealShares(meta.Secret, cosigner.threshold, uint8(total)) ccs.hrsMeta[hrsKey] = meta @@ -389,7 +378,7 @@ func (cosigner *LocalCosigner) LoadSignStateIfNecessary(chainID string) error { return nil } - shareSignState, err := LoadOrCreateSignState(cosigner.config.ShareStateFile(chainID)) + signState, err := LoadOrCreateSignState(cosigner.config.CosignerStateFile(chainID)) if err != nil { return err } @@ -399,17 +388,17 @@ func (cosigner *LocalCosigner) LoadSignStateIfNecessary(chainID string) error { return err } - key, err := LoadCosignerKey(keyFile) + key, err := LoadCosignerEd25519Key(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) + if key.ID != cosigner.GetID() { + return fmt.Errorf("key shard ID (%d) in (%s) does not match cosigner ID (%d)", key.ID, keyFile, cosigner.GetID()) } cosigner.chainState.Store(chainID, &ChainState{ - lastSignState: shareSignState, + lastSignState: signState, hrsMeta: make(map[HRSTKey]HrsMetadata), // cache the public key bytes for signing operations key: key, @@ -430,18 +419,18 @@ func (cosigner *LocalCosigner) GetEphemeralSecretParts( } res := &CosignerEphemeralSecretPartsResponse{ - EncryptedSecrets: make([]CosignerEphemeralSecretPart, 0, len(cosigner.peers)-1), + EncryptedSecrets: make([]CosignerEphemeralSecretPart, 0, len(cosigner.rsaPubKeys)-1), } id := cosigner.GetID() - for _, peer := range cosigner.peers { - if peer.ID == id { + for _, pubKey := range cosigner.rsaPubKeys { + if pubKey.ID == id { continue } secretPart, err := cosigner.getEphemeralSecretPart(CosignerGetEphemeralSecretPartRequest{ ChainID: chainID, - ID: peer.ID, + ID: pubKey.ID, Height: hrst.Height, Round: hrst.Round, Step: hrst.Step, @@ -510,19 +499,19 @@ func (cosigner *LocalCosigner) getEphemeralSecretPart( id := ccs.key.ID // set our values - meta.Peers[id-1].Share = meta.DealtShares[id-1] - meta.Peers[id-1].EphemeralSecretPublicKey = ourEphPublicKey + meta.Cosigners[id-1].Share = meta.DealtShares[id-1] + meta.Cosigners[id-1].EphemeralSecretPublicKey = ourEphPublicKey - // grab the peer info for the ID being requested - peer, ok := cosigner.peers[req.ID] + // grab the cosigner info for the ID being requested + pubKey, ok := cosigner.rsaPubKeys[req.ID] if !ok { - return res, errors.New("unknown peer ID") + return res, errors.New("unknown cosigner ID") } sharePart := meta.DealtShares[req.ID-1] // use RSA public to encrypt user's share part - encrypted, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &peer.PublicKey, sharePart, nil) + encrypted, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &pubKey.PublicKey, sharePart, nil) if err != nil { return res, err } @@ -541,7 +530,7 @@ func (cosigner *LocalCosigner) getEphemeralSecretPart( } digest := sha256.Sum256(jsonBytes) - signature, err := rsa.SignPSS(rand.Reader, &cosigner.rsaKey, crypto.SHA256, digest[:], nil) + signature, err := rsa.SignPSS(rand.Reader, &cosigner.key.RSAKey, crypto.SHA256, digest[:], nil) if err != nil { return res, err } @@ -585,14 +574,13 @@ func (cosigner *LocalCosigner) setEphemeralSecretPart(req CosignerSetEphemeralSe } digest := sha256.Sum256(digestBytes) - peer, ok := cosigner.peers[req.SourceID] + pubKey, ok := cosigner.rsaPubKeys[req.SourceID] if !ok { return fmt.Errorf("unknown cosigner: %d", req.SourceID) } - peerPub := peer.PublicKey - err = rsa.VerifyPSS(&peerPub, crypto.SHA256, digest[:], req.SourceSig, nil) + err = rsa.VerifyPSS(&pubKey.PublicKey, crypto.SHA256, digest[:], req.SourceSig, nil) if err != nil { return err } @@ -627,14 +615,14 @@ func (cosigner *LocalCosigner) setEphemeralSecretPart(req CosignerSetEphemeralSe } // decrypt share - sharePart, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, &cosigner.rsaKey, req.EncryptedSharePart, nil) + sharePart, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, &cosigner.key.RSAKey, req.EncryptedSharePart, nil) if err != nil { return err } // set slot - meta.Peers[req.SourceID-1].Share = sharePart - meta.Peers[req.SourceID-1].EphemeralSecretPublicKey = req.SourceEphemeralSecretPublicKey + meta.Cosigners[req.SourceID-1].Share = sharePart + meta.Cosigners[req.SourceID-1].EphemeralSecretPublicKey = req.SourceEphemeralSecretPublicKey return nil } diff --git a/signer/local_cosigner_test.go b/signer/local_cosigner_test.go index f607cfd8..5a104f17 100644 --- a/signer/local_cosigner_test.go +++ b/signer/local_cosigner_test.go @@ -27,23 +27,24 @@ func TestLocalCosignerGetID(t *testing.T) { rsaKey, err := rsa.GenerateKey(rand.Reader, bitSize) require.NoError(t, err) - key := CosignerKey{ - PubKey: dummyPub, - ShareKey: []byte{}, - ID: 1, + key := CosignerEd25519Key{ + PubKey: dummyPub, + PrivateShard: []byte{}, + ID: 1, } cosigner := NewLocalCosigner( &RuntimeConfig{}, - key.ID, - *rsaKey, - []CosignerPeer{{ + CosignerRSAKey{ + ID: key.ID, + RSAKey: *rsaKey, + }, + []CosignerRSAPubKey{{ ID: 1, PublicKey: rsaKey.PublicKey, }}, "", 0, - 0, ) require.Equal(t, 1, cosigner.GetID()) @@ -61,7 +62,7 @@ func TestLocalCosignerSign2of2(t *testing.T) { rsaKey2, err := rsa.GenerateKey(rand.Reader, bitSize) require.NoError(t, err) - peers := []CosignerPeer{{ + pubKeys := []CosignerRSAPubKey{{ ID: 1, PublicKey: rsaKey1.PublicKey, }, { @@ -73,18 +74,18 @@ func TestLocalCosignerSign2of2(t *testing.T) { privKeyBytes := [64]byte{} copy(privKeyBytes[:], privateKey[:]) - secretShares := tsed25519.DealShares(tsed25519.ExpandSecret(privKeyBytes[:32]), threshold, total) + privShards := tsed25519.DealShares(tsed25519.ExpandSecret(privKeyBytes[:32]), threshold, total) - key1 := CosignerKey{ - PubKey: privateKey.PubKey(), - ShareKey: secretShares[0], - ID: 1, + key1 := CosignerEd25519Key{ + PubKey: privateKey.PubKey(), + PrivateShard: privShards[0], + ID: 1, } - key2 := CosignerKey{ - PubKey: privateKey.PubKey(), - ShareKey: secretShares[1], - ID: 2, + key2 := CosignerEd25519Key{ + PubKey: privateKey.PubKey(), + PrivateShard: privShards[1], + ID: 2, } tmpDir := t.TempDir() @@ -101,11 +102,12 @@ func TestLocalCosignerSign2of2(t *testing.T) { HomeDir: cosigner1Dir, StateDir: cosigner1Dir, }, - key1.ID, - *rsaKey1, - peers, + CosignerRSAKey{ + ID: key1.ID, + RSAKey: *rsaKey1, + }, + pubKeys, "", - total, threshold, ) @@ -121,11 +123,12 @@ func TestLocalCosignerSign2of2(t *testing.T) { HomeDir: cosigner2Dir, StateDir: cosigner2Dir, }, - key2.ID, - *rsaKey2, - peers, + CosignerRSAKey{ + ID: key2.ID, + RSAKey: *rsaKey2, + }, + pubKeys, "", - total, threshold, ) diff --git a/signer/raft_store.go b/signer/raft_store.go index a1e81d4e..cc982499 100644 --- a/signer/raft_store.go +++ b/signer/raft_store.go @@ -49,7 +49,7 @@ type RaftStore struct { RaftDir string RaftBind string RaftTimeout time.Duration - Peers []Cosigner + Cosigners []Cosigner mu sync.Mutex m map[string]string // The key-value store for the system. @@ -64,7 +64,7 @@ type RaftStore struct { // New returns a new Store. func NewRaftStore( nodeID string, directory string, bindAddress string, timeout time.Duration, - logger log.Logger, cosigner *LocalCosigner, raftPeers []Cosigner) *RaftStore { + logger log.Logger, cosigner *LocalCosigner, cosigners []Cosigner) *RaftStore { cosignerRaftStore := &RaftStore{ NodeID: nodeID, RaftDir: directory, @@ -73,7 +73,7 @@ func NewRaftStore( m: make(map[string]string), logger: logger, cosigner: cosigner, - Peers: raftPeers, + Cosigners: cosigners, } cosignerRaftStore.BaseService = *service.NewBaseService(logger, "CosignerRaftStore", cosignerRaftStore) @@ -182,10 +182,10 @@ func (s *RaftStore) Open() (*raftgrpctransport.Manager, error) { }, }, } - for _, peer := range s.Peers { + for _, c := range s.Cosigners { configuration.Servers = append(configuration.Servers, raft.Server{ - ID: raft.ServerID(fmt.Sprint(peer.GetID())), - Address: raft.ServerAddress(p2pURLToRaftAddress(peer.GetAddress())), + ID: raft.ServerID(fmt.Sprint(c.GetID())), + Address: raft.ServerAddress(p2pURLToRaftAddress(c.GetAddress())), }) } s.raft.BootstrapCluster(configuration) diff --git a/signer/raft_store_test.go b/signer/raft_store_test.go index 1c4e0eaf..a4cd6fc6 100644 --- a/signer/raft_store_test.go +++ b/signer/raft_store_test.go @@ -22,22 +22,24 @@ func Test_StoreInMemOpenSingleNode(t *testing.T) { rsaKey, err := rsa.GenerateKey(rand.Reader, bitSize) require.NoError(t, err) - key := CosignerKey{ - PubKey: dummyPub, - ShareKey: []byte{}, - ID: 1, + key := CosignerEd25519Key{ + PubKey: dummyPub, + PrivateShard: []byte{}, + ID: 1, } cosigner := NewLocalCosigner( &RuntimeConfig{}, - key.ID, - *rsaKey, - []CosignerPeer{{ + CosignerRSAKey{ + ID: key.ID, + RSAKey: *rsaKey, + }, + []CosignerRSAPubKey{{ ID: 1, PublicKey: rsaKey.PublicKey, }}, "", - 0, 0, + 0, ) s := &RaftStore{ @@ -48,7 +50,6 @@ func Test_StoreInMemOpenSingleNode(t *testing.T) { m: make(map[string]string), logger: nil, cosigner: cosigner, - Peers: []Cosigner{}, } if _, err := s.Open(); err != nil { diff --git a/signer/testdata/rsa_keys.json b/signer/testdata/rsa_keys.json index 25864a2d..cd1b60c1 100644 --- a/signer/testdata/rsa_keys.json +++ b/signer/testdata/rsa_keys.json @@ -1,6 +1,6 @@ { - "rsa_key": "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", - "rsa_pubs": [ + "rsaKey": "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", + "rsaPubs": [ "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" diff --git a/signer/threshold_signer.go b/signer/threshold_signer.go index 48863f0b..55d7a72d 100644 --- a/signer/threshold_signer.go +++ b/signer/threshold_signer.go @@ -16,10 +16,10 @@ type ThresholdSigner interface { DealShares(req CosignerGetEphemeralSecretPartRequest) (HrsMetadata, error) GetEphemeralSecretPart(req CosignerGetEphemeralSecretPartRequest, m *LastSignStateWrapper, - peers map[int]CosignerPeer) (CosignerEphemeralSecretPart, error) + pubKeys map[int]CosignerRSAPubKey) (CosignerEphemeralSecretPart, error) SetEphemeralSecretPart(req CosignerSetEphemeralSecretPartRequest, m *LastSignStateWrapper, - peers map[int]CosignerPeer) error + pubKeys map[int]CosignerRSAPubKey) error Sign(req CosignerSignRequest, m *LastSignStateWrapper) (CosignerSignResponse, error) @@ -29,9 +29,9 @@ type ThresholdSigner interface { Stop() } -// PeerMetadata holds the share and the ephermeral secret public key +// CosignerMetadata holds the share and the ephermeral secret public key // Moved from Local cosigner to threshold_ed25519 -type PeerMetadata struct { +type CosignerMetadata struct { Share []byte EphemeralSecretPublicKey []byte } @@ -42,5 +42,5 @@ type HrsMetadata struct { // need to be _total_ entries per player Secret []byte DealtShares []tsed25519.Scalar - Peers []PeerMetadata + Cosigners []CosignerMetadata } diff --git a/signer/threshold_signer_soft.go b/signer/threshold_signer_soft.go index cf45ea7b..a4f09139 100644 --- a/signer/threshold_signer_soft.go +++ b/signer/threshold_signer_soft.go @@ -20,8 +20,8 @@ import ( // ThresholdSignerSoft is the implementation of a soft sign signer at the local level. type ThresholdSignerSoft struct { pubKeyBytes []byte - key CosignerKey - rsaKey CosignerKeyRSA + key CosignerEd25519Key + rsaKey CosignerRSAKey // total signers total uint8 threshold uint8 @@ -33,7 +33,7 @@ type ThresholdSignerSoft struct { // NewThresholdSignerSoft constructs a ThresholdSigner // that signs using the local key share file. -func NewThresholdSignerSoft(key CosignerKey, rsaKey CosignerKeyRSA, threshold, total uint8) ThresholdSigner { +func NewThresholdSignerSoft(key CosignerEd25519Key, rsaKey CosignerRSAKey, threshold, total uint8) ThresholdSigner { softSigner := &ThresholdSignerSoft{ key: key, rsaKey: rsaKey, @@ -109,12 +109,12 @@ func (softSigner *ThresholdSignerSoft) Sign( publicKeys := make([]tsed25519.Element, 0) // calculate secret and public keys - for _, peer := range meta.Peers { - if len(peer.Share) == 0 { + for _, c := range meta.Cosigners { + if len(c.Share) == 0 { continue } - shareParts = append(shareParts, peer.Share) - publicKeys = append(publicKeys, peer.EphemeralSecretPublicKey) + shareParts = append(shareParts, c.Share) + publicKeys = append(publicKeys, c.EphemeralSecretPublicKey) } ephemeralShare := tsed25519.AddScalars(shareParts) @@ -133,7 +133,7 @@ func (softSigner *ThresholdSignerSoft) Sign( } sig := tsed25519.SignWithShare( - req.SignBytes, softSigner.key.ShareKey, ephemeralShare, softSigner.pubKeyBytes, ephemeralPublic) + req.SignBytes, softSigner.key.PrivateShard, ephemeralShare, softSigner.pubKeyBytes, ephemeralPublic) m.LastSignState.EphemeralPublic = ephemeralPublic err = m.LastSignState.Save(SignStateConsensus{ @@ -184,8 +184,8 @@ func (softSigner *ThresholdSignerSoft) DealShares( } meta = HrsMetadata{ - Secret: secret, - Peers: make([]PeerMetadata, softSigner.total), + Secret: secret, + Cosigners: make([]CosignerMetadata, softSigner.total), } // split this secret with shamirs @@ -201,7 +201,7 @@ func (softSigner *ThresholdSignerSoft) DealShares( // The ephemeral secret part is encrypted for the receiver // Implements ThresholdSigner func (softSigner *ThresholdSignerSoft) GetEphemeralSecretPart( - req CosignerGetEphemeralSecretPartRequest, m *LastSignStateWrapper, peers map[int]CosignerPeer) ( + req CosignerGetEphemeralSecretPartRequest, m *LastSignStateWrapper, pubKeys map[int]CosignerRSAPubKey) ( CosignerEphemeralSecretPart, error) { res := CosignerEphemeralSecretPart{} @@ -239,19 +239,19 @@ func (softSigner *ThresholdSignerSoft) GetEphemeralSecretPart( ourEphPublicKey := tsed25519.ScalarMultiplyBase(meta.Secret) // set our values - meta.Peers[softSigner.key.ID-1].Share = meta.DealtShares[softSigner.key.ID-1] - meta.Peers[softSigner.key.ID-1].EphemeralSecretPublicKey = ourEphPublicKey + meta.Cosigners[softSigner.key.ID-1].Share = meta.DealtShares[softSigner.key.ID-1] + meta.Cosigners[softSigner.key.ID-1].EphemeralSecretPublicKey = ourEphPublicKey - // grab the peer info for the ID being requested - peer, ok := peers[req.ID] + // grab the info for the ID being requested + pubKey, ok := pubKeys[req.ID] if !ok { - return res, errors.New("unknown peer ID") + return res, errors.New("unknown cosigner ID") } sharePart := meta.DealtShares[req.ID-1] // use RSA public to encrypt user's share part - encrypted, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &peer.PublicKey, sharePart, nil) + encrypted, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &pubKey.PublicKey, sharePart, nil) if err != nil { return res, err } @@ -286,7 +286,7 @@ func (softSigner *ThresholdSignerSoft) GetEphemeralSecretPart( // Store an ephemeral secret share part provided by another cosigner (signer) // Implements ThresholdSigner func (softSigner *ThresholdSignerSoft) SetEphemeralSecretPart( - req CosignerSetEphemeralSecretPartRequest, m *LastSignStateWrapper, peers map[int]CosignerPeer) error { + req CosignerSetEphemeralSecretPartRequest, m *LastSignStateWrapper, pubKeys map[int]CosignerRSAPubKey) error { // Verify the source signature if req.SourceSig == nil { @@ -307,14 +307,13 @@ func (softSigner *ThresholdSignerSoft) SetEphemeralSecretPart( } digest := sha256.Sum256(digestBytes) - peer, ok := peers[req.SourceID] + pubKey, ok := pubKeys[req.SourceID] if !ok { return fmt.Errorf("unknown cosigner: %d", req.SourceID) } - peerPub := peer.PublicKey - err = rsa.VerifyPSS(&peerPub, crypto.SHA256, digest[:], req.SourceSig, nil) + err = rsa.VerifyPSS(&pubKey.PublicKey, crypto.SHA256, digest[:], req.SourceSig, nil) if err != nil { return err } @@ -352,8 +351,8 @@ func (softSigner *ThresholdSignerSoft) SetEphemeralSecretPart( } // set slot // Share & EphemeralSecretPublicKey is a SLICE so its a valid change of the shared struct softSigner! - meta.Peers[req.SourceID-1].Share = sharePart - meta.Peers[req.SourceID-1].EphemeralSecretPublicKey = req.SourceEphemeralSecretPublicKey + meta.Cosigners[req.SourceID-1].Share = sharePart + meta.Cosigners[req.SourceID-1].EphemeralSecretPublicKey = req.SourceEphemeralSecretPublicKey return nil } diff --git a/signer/threshold_validator.go b/signer/threshold_validator.go index a5796a76..d0e9674b 100644 --- a/signer/threshold_validator.go +++ b/signer/threshold_validator.go @@ -24,13 +24,15 @@ type ThresholdValidator struct { threshold int + grpcTimeout time.Duration + chainState sync.Map // our own cosigner - cosigner Cosigner + myCosigner Cosigner // peer cosigners - peers []Cosigner + peerCosigners []Cosigner raftStore *RaftStore @@ -55,17 +57,19 @@ func NewThresholdValidator( logger log.Logger, config *RuntimeConfig, threshold int, - cosigner Cosigner, - peers []Cosigner, + grpcTimeout time.Duration, + myCosigner Cosigner, + peerCosigners []Cosigner, raftStore *RaftStore, ) *ThresholdValidator { return &ThresholdValidator{ - logger: logger, - config: config, - threshold: threshold, - cosigner: cosigner, - peers: peers, - raftStore: raftStore, + logger: logger, + config: config, + threshold: threshold, + grpcTimeout: grpcTimeout, + myCosigner: myCosigner, + peerCosigners: peerCosigners, + raftStore: raftStore, } } @@ -116,7 +120,7 @@ func (pv *ThresholdValidator) Stop() { func (pv *ThresholdValidator) waitForSignStatesToFlushToDisk() { pv.pendingDiskWG.Wait() - switch cosigner := pv.cosigner.(type) { + switch cosigner := pv.myCosigner.(type) { case *LocalCosigner: cosigner.waitForSignStatesToFlushToDisk() default: @@ -126,7 +130,7 @@ func (pv *ThresholdValidator) waitForSignStatesToFlushToDisk() { // GetPubKey returns the public key of the validator. // Implements PrivValidator. func (pv *ThresholdValidator) GetPubKey(chainID string) (crypto.PubKey, error) { - return pv.cosigner.GetPubKey(chainID) + return pv.myCosigner.GetPubKey(chainID) } // SignVote signs a canonical representation of the vote, along with the @@ -222,7 +226,7 @@ func (pv *ThresholdValidator) waitForPeerEphemeralShares( peer Cosigner, hrst HRSTKey, wg *sync.WaitGroup, - encryptedEphemeralSharesThresholdMap *map[Cosigner][]CosignerEphemeralSecretPart, + encryptedEphemeralSharesThresholdMap map[Cosigner][]CosignerEphemeralSecretPart, thresholdPeersMutex *sync.Mutex, ) { peerStartTime := time.Now() @@ -241,8 +245,8 @@ func (pv *ThresholdValidator) waitForPeerEphemeralShares( // Check so that getEphemeralWaitGroup.Done is not called more than (threshold - 1) times which causes hardlock thresholdPeersMutex.Lock() - if len(*encryptedEphemeralSharesThresholdMap) < pv.threshold-1 { - (*encryptedEphemeralSharesThresholdMap)[peer] = ephemeralSecretParts.EncryptedSecrets + if len(encryptedEphemeralSharesThresholdMap) < pv.threshold-1 { + (encryptedEphemeralSharesThresholdMap)[peer] = ephemeralSecretParts.EncryptedSecrets defer wg.Done() } thresholdPeersMutex.Unlock() @@ -253,7 +257,7 @@ func (pv *ThresholdValidator) waitForPeerSetEphemeralSharesAndSign( ourID int, peer Cosigner, hrst HRSTKey, - encryptedEphemeralSharesThresholdMap *map[Cosigner][]CosignerEphemeralSecretPart, + encryptedEphemeralSharesThresholdMap map[Cosigner][]CosignerEphemeralSecretPart, signBytes []byte, shareSignatures *[][]byte, shareSignaturesMutex *sync.Mutex, @@ -263,11 +267,12 @@ func (pv *ThresholdValidator) waitForPeerSetEphemeralSharesAndSign( peerStartTime := time.Now() defer wg.Done() peerEphemeralSecretParts := make([]CosignerEphemeralSecretPart, 0, pv.threshold-1) - for _, EncryptedSecrets := range *encryptedEphemeralSharesThresholdMap { - for _, ephemeralSecretPart := range EncryptedSecrets { + + for _, encryptedSecrets := range encryptedEphemeralSharesThresholdMap { + for _, ephemeralSecretPart := range encryptedSecrets { // if share is intended for peer, check to make sure source peer is included in threshold if ephemeralSecretPart.DestinationID == peer.GetID() { - for thresholdPeer := range *encryptedEphemeralSharesThresholdMap { + for thresholdPeer := range encryptedEphemeralSharesThresholdMap { if thresholdPeer.GetID() == ephemeralSecretPart.SourceID { // source peer is included in threshold signature, include in sharing peerEphemeralSecretParts = append(peerEphemeralSecretParts, ephemeralSecretPart) @@ -355,7 +360,7 @@ func (pv *ThresholdValidator) LoadSignStateIfNecessary(chainID string) error { lastSignStateInitiatedMutex: &sync.Mutex{}, }) - switch cosigner := pv.cosigner.(type) { + switch cosigner := pv.myCosigner.(type) { case *LocalCosigner: return cosigner.LoadSignStateIfNecessary(chainID) default: @@ -492,7 +497,7 @@ func (pv *ThresholdValidator) SignBlock(chainID string, block *Block) ([]byte, t } } - numPeers := len(pv.peers) + numPeers := len(pv.peerCosigners) total := uint8(numPeers + 1) getEphemeralWaitGroup := sync.WaitGroup{} @@ -500,17 +505,17 @@ func (pv *ThresholdValidator) SignBlock(chainID string, block *Block) ([]byte, t getEphemeralWaitGroup.Add(pv.threshold - 1) // Used to track how close we are to threshold - ourID := pv.cosigner.GetID() + ourID := pv.myCosigner.GetID() - encryptedEphemeralSharesThresholdMap := make(map[Cosigner][]CosignerEphemeralSecretPart) + ephSecrets := make(map[Cosigner][]CosignerEphemeralSecretPart) thresholdPeersMutex := sync.Mutex{} - for _, peer := range pv.peers { - go pv.waitForPeerEphemeralShares(chainID, peer, hrst, &getEphemeralWaitGroup, - &encryptedEphemeralSharesThresholdMap, &thresholdPeersMutex) + for _, c := range pv.peerCosigners { + go pv.waitForPeerEphemeralShares(chainID, c, hrst, &getEphemeralWaitGroup, + ephSecrets, &thresholdPeersMutex) } - ourEphemeralSecretParts, err := pv.cosigner.GetEphemeralSecretParts(chainID, hrst) + myEphSecrets, err := pv.myCosigner.GetEphemeralSecretParts(chainID, hrst) if err != nil { // Our ephemeral secret parts are required, cannot proceed return nil, stamp, err @@ -518,12 +523,12 @@ func (pv *ThresholdValidator) SignBlock(chainID string, block *Block) ([]byte, t // Wait for threshold cosigners to be complete // A Cosigner will either respond in time, or be cancelled with timeout - if waitUntilCompleteOrTimeout(&getEphemeralWaitGroup, 4*time.Second) { + if waitUntilCompleteOrTimeout(&getEphemeralWaitGroup, pv.grpcTimeout) { return nil, stamp, errors.New("timed out waiting for ephemeral shares") } thresholdPeersMutex.Lock() - encryptedEphemeralSharesThresholdMap[pv.cosigner] = ourEphemeralSecretParts.EncryptedSecrets + ephSecrets[pv.myCosigner] = myEphSecrets.EncryptedSecrets thresholdPeersMutex.Unlock() timedSignBlockThresholdLag.Observe(time.Since(timeStartSignBlock).Seconds()) @@ -548,9 +553,9 @@ func (pv *ThresholdValidator) SignBlock(chainID string, block *Block) ([]byte, t var ephemeralPublic []byte - for peer := range encryptedEphemeralSharesThresholdMap { + for cosigner := range ephSecrets { // set peerEphemeralSecretParts and sign in single rpc call. - go pv.waitForPeerSetEphemeralSharesAndSign(chainID, ourID, peer, hrst, &encryptedEphemeralSharesThresholdMap, + go pv.waitForPeerSetEphemeralSharesAndSign(chainID, ourID, cosigner, hrst, ephSecrets, signBytes, &shareSignatures, &shareSignaturesMutex, &ephemeralPublic, &setEphemeralAndSignWaitGroup) } @@ -595,7 +600,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.cosigner.VerifySignature(chainID, signBytes, signature) { + if !pv.myCosigner.VerifySignature(chainID, signBytes, signature) { totalInvalidSignature.Inc() return nil, stamp, errors.New("combined signature is not valid") } diff --git a/signer/threshold_validator_test.go b/signer/threshold_validator_test.go index 5cc9bc7a..22f7424b 100644 --- a/signer/threshold_validator_test.go +++ b/signer/threshold_validator_test.go @@ -47,7 +47,6 @@ func getMockRaftStore(cosigner Cosigner, tmpDir string) *RaftStore { m: make(map[string]string), logger: nil, cosigner: cosigner.(*LocalCosigner), - Peers: []Cosigner{}, } } @@ -55,12 +54,12 @@ func loadKeyForLocalCosigner( cosigner *LocalCosigner, pubKey cometcrypto.PubKey, chainID string, - secretShare []byte, + privateShard []byte, ) error { - key := CosignerKey{ - PubKey: pubKey, - ShareKey: secretShare, - ID: cosigner.GetID(), + key := CosignerEd25519Key{ + PubKey: pubKey, + PrivateShard: privateShard, + ID: cosigner.GetID(), } keyBz, err := key.MarshalJSON() @@ -73,7 +72,7 @@ func loadKeyForLocalCosigner( func testThresholdValidator(t *testing.T, threshold, total uint8) { rsaKeys := make([]*rsa.PrivateKey, total) - peers := make([]CosignerPeer, total) + pubKeys := make([]CosignerRSAPubKey, total) cosigners := make([]*LocalCosigner, total) for i := uint8(0); i < total; i++ { @@ -82,7 +81,7 @@ func testThresholdValidator(t *testing.T, threshold, total uint8) { rsaKeys[i] = rsaKey - peers[i] = CosignerPeer{ + pubKeys[i] = CosignerRSAPubKey{ ID: int(i) + 1, PublicKey: rsaKey.PublicKey, } @@ -90,12 +89,12 @@ func testThresholdValidator(t *testing.T, threshold, total uint8) { privateKey := cometcryptoed25519.GenPrivKey() privKeyBytes := privateKey[:] - secretShares := tsed25519.DealShares(tsed25519.ExpandSecret(privKeyBytes[:32]), threshold, total) + privShards := tsed25519.DealShares(tsed25519.ExpandSecret(privKeyBytes[:32]), threshold, total) tmpDir := t.TempDir() - for i, peer := range peers { - cosignerDir := filepath.Join(tmpDir, fmt.Sprintf("cosigner_%d", peer.ID)) + for i, pubKey := range pubKeys { + cosignerDir := filepath.Join(tmpDir, fmt.Sprintf("cosigner_%d", pubKey.ID)) err := os.MkdirAll(cosignerDir, 0777) require.NoError(t, err) @@ -106,27 +105,30 @@ func testThresholdValidator(t *testing.T, threshold, total uint8) { cosigner := NewLocalCosigner( cosignerConfig, - peer.ID, *rsaKeys[i], - peers, "", total, threshold, + CosignerRSAKey{ + ID: pubKey.ID, + RSAKey: *rsaKeys[i], + }, + pubKeys, "", threshold, ) require.NoError(t, err) cosigners[i] = cosigner - err = loadKeyForLocalCosigner(cosigner, privateKey.PubKey(), testChainID, secretShares[i]) + err = loadKeyForLocalCosigner(cosigner, privateKey.PubKey(), testChainID, privShards[i]) require.NoError(t, err) - err = loadKeyForLocalCosigner(cosigner, privateKey.PubKey(), testChainID2, secretShares[i]) + err = loadKeyForLocalCosigner(cosigner, privateKey.PubKey(), testChainID2, privShards[i]) require.NoError(t, err) } - thresholdPeers := make([]Cosigner, 0, threshold-1) + thresholdCosigners := make([]Cosigner, 0, threshold-1) for i, cosigner := range cosigners { require.Equal(t, i+1, cosigner.GetID()) - if i != 0 && len(thresholdPeers) != int(threshold)-1 { - thresholdPeers = append(thresholdPeers, cosigner) + if i != 0 && len(thresholdCosigners) != int(threshold)-1 { + thresholdCosigners = append(thresholdCosigners, cosigner) } } @@ -136,8 +138,9 @@ func testThresholdValidator(t *testing.T, threshold, total uint8) { cometlog.NewTMLogger(cometlog.NewSyncWriter(os.Stdout)).With("module", "validator"), cosigners[0].config, int(threshold), + time.Second, cosigners[0], - thresholdPeers, + thresholdCosigners, raftStore, ) defer validator.Stop() @@ -218,8 +221,9 @@ func testThresholdValidator(t *testing.T, threshold, total uint8) { cometlog.NewTMLogger(cometlog.NewSyncWriter(os.Stdout)).With("module", "validator"), cosigners[0].config, int(threshold), + time.Second, cosigners[0], - thresholdPeers, + thresholdCosigners, raftStore, ) defer newValidator.Stop() diff --git a/test/horcrux_test.go b/test/horcrux_test.go index 211df1c7..72258c9b 100644 --- a/test/horcrux_test.go +++ b/test/horcrux_test.go @@ -40,7 +40,9 @@ func testChainSingleNodeAndHorcrux( require.NoError(t, Genesis(ctx, t, chain, otherValidatorNodes, []*Node{}, []*Validator{ourValidator})) // Wait for all nodes to get to given block height - require.NoError(t, GetAllNodes(otherValidatorNodes, ourValidator.Sentries).WaitForHeight(5)) + require.NoError(t, GetAllNodes(otherValidatorNodes, ourValidator.Sentries[chainID]).WaitForHeight(5)) + + require.NoError(t, ourValidator.WaitForConsecutiveBlocks(chainID, 10)) t.Logf("{%s} -> Checking that slashing has not occurred...", ourValidator.Name()) require.NoError(t, ourValidator.EnsureNotSlashed(chainID)) @@ -185,7 +187,7 @@ func TestUpgradeValidatorToHorcrux(t *testing.T) { // create horcrux validator with same consensus key ourValidatorUpgradedToHorcrux, err := NewHorcruxValidatorWithPrivValKey(t, pool, network, home, - chainID, 0, 0, totalSigners, threshold, chain, ourValidatorPrivValKey) + 0, 0, totalSigners, threshold, chain, ourValidatorPrivValKey) require.NoError(t, err) // stop our validator node before upgrading to horcrux @@ -195,7 +197,7 @@ func TestUpgradeValidatorToHorcrux(t *testing.T) { time.Sleep(5 * time.Second) // wait for all containers to stop // bring in single signer node as a sentry for horcrux - ourValidatorUpgradedToHorcrux.Sentries = []*Node{ourValidatorNode} + ourValidatorUpgradedToHorcrux.Sentries[chainID] = []*Node{ourValidatorNode} // modify node config to listen for private validator connections ourValidatorNode.SetPrivValListen(validators.PeerString()) @@ -245,7 +247,7 @@ func TestDownedSigners2of3(t *testing.T) { require.NoError(t, Genesis(ctx, t, chain, otherValidatorNodes, []*Node{}, []*Validator{ourValidator})) // Wait for all nodes to get to given block height - require.NoError(t, GetAllNodes(otherValidatorNodes, ourValidator.Sentries).WaitForHeight(5)) + require.NoError(t, GetAllNodes(otherValidatorNodes, ourValidator.Sentries[chainID]).WaitForHeight(5)) // Test taking down each node in the signer cluster for a period of time for _, signer := range ourValidator.Signers { @@ -294,7 +296,7 @@ func TestLeaderElection2of3(t *testing.T) { require.NoError(t, Genesis(ctx, t, chain, otherValidatorNodes, []*Node{}, []*Validator{ourValidator})) // Wait for all nodes to get to given block height - require.NoError(t, GetAllNodes(otherValidatorNodes, ourValidator.Sentries).WaitForHeight(5)) + require.NoError(t, GetAllNodes(otherValidatorNodes, ourValidator.Sentries[chainID]).WaitForHeight(5)) // Test electing each node in the signer cluster for a period of time for _, signer := range ourValidator.Signers { @@ -367,7 +369,7 @@ func TestDownedSigners3of5(t *testing.T) { require.NoError(t, Genesis(ctx, t, chain, otherValidatorNodes, []*Node{}, []*Validator{ourValidator})) // Wait for all nodes to get to given block height - require.NoError(t, GetAllNodes(otherValidatorNodes, ourValidator.Sentries).WaitForHeight(5)) + require.NoError(t, GetAllNodes(otherValidatorNodes, ourValidator.Sentries[chainID]).WaitForHeight(5)) // Test taking down 2 nodes at a time in the signer cluster for a period of time for i := 0; i < len(ourValidator.Signers); i++ { @@ -433,7 +435,7 @@ func TestChainPureHorcrux(t *testing.T) { signersPerValidator, threshold, chain) require.NoError(t, err) validators = append(validators, validator) - allNodes = append(allNodes, validator.Sentries...) + allNodes = append(allNodes, validator.Sentries[chainID]...) startValidatorsErrGroup.Go(func() error { return validator.StartHorcruxCluster(sentriesPerSigner) }) @@ -490,7 +492,8 @@ func TestMultipleChainHorcrux(t *testing.T) { signersPerValidator, threshold, chain1, chain2) require.NoError(t, err) validators = append(validators, validator) - allNodes = append(allNodes, validator.Sentries...) + allNodes = append(allNodes, validator.Sentries[chainID1]...) + allNodes = append(allNodes, validator.Sentries[chainID2]...) startValidatorsErrGroup.Go(func() error { return validator.StartHorcruxCluster(sentriesPerSigner) }) diff --git a/test/node.go b/test/node.go index 58c686ef..7ca96217 100644 --- a/test/node.go +++ b/test/node.go @@ -912,12 +912,11 @@ func (tn Nodes) Peers(node *Node) (out Nodes) { return } -func (tn Nodes) ListenAddrs() string { - out := []string{} +func (tn Nodes) ConfigInitFlags() (out []string) { for _, n := range tn { - out = append(out, fmt.Sprintf("%s:%s", n.Name(), "1234")) + out = append(out, "--node", fmt.Sprintf("%s:%s", n.Name(), "1234")) } - return strings.Join(out, ",") + return out } // LogGenesisHashes logs the genesis hashes for the various nodes diff --git a/test/setup.go b/test/setup.go index 520ebb31..854ba1d6 100644 --- a/test/setup.go +++ b/test/setup.go @@ -56,9 +56,11 @@ func Genesis( ) error { var eg errgroup.Group + chainID := chain.ChainID + // sign gentx for each validator for _, v := range nonHorcruxValidators { - if v.ChainID != chain.ChainID { + if v.ChainID != chainID { continue } v := v @@ -80,29 +82,16 @@ func Genesis( } i := 0 - var firstSentry *Node - - for ; i < len(v.Sentries); i++ { - if v.Sentries[i].ChainID == chain.ChainID { - firstSentry = v.Sentries[i] - break - } - } - - if firstSentry == nil { - return fmt.Errorf("no sentry found for chain id: %s", chain.ChainID) - } + firstSentry := v.Sentries[chainID][0] // using the first sentry for each horcrux validator as the keyring for the account key (not consensus key) // to sign gentx eg.Go(func() error { return firstSentry.InitValidatorFiles(ctx, pubKey) }) - for i++; i < len(v.Sentries); i++ { - s := v.Sentries[i] - if s.ChainID != chain.ChainID { - continue - } + for i++; i < len(v.Sentries[chainID]); i++ { + s := v.Sentries[chainID][i] + eg.Go(func() error { return s.InitFullNodeFiles(ctx) }) } } @@ -134,25 +123,12 @@ func Genesis( for _, horcruxValidator := range horcruxValidators { if len(horcruxValidator.Sentries) > 0 { - var firstSentry *Node - for _, n := range horcruxValidator.Sentries { - if n.ChainID == chain.ChainID { - firstSentry = n - break - } - } - if firstSentry == nil { - return fmt.Errorf("no sentry found for chain id: %s", chain.ChainID) - } + firstSentry := horcruxValidator.Sentries[chain.ChainID][0] // for test purposes, account key (not consensus key) will come from first sentry validators = append(validators, firstSentry) } - for _, n := range horcruxValidator.Sentries { - if n.ChainID == chain.ChainID { - nodes = append(nodes, n) - } - } + nodes = append(nodes, horcruxValidator.Sentries[chain.ChainID]...) } for _, n := range fullnodes { @@ -220,11 +196,8 @@ func Genesis( // start horcrux sentries. privval listener enabled for _, v := range horcruxValidators { - for _, sentry := range v.Sentries { - if sentry.ChainID != chain.ChainID { - continue - } - s := sentry + for _, s := range v.Sentries[chainID] { + s := s tl.Logf("{%s} => starting container...", s.Name()) eg.Go(func() error { return s.Start(ctx, func() { diff --git a/test/signer.go b/test/signer.go index c723b240..597e197c 100644 --- a/test/signer.go +++ b/test/signer.go @@ -37,7 +37,7 @@ type Signer struct { Pool *dockertest.Pool networkID string Container *docker.Container - Key signer.CosignerKey + Key signer.CosignerEd25519Key tl Logger } @@ -128,82 +128,82 @@ func StartSingleSignerContainers( } // StartCosignerContainers will generate the necessary config files for the nodes in the signer cluster, -// shard the validator's priv_validator_key.json key, write the sharded key shares to the appropriate +// shard the validator's priv_validator_key.json key, write the sharded key shards to the appropriate // signer nodes directory and start the signer cluster. // NOTE: Zero or negative values for sentriesPerSigner configures the nodes in the signer cluster to connect to the // same sentry node. func StartCosignerContainers( signers Signers, - sentries Nodes, + sentryMap map[string]Nodes, threshold uint8, sentriesPerSigner int, ) error { eg := new(errgroup.Group) ctx := context.Background() - // init config files, for each node in the signer cluster, with the appropriate number of sentries in front of the node - switch { - // Each node in the signer cluster is connected to a unique sentry node - case sentriesPerSigner == 1: - singleSentryIndex := 0 - for i, s := range signers { - s := s - - var peers Nodes - - if len(sentries) == 1 || len(signers) > len(sentries) { - peers = sentries[singleSentryIndex : singleSentryIndex+1] - singleSentryIndex++ - if singleSentryIndex >= len(sentries) { - singleSentryIndex = 0 + peers := make([]Nodes, len(signers)) + + for _, sentries := range sentryMap { + // init config files, for each node in the signer cluster, + // with the appropriate number of sentries in front of the node + switch { + // Each node in the signer cluster is connected to a unique sentry node + case sentriesPerSigner == 1: + singleSentryIndex := 0 + for i := range signers { + if len(sentries) == 1 || len(signers) > len(sentries) { + peers[i] = append(peers[i], sentries[singleSentryIndex:singleSentryIndex+1]...) + singleSentryIndex++ + if singleSentryIndex >= len(sentries) { + singleSentryIndex = 0 + } + } else { + peers[i] = append(peers[i], sentries[i:i+1]...) } - } else { - peers = sentries[i : i+1] } - eg.Go(func() error { return s.InitCosignerConfig(ctx, peers, signers, s.Index, threshold) }) - } - - // Each node in the signer cluster is connected to the number of sentry nodes specified by sentriesPerSigner - case sentriesPerSigner > 1: - sentriesIndex := 0 - for _, s := range signers { - s := s - var peers Nodes - // if we are indexing sentries up to the end of the slice - switch { - case sentriesIndex+sentriesPerSigner == len(sentries): - peers = sentries[sentriesIndex:] - sentriesIndex++ - - // if there aren't enough sentries left in the slice use the sentries left in slice, - // calculate how many more are needed, then start back at the beginning of - // the slice to grab the rest. After, check if index into slice of sentries needs reset - case sentriesIndex+sentriesPerSigner > len(sentries): - remainingSentries := sentries[sentriesIndex:] - peers = append(peers, remainingSentries...) - - neededSentries := sentriesPerSigner - len(remainingSentries) - peers = append(peers, sentries[0:neededSentries]...) - - sentriesIndex++ - if sentriesIndex >= len(sentries) { - sentriesIndex = 0 + // Each node in the signer cluster is connected to the number of sentry nodes specified by sentriesPerSigner + case sentriesPerSigner > 1: + sentriesIndex := 0 + for i := range signers { + // if we are indexing sentries up to the end of the slice + switch { + case sentriesIndex+sentriesPerSigner == len(sentries): + peers[i] = append(peers[i], sentries[sentriesIndex:]...) + sentriesIndex++ + + // if there aren't enough sentries left in the slice use the sentries left in slice, + // calculate how many more are needed, then start back at the beginning of + // the slice to grab the rest. After, check if index into slice of sentries needs reset + case sentriesIndex+sentriesPerSigner > len(sentries): + remainingSentries := sentries[sentriesIndex:] + peers[i] = append(peers[i], remainingSentries...) + + neededSentries := sentriesPerSigner - len(remainingSentries) + peers[i] = append(peers[i], sentries[0:neededSentries]...) + + sentriesIndex++ + if sentriesIndex >= len(sentries) { + sentriesIndex = 0 + } + default: + peers[i] = append(peers[i], sentries[sentriesIndex:sentriesIndex+sentriesPerSigner]...) + sentriesIndex++ } - default: - peers = sentries[sentriesIndex : sentriesIndex+sentriesPerSigner] - sentriesIndex++ } - eg.Go(func() error { return s.InitCosignerConfig(ctx, peers, signers, s.Index, threshold) }) + // All nodes in the signer cluster are connected to all sentry nodes + default: + for i := range signers { + peers[i] = append(peers[i], sentries...) + } } + } - // All nodes in the signer cluster are connected to all sentry nodes - default: - for _, s := range signers { - s := s - eg.Go(func() error { return s.InitCosignerConfig(ctx, sentries, signers, s.Index, threshold) }) - } + for i, s := range signers { + i := i + s := s + eg.Go(func() error { return s.InitThresholdModeConfig(ctx, peers[i], signers, threshold) }) } err := eg.Wait() if err != nil { @@ -232,17 +232,12 @@ func StartCosignerContainers( return eg.Wait() } -// PeerString returns a string representing a Signer's connectable private peers -// skip is the calling Signer's index -func (ts Signers) PeerString(skip int) string { - var out strings.Builder +// CosignerFlags returns a string slice representing Signers' connectable private peers +func (ts Signers) ConfigInitFlags() (out []string) { for _, s := range ts { - // Skip over the calling signer so its peer list does not include itself - if s.Index != skip { - out.WriteString(fmt.Sprintf("tcp://%s:%s|%d,", s.Name(), signerPort, s.Index)) - } + out = append(out, "--cosigner", fmt.Sprintf("tcp://%s:%s", s.Name(), signerPort)) } - return strings.TrimSuffix(out.String(), ",") + return out } // MakeSigners creates the Signer objects required for bootstrapping tests @@ -262,7 +257,6 @@ func MakeSigners( Pool: pool, networkID: networkID, Container: nil, - Key: signer.CosignerKey{}, tl: tl, } out = append(out, ts) @@ -382,21 +376,20 @@ func (ts *Signer) ExecHorcruxCmd(ctx context.Context, cmd ...string) error { // InitSingleSignerConfig creates and runs a container to init a single signers config files // blocks until the container exits func (ts *Signer) InitSingleSignerConfig(ctx context.Context, listenNodes Nodes) error { - return ts.ExecHorcruxCmd(ctx, - "config", "init", listenNodes.ListenAddrs()) + cmd := []string{"config", "init", "--mode", "single"} + cmd = append(cmd, listenNodes.ConfigInitFlags()...) + return ts.ExecHorcruxCmd(ctx, cmd...) } -// InitCosignerConfig creates and runs a container to init a signer nodes config files +// InitThresholdModeConfig 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 int, threshold uint8) error { - return ts.ExecHorcruxCmd(ctx, - "config", "init", listenNodes.ListenAddrs(), - "--cosigner", - fmt.Sprintf("--peers=%s", peers.PeerString(skip)), - fmt.Sprintf("--threshold=%d", threshold), - fmt.Sprintf("--listen=%s", ts.GRPCAddress()), - ) +func (ts *Signer) InitThresholdModeConfig( + ctx context.Context, listenNodes Nodes, cosigners Signers, threshold uint8) error { + cmd := []string{"config", "init"} + cmd = append(cmd, listenNodes.ConfigInitFlags()...) + cmd = append(cmd, cosigners.ConfigInitFlags()...) + cmd = append(cmd, "--threshold", fmt.Sprint(threshold)) + return ts.ExecHorcruxCmd(ctx, cmd...) } // StartContainer starts a Signers container and assigns the new running container to replace the old one @@ -441,7 +434,7 @@ func (ts *Signer) CreateSingleSignerContainer() error { Name: ts.Name(), Config: &docker.Config{ User: getDockerUserString(), - Cmd: []string{binary, "signer", "start", "--accept-risk", fmt.Sprintf("--home=%s", ts.Dir())}, + Cmd: []string{binary, "start", "--accept-risk", fmt.Sprintf("--home=%s", ts.Dir())}, Hostname: ts.Name(), ExposedPorts: map[docker.Port]struct{}{ docker.Port(signerPortDocker): {}, @@ -483,7 +476,7 @@ func (ts *Signer) CreateCosignerContainer() error { Name: ts.Name(), Config: &docker.Config{ User: getDockerUserString(), - Cmd: []string{binary, "cosigner", "start", fmt.Sprintf("--home=%s", ts.Dir())}, + Cmd: []string{binary, "start", fmt.Sprintf("--home=%s", ts.Dir())}, Hostname: ts.Name(), ExposedPorts: map[docker.Port]struct{}{ docker.Port(signerPortDocker): {}, diff --git a/test/validator.go b/test/validator.go index 968f66b3..b577315f 100644 --- a/test/validator.go +++ b/test/validator.go @@ -14,7 +14,7 @@ import ( type Validator struct { Index int - Sentries Nodes + Sentries map[string]Nodes Signers Signers tl Logger Home string @@ -32,20 +32,18 @@ func NewHorcruxValidator( threshold uint8, chains ...*ChainType, ) (*Validator, error) { - var sentries Nodes chainIDs := make([]string, 0, len(chains)) + sentries := make(map[string]Nodes) for _, chain := range chains { - sentries = append(sentries, - MakeNodes( - index, - chain.NumSentries, - home, - chain.ChainID, - chain, - pool, - networkID, - tl, - )..., + sentries[chain.ChainID] = MakeNodes( + index, + chain.NumSentries, + home, + chain.ChainID, + chain, + pool, + networkID, + tl, ) chainIDs = append(chainIDs, chain.ChainID) } @@ -58,7 +56,7 @@ func NewHorcruxValidator( Threshold: threshold, PubKeys: make(map[string]cometcrypto.PubKey), } - if err := testValidator.genPrivKeyAndShares(nil, chainIDs...); err != nil { + if err := testValidator.genPrivKeyAndShards(nil, chainIDs...); err != nil { return nil, err } return testValidator, nil @@ -69,7 +67,6 @@ func NewHorcruxValidatorWithPrivValKey( pool *dockertest.Pool, networkID string, home string, - chainID string, index int, numSentries int, numSigners int, @@ -77,16 +74,19 @@ func NewHorcruxValidatorWithPrivValKey( chainType *ChainType, privValKey privval.FilePVKey, ) (*Validator, error) { + chainID := chainType.ChainID + sentries := make(map[string]Nodes) + sentries[chainID] = MakeNodes(index, numSentries, home, chainID, chainType, pool, networkID, tl) testValidator := &Validator{ Index: index, - Sentries: MakeNodes(index, numSentries, home, chainID, chainType, pool, networkID, tl), + Sentries: sentries, Signers: MakeSigners(index, numSigners, home, pool, networkID, tl), tl: tl, Home: home, Threshold: threshold, PubKeys: make(map[string]cometcrypto.PubKey), } - if err := testValidator.genPrivKeyAndShares(&privValKey, chainID); err != nil { + if err := testValidator.genPrivKeyAndShards(&privValKey, chainID); err != nil { return nil, err } return testValidator, nil @@ -103,19 +103,19 @@ func (tv *Validator) Dir() string { } func (tv *Validator) genRSAShares() error { - rsaShares, err := signer.CreateCosignerSharesRSA(len(tv.Signers)) + rsaShards, err := signer.CreateCosignerRSAShards(len(tv.Signers)) if err != nil { return err } for i, s := range tv.Signers { - tv.tl.Logf("{%s} -> Writing RSA Key Share To File... ", s.Name()) + tv.tl.Logf("{%s} -> Writing RSA Key Shard To File... ", s.Name()) if err := os.MkdirAll(s.Dir(), 0700); err != nil { return err } cosignerFilename := filepath.Join(s.Dir(), "rsa_keys.json") - if err := signer.WriteCosignerShareRSAFile(rsaShares[i], cosignerFilename); err != nil { + if err := signer.WriteCosignerRSAShardFile(rsaShards[i], cosignerFilename); err != nil { return err } } @@ -123,15 +123,15 @@ func (tv *Validator) genRSAShares() error { 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 { +// genPrivKeyAndShards generates cosigner RSA shards. +// If existingKey is nil, generates Ed25519 key shards, otherwise shards existing key. +func (tv *Validator) genPrivKeyAndShards(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 { + if err := tv.genEd25519Shards(existingKey, chainID); err != nil { return err } } @@ -139,7 +139,7 @@ func (tv *Validator) genPrivKeyAndShares(existingKey *privval.FilePVKey, chainID return nil } -func (tv *Validator) genEd25519Shares( +func (tv *Validator) genEd25519Shards( existingKey *privval.FilePVKey, chainID string, ) error { @@ -158,16 +158,16 @@ func (tv *Validator) genEd25519Shares( tv.PubKeys[chainID] = key.PubKey - shares, err := signer.CreateCosignerShares(key, tv.Threshold, uint8(len(tv.Signers))) + shards, err := signer.CreateCosignerEd25519Shards(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()) + tv.tl.Logf("{%s} -> Writing Ed25519 Key Shard To File... ", s.Name()) - privateFilename := filepath.Join(s.Dir(), fmt.Sprintf("%s_share.json", chainID)) - if err := signer.WriteCosignerShareFile(shares[i], privateFilename); err != nil { + privateFilename := filepath.Join(s.Dir(), fmt.Sprintf("%s_shard.json", chainID)) + if err := signer.WriteCosignerEd25519ShardFile(shards[i], privateFilename); err != nil { return err } } @@ -182,18 +182,18 @@ 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.PubKeys[chainID].Address()) + for sentryChainID, n := range tv.Sentries { + if sentryChainID == chainID { + return n[0].WaitForConsecutiveBlocks(blocks, tv.PubKeys[chainID].Address()) } } return fmt.Errorf("no sentry found with chain id: %s", chainID) } func (tv *Validator) EnsureNotSlashed(chainID string) error { - for _, n := range tv.Sentries { - if n.ChainID == chainID { - return n.EnsureNotSlashed(tv.PubKeys[chainID].Address()) + for sentryChainID, n := range tv.Sentries { + if sentryChainID == chainID { + return n[0].EnsureNotSlashed(tv.PubKeys[chainID].Address()) } } return fmt.Errorf("no sentry found with chain id: %s", chainID)