diff --git a/config/config.go b/config/config.go index 34d69cc1a675..7d11795cd182 100644 --- a/config/config.go +++ b/config/config.go @@ -9,7 +9,6 @@ import ( "encoding/json" "errors" "fmt" - "io/fs" "math" "os" "path/filepath" @@ -34,10 +33,9 @@ import ( "github.com/ava-labs/avalanchego/subnets" "github.com/ava-labs/avalanchego/trace" "github.com/ava-labs/avalanchego/upgrade" + "github.com/ava-labs/avalanchego/utils/bag" "github.com/ava-labs/avalanchego/utils/compression" "github.com/ava-labs/avalanchego/utils/constants" - "github.com/ava-labs/avalanchego/utils/crypto/bls" - "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner" "github.com/ava-labs/avalanchego/utils/ips" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/perms" @@ -78,11 +76,11 @@ var ( errCannotTrackPrimaryNetwork = errors.New("cannot track primary network") errStakingKeyContentUnset = fmt.Errorf("%s key not set but %s set", StakingTLSKeyContentKey, StakingCertContentKey) errStakingCertContentUnset = fmt.Errorf("%s key set but %s not set", StakingTLSKeyContentKey, StakingCertContentKey) - errMissingStakingSigningKeyFile = errors.New("missing staking signing key file") errPluginDirNotADirectory = errors.New("plugin dir is not a directory") errCannotReadDirectory = errors.New("cannot read directory") errUnmarshalling = errors.New("unmarshalling failed") errFileDoesNotExist = errors.New("file does not exist") + errInvalidSignerConfig = fmt.Errorf("only one of the following flags can be set: %s, %s, %s, %s", StakingEphemeralSignerEnabledKey, StakingSignerKeyContentKey, StakingSignerKeyPathKey, StakingRPCSignerEndpointKey) ) func getConsensusConfig(v *viper.Viper) snowball.Parameters { @@ -639,74 +637,15 @@ func getStakingTLSCert(v *viper.Viper) (tls.Certificate, error) { } } -func getStakingSigner(v *viper.Viper) (bls.Signer, error) { - if v.GetBool(StakingEphemeralSignerEnabledKey) { - key, err := localsigner.New() - if err != nil { - return nil, fmt.Errorf("couldn't generate ephemeral signing key: %w", err) - } - return key, nil - } - - if v.IsSet(StakingSignerKeyContentKey) { - signerKeyRawContent := v.GetString(StakingSignerKeyContentKey) - signerKeyContent, err := base64.StdEncoding.DecodeString(signerKeyRawContent) - if err != nil { - return nil, fmt.Errorf("unable to decode base64 content: %w", err) - } - key, err := localsigner.FromBytes(signerKeyContent) - if err != nil { - return nil, fmt.Errorf("couldn't parse signing key: %w", err) - } - return key, nil - } - - signingKeyPath := getExpandedArg(v, StakingSignerKeyPathKey) - _, err := os.Stat(signingKeyPath) - if !errors.Is(err, fs.ErrNotExist) { - signingKeyBytes, err := os.ReadFile(signingKeyPath) - if err != nil { - return nil, err - } - key, err := localsigner.FromBytes(signingKeyBytes) - if err != nil { - return nil, fmt.Errorf("couldn't parse signing key: %w", err) - } - return key, nil - } - - if v.IsSet(StakingSignerKeyPathKey) { - return nil, errMissingStakingSigningKeyFile - } - - key, err := localsigner.New() - if err != nil { - return nil, fmt.Errorf("couldn't generate new signing key: %w", err) - } - - if err := os.MkdirAll(filepath.Dir(signingKeyPath), perms.ReadWriteExecute); err != nil { - return nil, fmt.Errorf("couldn't create path for signing key at %s: %w", signingKeyPath, err) - } - - keyBytes := key.ToBytes() - if err := os.WriteFile(signingKeyPath, keyBytes, perms.ReadWrite); err != nil { - return nil, fmt.Errorf("couldn't write new signing key to %s: %w", signingKeyPath, err) - } - if err := os.Chmod(signingKeyPath, perms.ReadOnly); err != nil { - return nil, fmt.Errorf("couldn't restrict permissions on new signing key at %s: %w", signingKeyPath, err) - } - return key, nil -} - func getStakingConfig(v *viper.Viper, networkID uint32) (node.StakingConfig, error) { config := node.StakingConfig{ SybilProtectionEnabled: v.GetBool(SybilProtectionEnabledKey), SybilProtectionDisabledWeight: v.GetUint64(SybilProtectionDisabledWeightKey), PartialSyncPrimaryNetwork: v.GetBool(PartialSyncPrimaryNetworkKey), - StakingKeyPath: getExpandedArg(v, StakingTLSKeyPathKey), - StakingCertPath: getExpandedArg(v, StakingCertPathKey), - StakingSignerPath: getExpandedArg(v, StakingSignerKeyPathKey), + StakingTLSKeyPath: getExpandedArg(v, StakingTLSKeyPathKey), + StakingTLSCertPath: getExpandedArg(v, StakingCertPathKey), } + if !config.SybilProtectionEnabled && config.SybilProtectionDisabledWeight == 0 { return node.StakingConfig{}, errSybilProtectionDisabledStakerWeights } @@ -720,10 +659,12 @@ func getStakingConfig(v *viper.Viper, networkID uint32) (node.StakingConfig, err if err != nil { return node.StakingConfig{}, err } - config.StakingSigningKey, err = getStakingSigner(v) + + config.StakingSignerConfig, err = getStakingSignerConfig(v) if err != nil { return node.StakingConfig{}, err } + if networkID != constants.MainnetID && networkID != constants.FujiID { config.UptimeRequirement = v.GetFloat64(UptimeRequirementKey) config.MinValidatorStake = v.GetUint64(MinValidatorStakeKey) @@ -736,6 +677,7 @@ func getStakingConfig(v *viper.Viper, networkID uint32) (node.StakingConfig, err config.RewardConfig.MintingPeriod = v.GetDuration(StakeMintingPeriodKey) config.RewardConfig.SupplyCap = v.GetUint64(StakeSupplyCapKey) config.MinDelegationFee = v.GetUint32(MinDelegatorFeeKey) + switch { case config.UptimeRequirement < 0 || config.UptimeRequirement > 1: return node.StakingConfig{}, errInvalidUptimeRequirement @@ -757,9 +699,48 @@ func getStakingConfig(v *viper.Viper, networkID uint32) (node.StakingConfig, err } else { config.StakingConfig = genesis.GetStakingConfig(networkID) } + return config, nil } +func getStakingSignerConfig(v *viper.Viper) (any, error) { + // A maximum of one signer option can be set + bools := bag.Of( + v.GetBool(StakingEphemeralSignerEnabledKey), + v.IsSet(StakingSignerKeyContentKey), + v.IsSet(StakingSignerKeyPathKey), + v.IsSet(StakingRPCSignerEndpointKey), + ) + if bools.Count(true) > 1 { + return node.StakingConfig{}, errInvalidSignerConfig + } + + switch { + case v.GetBool(StakingEphemeralSignerEnabledKey): + return node.EphemeralSignerConfig{}, nil + + case v.IsSet(StakingSignerKeyContentKey): + return node.ContentKeyConfig{ + SignerKeyRawContent: getExpandedArg(v, StakingSignerKeyContentKey), + }, nil + + case v.IsSet(StakingRPCSignerEndpointKey): + return node.RPCSignerConfig{ + StakingSignerRPC: getExpandedArg(v, StakingRPCSignerEndpointKey), + }, nil + + case v.IsSet(StakingSignerKeyPathKey): + return node.SignerPathConfig{ + SignerKeyPath: getExpandedArg(v, StakingSignerKeyPathKey), + }, nil + + default: + return node.DefaultSignerConfig{ + SignerKeyPath: getExpandedArg(v, StakingSignerKeyPathKey), + }, nil + } +} + func getTxFeeConfig(v *viper.Viper, networkID uint32) genesis.TxFeeConfig { if networkID != constants.MainnetID && networkID != constants.FujiID { return genesis.TxFeeConfig{ @@ -1450,6 +1431,7 @@ func GetNodeConfig(v *viper.Viper) (node.Config, error) { nodeConfig.ProcessContextFilePath = getExpandedArg(v, ProcessContextFileKey) nodeConfig.ProvidedFlags = providedFlags(v) + return nodeConfig, nil } diff --git a/config/config_test.go b/config/config_test.go index 0aed70c79f44..af88fb86f724 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -10,6 +10,7 @@ import ( "log" "os" "path/filepath" + "strings" "testing" "github.com/spf13/pflag" @@ -17,9 +18,11 @@ import ( "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/chains" + "github.com/ava-labs/avalanchego/config/node" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow/consensus/snowball" "github.com/ava-labs/avalanchego/subnets" + "github.com/ava-labs/avalanchego/utils/perms" ) const chainConfigFilenameExtension = ".ex" @@ -549,6 +552,92 @@ func TestGetSubnetConfigsFromFlags(t *testing.T) { } } +func TestGetStakingSigner(t *testing.T) { + testKey := "HLimS3vRibTMk9lZD4b+Z+GLuSBShvgbsu0WTLt2Kd4=" + testKeyPath1 := filepath.Join(t.TempDir(), ".avalanchego/staking/signer.key") + testKeyPath2 := strings.Replace(testKeyPath1, "001", "002", 1) // Anticipate the new temp dir that will be created + + tests := []struct { + name string + viperKeys string + config map[string]any + expectedSignerConfig any + expectedErr error + }{ + { + name: "default signer", + expectedSignerConfig: node.DefaultSignerConfig{ + SignerKeyPath: testKeyPath2, + }, + }, + { + name: "ephemeral signer", + config: map[string]any{StakingEphemeralSignerEnabledKey: true}, + expectedSignerConfig: node.EphemeralSignerConfig{}, + }, + { + name: "content key", + config: map[string]any{StakingSignerKeyContentKey: testKey}, + expectedSignerConfig: node.ContentKeyConfig{ + SignerKeyRawContent: testKey, + }, + }, + { + name: "file key", + config: map[string]any{ + StakingSignerKeyPathKey: func() string { + require.NoError(t, os.MkdirAll( + filepath.Dir(testKeyPath1), + os.ModePerm, + )) + + bytes, err := base64.StdEncoding.DecodeString(testKey) + require.NoError(t, err) + require.NoError(t, os.WriteFile(testKeyPath1, bytes, perms.ReadWrite)) + return testKeyPath1 + }(), + }, + expectedSignerConfig: node.SignerPathConfig{ + SignerKeyPath: testKeyPath1, + }, + }, + { + name: "rpc signer", + config: map[string]any{StakingRPCSignerEndpointKey: "localhost"}, + expectedSignerConfig: node.RPCSignerConfig{ + StakingSignerRPC: "localhost", + }, + }, + { + name: "multiple configurations set", + config: map[string]any{ + StakingEphemeralSignerEnabledKey: true, + StakingSignerKeyContentKey: testKey, + }, + expectedErr: errInvalidSignerConfig, + }, + } + + // required for proper write permissions for the default signer-key location + t.Setenv("HOME", t.TempDir()) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + v := setupViperFlags() + + for key, value := range tt.config { + v.Set(key, value) + } + + config, err := GetNodeConfig(v) + + require.ErrorIs(err, tt.expectedErr) + require.Equal(tt.expectedSignerConfig, config.StakingSignerConfig) + }) + } +} + // setups config json file and writes content func setupConfigJSON(t *testing.T, rootPath string, value string) string { configFilePath := filepath.Join(rootPath, "config.json") diff --git a/config/flags.go b/config/flags.go index ac77383153c0..a5bde8f53d32 100644 --- a/config/flags.go +++ b/config/flags.go @@ -264,6 +264,7 @@ func addNodeFlags(fs *pflag.FlagSet) { fs.Bool(StakingEphemeralSignerEnabledKey, false, "If true, the node uses an ephemeral staking signer key") fs.String(StakingSignerKeyPathKey, defaultStakingSignerKeyPath, fmt.Sprintf("Path to the signer private key for staking. Ignored if %s is specified", StakingSignerKeyContentKey)) fs.String(StakingSignerKeyContentKey, "", "Specifies base64 encoded signer private key for staking") + fs.String(StakingRPCSignerEndpointKey, "", "Specifies the RPC endpoint of the staking signer") fs.Bool(SybilProtectionEnabledKey, true, "Enables sybil protection. If enabled, Network TLS is required") fs.Uint64(SybilProtectionDisabledWeightKey, 100, "Weight to provide to each peer when sybil protection is disabled") fs.Bool(PartialSyncPrimaryNetworkKey, false, "Only sync the P-chain on the Primary Network. If the node is a Primary Network validator, it will report unhealthy") diff --git a/config/keys.go b/config/keys.go index 20189b3df565..9a98d206657f 100644 --- a/config/keys.go +++ b/config/keys.go @@ -85,6 +85,7 @@ const ( StakingEphemeralSignerEnabledKey = "staking-ephemeral-signer-enabled" StakingSignerKeyPathKey = "staking-signer-key-file" StakingSignerKeyContentKey = "staking-signer-key-file-content" + StakingRPCSignerEndpointKey = "staking-rpc-signer-endpoint" SybilProtectionEnabledKey = "sybil-protection-enabled" SybilProtectionDisabledWeightKey = "sybil-protection-disabled-weight" NetworkInitialTimeoutKey = "network-initial-timeout" diff --git a/config/node/config.go b/config/node/config.go index a97c3b84df2e..50c8a5c1188e 100644 --- a/config/node/config.go +++ b/config/node/config.go @@ -19,7 +19,6 @@ import ( "github.com/ava-labs/avalanchego/subnets" "github.com/ava-labs/avalanchego/trace" "github.com/ava-labs/avalanchego/upgrade" - "github.com/ava-labs/avalanchego/utils/crypto/bls" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/profiler" "github.com/ava-labs/avalanchego/utils/set" @@ -76,12 +75,30 @@ type StakingConfig struct { SybilProtectionEnabled bool `json:"sybilProtectionEnabled"` PartialSyncPrimaryNetwork bool `json:"partialSyncPrimaryNetwork"` StakingTLSCert tls.Certificate `json:"-"` - StakingSigningKey bls.Signer `json:"-"` SybilProtectionDisabledWeight uint64 `json:"sybilProtectionDisabledWeight"` - // not accessed but used for logging - StakingKeyPath string `json:"stakingKeyPath"` - StakingCertPath string `json:"stakingCertPath"` - StakingSignerPath string `json:"stakingSignerPath"` + StakingTLSKeyPath string `json:"stakingTLSKeyPath"` + StakingTLSCertPath string `json:"stakingTLSCertPath"` + + // This is set in order to instatiate the correct signer type at runtime. + StakingSignerConfig any +} + +type EphemeralSignerConfig struct{} + +type ContentKeyConfig struct { + SignerKeyRawContent string +} + +type SignerPathConfig struct { + SignerKeyPath string +} + +type DefaultSignerConfig struct { + SignerKeyPath string +} + +type RPCSignerConfig struct { + StakingSignerRPC string } type StateSyncConfig struct { diff --git a/node/node.go b/node/node.go index ee442c3d2953..4548cb7e77a1 100644 --- a/node/node.go +++ b/node/node.go @@ -134,7 +134,12 @@ func New( Config: config, } - pop, err := signer.NewProofOfPossession(n.Config.StakingSigningKey) + n.StakingSigner, err = NewStakingSigner(config.StakingSignerConfig) + if err != nil { + return nil, fmt.Errorf("problem initializing staking signer: %w", err) + } + + pop, err := signer.NewProofOfPossession(n.StakingSigner) if err != nil { return nil, fmt.Errorf("problem creating proof of possession: %w", err) } @@ -284,6 +289,7 @@ type Node struct { StakingTLSSigner crypto.Signer StakingTLSCert *staking.Certificate + StakingSigner bls.Signer // Storage for this node DB database.Database @@ -571,7 +577,7 @@ func (n *Node) initNetworking(reg prometheus.Registerer) error { err := n.vdrs.AddStaker( constants.PrimaryNetworkID, n.ID, - n.Config.StakingSigningKey.PublicKey(), + n.StakingSigner.PublicKey(), dummyTxID, n.Config.SybilProtectionDisabledWeight, ) @@ -610,7 +616,7 @@ func (n *Node) initNetworking(reg prometheus.Registerer) error { n.Config.NetworkConfig.Beacons = n.bootstrappers n.Config.NetworkConfig.TLSConfig = tlsConfig n.Config.NetworkConfig.TLSKey = tlsKey - n.Config.NetworkConfig.BLSKey = n.Config.StakingSigningKey + n.Config.NetworkConfig.BLSKey = n.StakingSigner n.Config.NetworkConfig.TrackedSubnets = n.Config.TrackedSubnets n.Config.NetworkConfig.UptimeCalculator = n.uptimeCalculator n.Config.NetworkConfig.UptimeRequirement = n.Config.UptimeRequirement @@ -1100,7 +1106,7 @@ func (n *Node) initChainManager(avaxAssetID ids.ID) error { SybilProtectionEnabled: n.Config.SybilProtectionEnabled, StakingTLSSigner: n.StakingTLSSigner, StakingTLSCert: n.StakingTLSCert, - StakingBLSKey: n.Config.StakingSigningKey, + StakingBLSKey: n.StakingSigner, Log: n.Log, LogFactory: n.LogFactory, VMManager: n.VMManager, @@ -1344,7 +1350,7 @@ func (n *Node) initInfoAPI() error { n.Log.Info("initializing info API") - pop, err := signer.NewProofOfPossession(n.Config.StakingSigningKey) + pop, err := signer.NewProofOfPossession(n.StakingSigner) if err != nil { return fmt.Errorf("problem creating proof of possession: %w", err) } @@ -1455,7 +1461,7 @@ func (n *Node) initHealthAPI() error { return "validator doesn't have a BLS key", nil } - nodePK := n.Config.StakingSigningKey.PublicKey() + nodePK := n.StakingSigner.PublicKey() if nodePK.Equals(vdrPK) { return "node has the correct BLS key", nil } @@ -1650,6 +1656,14 @@ func (n *Node) shutdown() { time.Sleep(n.Config.ShutdownWait) } + if n.StakingSigner != nil { + if err := n.StakingSigner.Shutdown(); err != nil { + n.Log.Debug( + "error during staking signer shutdown", + zap.Error(err), + ) + } + } if n.resourceManager != nil { n.resourceManager.Shutdown() } diff --git a/node/signer.go b/node/signer.go new file mode 100644 index 000000000000..c8ce0f42ed6f --- /dev/null +++ b/node/signer.go @@ -0,0 +1,106 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package node + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "time" + + "github.com/ava-labs/avalanchego/config/node" + "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner" + "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/rpcsigner" + "github.com/ava-labs/avalanchego/utils/perms" +) + +// NewStakingSigner returns a BLS signer based on the provided configuration. +func NewStakingSigner( + config any, +) (bls.Signer, error) { + switch cfg := config.(type) { + case node.EphemeralSignerConfig: + signer, err := localsigner.New() + if err != nil { + return nil, fmt.Errorf("couldn't generate ephemeral signer: %w", err) + } + + return signer, nil + + case node.ContentKeyConfig: + signerKeyContent, err := base64.StdEncoding.DecodeString(cfg.SignerKeyRawContent) + if err != nil { + return nil, fmt.Errorf("unable to decode base64 content: %w", err) + } + + signer, err := localsigner.FromBytes(signerKeyContent) + if err != nil { + return nil, fmt.Errorf("couldn't parse signing key: %w", err) + } + + return signer, nil + + case node.RPCSignerConfig: + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + signer, err := rpcsigner.NewClient(ctx, cfg.StakingSignerRPC) + cancel() + if err != nil { + return nil, fmt.Errorf("couldn't create rpc signer client: %w", err) + } + + return signer, nil + + case node.SignerPathConfig: + return createSignerFromFile(cfg.SignerKeyPath) + + case node.DefaultSignerConfig: + _, err := os.Stat(cfg.SignerKeyPath) + if !errors.Is(err, fs.ErrNotExist) { + return createSignerFromFile(cfg.SignerKeyPath) + } + return createSignerFromNewKey(cfg.SignerKeyPath) + + default: + return nil, fmt.Errorf("unsupported signer type: %T", cfg) + } +} + +func createSignerFromFile(signerKeyPath string) (bls.Signer, error) { + signingKeyBytes, err := os.ReadFile(signerKeyPath) + if err != nil { + return nil, fmt.Errorf("couldn't read signing key from %s: %w", signerKeyPath, err) + } + + signer, err := localsigner.FromBytes(signingKeyBytes) + if err != nil { + return nil, fmt.Errorf("couldn't parse signing key: %w", err) + } + + return signer, nil +} + +func createSignerFromNewKey(signerKeyPath string) (bls.Signer, error) { + signer, err := localsigner.New() + if err != nil { + return nil, fmt.Errorf("couldn't generate new signing key: %w", err) + } + + if err := os.MkdirAll(filepath.Dir(signerKeyPath), perms.ReadWriteExecute); err != nil { + return nil, fmt.Errorf("couldn't create path for signing key at %s: %w", signerKeyPath, err) + } + + keyBytes := signer.ToBytes() + if err := os.WriteFile(signerKeyPath, keyBytes, perms.ReadWrite); err != nil { + return nil, fmt.Errorf("couldn't write new signing key to %s: %w", signerKeyPath, err) + } + if err := os.Chmod(signerKeyPath, perms.ReadOnly); err != nil { + return nil, fmt.Errorf("couldn't restrict permissions on new signing key at %s: %w", signerKeyPath, err) + } + return signer, nil +} diff --git a/node/signer_test.go b/node/signer_test.go new file mode 100644 index 000000000000..471e5ce26e11 --- /dev/null +++ b/node/signer_test.go @@ -0,0 +1,41 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package node + +import ( + "log" + "testing" + + "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/config" +) + +func TestDefaultConfigInitializationUsesExistingDefaultKey(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + + require := require.New(t) + v := setupViperFlags() + + config1, err := config.GetNodeConfig(v) + require.NoError(err) + signer1, err := NewStakingSigner(config1.StakingSignerConfig) + require.NoError(err) + signer2, err := NewStakingSigner(config1.StakingSignerConfig) + require.NoError(err) + + require.Equal(signer1.PublicKey(), signer2.PublicKey()) +} + +func setupViperFlags() *viper.Viper { + v := viper.New() + fs := config.BuildFlagSet() + pflag.Parse() + if err := v.BindPFlags(fs); err != nil { + log.Fatal(err) + } + return v +} diff --git a/utils/crypto/bls/signer.go b/utils/crypto/bls/signer.go index 499b24d2cf3d..4d8f5cccbdc6 100644 --- a/utils/crypto/bls/signer.go +++ b/utils/crypto/bls/signer.go @@ -7,4 +7,5 @@ type Signer interface { PublicKey() *PublicKey Sign(msg []byte) (*Signature, error) SignProofOfPossession(msg []byte) (*Signature, error) + Shutdown() error } diff --git a/utils/crypto/bls/signer/localsigner/localsigner.go b/utils/crypto/bls/signer/localsigner/localsigner.go index 570f22736446..be4e4c36d0c9 100644 --- a/utils/crypto/bls/signer/localsigner/localsigner.go +++ b/utils/crypto/bls/signer/localsigner/localsigner.go @@ -75,3 +75,7 @@ func (s *LocalSigner) Sign(msg []byte) (*bls.Signature, error) { func (s *LocalSigner) SignProofOfPossession(msg []byte) (*bls.Signature, error) { return new(bls.Signature).Sign(s.sk, msg, bls.CiphersuiteProofOfPossession.Bytes()), nil } + +func (*LocalSigner) Shutdown() error { + return nil +} diff --git a/utils/crypto/bls/signer/rpcsigner/client.go b/utils/crypto/bls/signer/rpcsigner/client.go index 5c02d672352d..a5fa14eb54b6 100644 --- a/utils/crypto/bls/signer/rpcsigner/client.go +++ b/utils/crypto/bls/signer/rpcsigner/client.go @@ -5,8 +5,13 @@ package rpcsigner import ( "context" + "errors" + "fmt" + "time" "google.golang.org/grpc" + "google.golang.org/grpc/backoff" + "google.golang.org/grpc/credentials/insecure" "github.com/ava-labs/avalanchego/utils/crypto/bls" @@ -18,25 +23,48 @@ var _ bls.Signer = (*Client)(nil) type Client struct { client pb.SignerClient pk *bls.PublicKey + // grpc.ClientConn handles transient connection errors. + connection *grpc.ClientConn } -func NewClient(ctx context.Context, conn *grpc.ClientConn) (*Client, error) { +func NewClient(ctx context.Context, url string) (*Client, error) { + // TODO: figure out the best parameters here given the target block-time + opts := grpc.WithConnectParams(grpc.ConnectParams{ + Backoff: backoff.DefaultConfig, + // same as grpc default + MinConnectTimeout: 20 * time.Second, + }) + + // the rpc-signer client should call a proxy server (on the same machine) that forwards + // the request to the actual signer instead of relying on tls-credentials + conn, err := grpc.NewClient(url, opts, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, fmt.Errorf("failed to create rpc signer client: %w", err) + } + client := pb.NewSignerClient(conn) pubkeyResponse, err := client.PublicKey(ctx, &pb.PublicKeyRequest{}) if err != nil { - return nil, err + return nil, errors.Join( + fmt.Errorf("failed to get pubkey response: %w", err), + conn.Close(), + ) } pkBytes := pubkeyResponse.GetPublicKey() pk, err := bls.PublicKeyFromCompressedBytes(pkBytes) if err != nil { - return nil, err + return nil, errors.Join( + fmt.Errorf("failed to uncompress public key bytes: %w", err), + conn.Close(), + ) } return &Client{ - client: client, - pk: pk, + client: client, + pk: pk, + connection: conn, }, nil } @@ -47,19 +75,33 @@ func (c *Client) PublicKey() *bls.PublicKey { func (c *Client) Sign(message []byte) (*bls.Signature, error) { resp, err := c.client.Sign(context.TODO(), &pb.SignRequest{Message: message}) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to sign message: %w", err) } - signature := resp.GetSignature() - return bls.SignatureFromBytes(signature) + sigBytes := resp.GetSignature() + sig, err := bls.SignatureFromBytes(sigBytes) + if err != nil { + return nil, fmt.Errorf("failed to parse signature: %w", err) + } + return sig, nil } +// SignProofOfPossession produces a ProofOfPossession signature. +// See BLS spec for more details. func (c *Client) SignProofOfPossession(message []byte) (*bls.Signature, error) { resp, err := c.client.SignProofOfPossession(context.TODO(), &pb.SignProofOfPossessionRequest{Message: message}) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to sign proof of possession: %w", err) } - signature := resp.GetSignature() - return bls.SignatureFromBytes(signature) + sigBytes := resp.GetSignature() + sig, err := bls.SignatureFromBytes(sigBytes) + if err != nil { + return nil, fmt.Errorf("failed to parse signature: %w", err) + } + return sig, nil +} + +func (c *Client) Shutdown() error { + return fmt.Errorf("failed to close connection: %w", c.connection.Close()) }