diff --git a/cmd/horcrux/cmd/address.go b/cmd/horcrux/cmd/address.go index d51766fc..65872942 100644 --- a/cmd/horcrux/cmd/address.go +++ b/cmd/horcrux/cmd/address.go @@ -30,7 +30,7 @@ func addressCmd() *cobra.Command { Args: cobra.RangeArgs(1, 2), RunE: func(cmd *cobra.Command, args []string) error { - var pubKey crypto.PubKey + var pubKey tss.PubKey chainID := args[0] @@ -51,7 +51,7 @@ func addressCmd() *cobra.Command { return fmt.Errorf("error reading threshold key: %w, check that key is present for chain id: %s", err, chainID) } - pubKey = key.PubKey + pubKey = key.PubKey.(crypto.PubKey) case cconfig.SignModeSingle: err := config.Config.ValidateSingleSignerConfig() if err != nil { @@ -68,10 +68,10 @@ func addressCmd() *cobra.Command { default: panic(fmt.Errorf("unexpected sign mode: %s", config.Config.SignMode)) } + pubKeyComet := pubKey.(crypto.PubKey) + pubKeyAddress := pubKeyComet.Address() - pubKeyAddress := pubKey.Address() - - pubKeyJSON, err := cconfig.PubKey("", pubKey) + pubKeyJSON, err := cconfig.PubKey("", pubKeyComet) if err != nil { return err } @@ -87,7 +87,7 @@ func addressCmd() *cobra.Command { return err } output.ValConsAddress = bech32ValConsAddress - pubKeyBech32, err := cconfig.PubKey(args[1], pubKey) + pubKeyBech32, err := cconfig.PubKey(args[1], pubKeyComet) if err != nil { return err } diff --git a/cmd/horcrux/cmd/config.go b/cmd/horcrux/cmd/config.go index da434e86..1a5e0629 100644 --- a/cmd/horcrux/cmd/config.go +++ b/cmd/horcrux/cmd/config.go @@ -135,7 +135,7 @@ for threshold signer mode, --cosigner flags and --threshold flag are required. f := cmd.Flags() f.StringP(flagSignMode, "m", string(cconfig.SignModeThreshold), - `sign mode, "threshold" (recommended) or "single" (unsupported). threshold mode requires --cosigner (multiple) and --threshold`, // nolint + `sign mode, "threshold" (recommended) or "single" (unsupported). threshold mode requires --cosigner (multiple) and --threshold`, //nolint ) f.StringSliceP(flagNode, "n", []string{}, "chain cosigner in format tcp://{node-addr}:{privval-port} \n"+ "(e.g. --node tcp://sentry-1:1234 --node tcp://sentry-2:1234 --node tcp://sentry-3:1234 )") diff --git a/cmd/horcrux/cmd/shards.go b/cmd/horcrux/cmd/shards.go index 3f3e12d5..dbf31c5a 100644 --- a/cmd/horcrux/cmd/shards.go +++ b/cmd/horcrux/cmd/shards.go @@ -21,7 +21,7 @@ import ( "path/filepath" "github.com/strangelove-ventures/horcrux/pkg/cosigner/nodesecurity" - tss "github.com/strangelove-ventures/horcrux/pkg/tss" + "github.com/strangelove-ventures/horcrux/pkg/tss" "github.com/spf13/cobra" ) diff --git a/pkg/cosigner/remote.go b/pkg/cosigner/client.go similarity index 88% rename from pkg/cosigner/remote.go rename to pkg/cosigner/client.go index 13c4b093..a7535815 100644 --- a/pkg/cosigner/remote.go +++ b/pkg/cosigner/client.go @@ -2,11 +2,9 @@ package cosigner import ( "context" - "fmt" "net/url" "time" - cometcrypto "github.com/cometbft/cometbft/crypto" "github.com/google/uuid" "github.com/strangelove-ventures/horcrux/signer/proto" "google.golang.org/grpc" @@ -69,15 +67,15 @@ func (cosigner *RemoteCosigner) GetAddress() string { // GetPubKey returns public key of the validator. // Implements Cosigner interface -func (cosigner *RemoteCosigner) GetPubKey(_ string) (cometcrypto.PubKey, error) { - return nil, fmt.Errorf("unexpected call to RemoteCosigner.GetPubKey") -} +// func (cosigner *RemoteCosigner) GetPubKey(_ string) (cometcrypto.PubKey, error) { +// return nil, fmt.Errorf("unexpected call to RemoteCosigner.GetPubKey") +// } // VerifySignature validates a signed payload against the public key. // Implements Cosigner interface -func (cosigner *RemoteCosigner) VerifySignature(_ string, _, _ []byte) bool { - return false -} +// func (cosigner *RemoteCosigner) VerifySignature(_ string, _, _ []byte) bool { +// return false +// } func getGRPCClient(address string) (proto.CosignerClient, error) { var grpcAddress string @@ -114,7 +112,7 @@ func (cosigner *RemoteCosigner) GetNonces( for i, nonces := range res.Nonces { out[i] = &CosignerUUIDNonces{ UUID: uuid.UUID(nonces.Uuid), - Nonces: CosignerNoncesFromProto(nonces.Nonces), + Nonces: FromProtoToNonces(nonces.Nonces), } } return out, nil @@ -123,7 +121,7 @@ func (cosigner *RemoteCosigner) GetNonces( // Implements the cosigner interface func (cosigner *RemoteCosigner) SetNoncesAndSign( ctx context.Context, - req CosignerSetNoncesAndSignRequest) (*CosignerSignResponse, error) { + req CosignerSetNoncesAndSignRequest) (*SignatureResponse, error) { res, err := cosigner.Client.SetNoncesAndSign(ctx, &proto.SetNoncesAndSignRequest{ Uuid: req.Nonces.UUID[:], ChainID: req.ChainID, @@ -134,13 +132,14 @@ func (cosigner *RemoteCosigner) SetNoncesAndSign( if err != nil { return nil, err } - return &CosignerSignResponse{ + return &SignatureResponse{ NoncePublic: res.GetNoncePublic(), Timestamp: time.Unix(0, res.GetTimestamp()), Signature: res.GetSignature(), }, nil } +// TODO: This should move to ThresholdValidator. Its is not the responsibility of the cosigner func (cosigner *RemoteCosigner) Sign( ctx context.Context, req CosignerSignBlockRequest, diff --git a/pkg/cosigner/cosigner.go b/pkg/cosigner/cosigner.go index 3564990a..01ac2dda 100644 --- a/pkg/cosigner/cosigner.go +++ b/pkg/cosigner/cosigner.go @@ -20,44 +20,52 @@ import ( /* // threshold-ed25519 type MPC struct { - // our own cosigner + // our own cosigner (i.e server) MyCosigner *LocalCosigner // TODO Should be an interface as well. + server - // peer cosigners + clients // peers to call + // peer cosigners (i.e clients to call) peerCosigners []*RemoteCosigner // "i.e clients to call" + cosignerHealth *CosignerHealth + + // FIX: This f-up a lot. Now its like 3-4 places that + // spaggettio leaders, cosigners etc etc + nonceCache *CosignerNonceCache + } -type Localcosigner interface { +type ServerCosigner interface { // TODO - add methods } -type Remotecosigner interface { +type ClientCosigner interface { // TODO - add methods } */ -// CosignerSignRequest is sent to a co-signer to obtain their signature for the SignBytes +// SignatureRequest is sent to a co-signer to obtain their signature for the SignBytes // The SignBytes should be a serialized block -type CosignerSignRequest struct { +type SignatureRequest struct { ChainID string SignBytes []byte UUID uuid.UUID } -type CosignerSignResponse struct { +type SignatureResponse struct { NoncePublic []byte Timestamp time.Time Signature []byte } -type CosignerNonce struct { - SourceID int - DestinationID int +type Nonce struct { + SourceID int // Client ID + DestinationID int // Server ID PubKey []byte Share []byte Signature []byte } -func (secretPart *CosignerNonce) toProto() *proto.Nonce { +func (secretPart *Nonce) toProto() *proto.Nonce { return &proto.Nonce{ SourceID: int32(secretPart.SourceID), DestinationID: int32(secretPart.DestinationID), @@ -67,18 +75,19 @@ func (secretPart *CosignerNonce) toProto() *proto.Nonce { } } -// CosignerNonces are a list of CosignerNonce -type CosignerNonces []CosignerNonce +// Nonces is a list of CosignerNonce +type Nonces []Nonce -func (secretParts CosignerNonces) toProto() (out []*proto.Nonce) { +func (secretParts Nonces) toProto() (out []*proto.Nonce) { for _, secretPart := range secretParts { out = append(out, secretPart.toProto()) } return } -func CosignerNonceFromProto(secretPart *proto.Nonce) CosignerNonce { - return CosignerNonce{ +// FromProtoToNonce converts a proto.Nonce to a Nonce +func FromProtoToNonce(secretPart *proto.Nonce) Nonce { + return Nonce{ SourceID: int(secretPart.SourceID), DestinationID: int(secretPart.DestinationID), PubKey: secretPart.PubKey, @@ -87,10 +96,10 @@ func CosignerNonceFromProto(secretPart *proto.Nonce) CosignerNonce { } } -func CosignerNoncesFromProto(secretParts []*proto.Nonce) []CosignerNonce { - out := make([]CosignerNonce, len(secretParts)) +func FromProtoToNonces(secretParts []*proto.Nonce) []Nonce { + out := make([]Nonce, len(secretParts)) for i, secretPart := range secretParts { - out[i] = CosignerNonceFromProto(secretPart) + out[i] = FromProtoToNonce(secretPart) } return out } @@ -103,9 +112,11 @@ type CosignerSignBlockRequest struct { type CosignerSignBlockResponse struct { Signature []byte } + +// CosignerUUIDNonces type CosignerUUIDNonces struct { - UUID uuid.UUID - Nonces CosignerNonces + UUID uuid.UUID // UUID is the unique identifier of the nonce + Nonces Nonces } func (n *CosignerUUIDNonces) For(id int) *CosignerUUIDNonces { diff --git a/pkg/cosigner/icosigner.go b/pkg/cosigner/icosigner.go new file mode 100644 index 00000000..91b1b26c --- /dev/null +++ b/pkg/cosigner/icosigner.go @@ -0,0 +1,5 @@ +package cosigner + +type iCosigner interface { + Health() +} diff --git a/pkg/cosigner/icosigner_security.go b/pkg/cosigner/icosigner_security.go index 552ef943..5cf5344f 100644 --- a/pkg/cosigner/icosigner_security.go +++ b/pkg/cosigner/icosigner_security.go @@ -10,7 +10,7 @@ type ICosignerSecurity interface { id int, noncePub []byte, nonceShare []byte, - ) (CosignerNonce, error) + ) (Nonce, error) // DecryptAndVerify decrypts the nonce and verifies the signature to authenticate the source cosigner. DecryptAndVerify( diff --git a/pkg/cosigner/local_cosigner_test.go b/pkg/cosigner/local_cosigner_test.go index e7cade8f..47f1b133 100644 --- a/pkg/cosigner/local_cosigner_test.go +++ b/pkg/cosigner/local_cosigner_test.go @@ -111,7 +111,8 @@ func testLocalCosignerSign(t *testing.T, threshold, total uint8, security []cosi privKeyBytes := [64]byte{} copy(privKeyBytes[:], privateKey[:]) privShards := tsed25519.DealShares(tsed25519.ExpandSecret(privKeyBytes[:32]), threshold, total) - pubKey := privateKey.PubKey() + // Returns the public key from the private key and type asserts it to an tss key. + pubKey := privateKey.PubKey().(tss.PubKey) cfg := config.Config{ ThresholdModeConfig: &config.ThresholdModeConfig{ @@ -125,7 +126,7 @@ func testLocalCosignerSign(t *testing.T, threshold, total uint8, security []cosi tmpDir := t.TempDir() thresholdCosigners := make([]*cosigner.LocalCosigner, threshold) - nonces := make([][]cosigner.CosignerNonce, threshold) + nonces := make([][]cosigner.Nonce, threshold) now := time.Now() @@ -201,7 +202,7 @@ func testLocalCosignerSign(t *testing.T, threshold, total uint8, security []cosi sigs := make([]types.PartialSignature, threshold) for i, local_cosigner := range thresholdCosigners { - cosignerNonces := make([]cosigner.CosignerNonce, 0, threshold-1) + cosignerNonces := make([]cosigner.Nonce, 0, threshold-1) for j, nonce := range nonces { if i == j { diff --git a/pkg/cosigner/nodesecurity/cosigner_security_ecies.go b/pkg/cosigner/nodesecurity/cosigner_security_ecies.go index e29d8e68..60446146 100644 --- a/pkg/cosigner/nodesecurity/cosigner_security_ecies.go +++ b/pkg/cosigner/nodesecurity/cosigner_security_ecies.go @@ -138,8 +138,8 @@ func (c *CosignerSecurityECIES) GetID() int { // EncryptAndSign encrypts the nonce and signs it for authentication. func (c *CosignerSecurityECIES) EncryptAndSign( - id int, noncePub []byte, nonceShare []byte) (cosigner.CosignerNonce, error) { - nonce := cosigner.CosignerNonce{ + id int, noncePub []byte, nonceShare []byte) (cosigner.Nonce, error) { + nonce := cosigner.Nonce{ SourceID: c.key.ID, } @@ -208,7 +208,7 @@ func (c *CosignerSecurityECIES) DecryptAndVerify( return nil, nil, fmt.Errorf("unknown cosigner: %d", id) } - digestMsg := cosigner.CosignerNonce{ + digestMsg := cosigner.Nonce{ SourceID: id, PubKey: encryptedNoncePub, Share: encryptedNonceShare, diff --git a/pkg/cosigner/nodesecurity/cosigner_security_rsa.go b/pkg/cosigner/nodesecurity/cosigner_security_rsa.go index b89cd250..065116cc 100644 --- a/pkg/cosigner/nodesecurity/cosigner_security_rsa.go +++ b/pkg/cosigner/nodesecurity/cosigner_security_rsa.go @@ -129,8 +129,8 @@ func (c *CosignerSecurityRSA) GetID() int { } // EncryptAndSign encrypts the nonce and signs it for authentication. -func (c *CosignerSecurityRSA) EncryptAndSign(id int, noncePub []byte, nonceShare []byte) (cosigner.CosignerNonce, error) { - nonce := cosigner.CosignerNonce{ +func (c *CosignerSecurityRSA) EncryptAndSign(id int, noncePub []byte, nonceShare []byte) (cosigner.Nonce, error) { + nonce := cosigner.Nonce{ SourceID: c.key.ID, } @@ -195,7 +195,7 @@ func (c *CosignerSecurityRSA) DecryptAndVerify( return nil, nil, fmt.Errorf("unknown cosigner: %d", id) } - digestMsg := cosigner.CosignerNonce{ + digestMsg := cosigner.Nonce{ SourceID: id, PubKey: encryptedNoncePub, Share: encryptedNonceShare, diff --git a/pkg/cosigner/local.go b/pkg/cosigner/server.go similarity index 96% rename from pkg/cosigner/local.go rename to pkg/cosigner/server.go index 3646d2b6..491cd108 100644 --- a/pkg/cosigner/local.go +++ b/pkg/cosigner/server.go @@ -162,7 +162,7 @@ func (cosigner *LocalCosigner) getChainState(chainID string) (*ChainState, error // Asserting cs (type any) is actually of type *ChainState ccs, ok := cs.(*ChainState) if !ok { - return nil, fmt.Errorf("Expected: (*ChainState), actual: (%T)", cs) + return nil, fmt.Errorf("expected: (*ChainState), actual: (%T)", cs) } return ccs, nil @@ -214,10 +214,10 @@ func (cosigner *LocalCosigner) VerifySignature(chainID string, payload, signatur // 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) { +func (cosigner *LocalCosigner) sign(req SignatureRequest) (SignatureResponse, error) { chainID := req.ChainID - res := CosignerSignResponse{} + res := SignatureResponse{} ccs, err := cosigner.getChainState(chainID) if err != nil { @@ -288,7 +288,7 @@ func (cosigner *LocalCosigner) generateNonces() ([]types.Nonces, error) { // TODO: This should only generate nonces for the cosigners that are online // although it might doesnt matter if we arent doing DKG - // Should call the interface + // Should call an interface: dealnonce or something nonces, err := tss.NonceGenerator{}.GenerateNonces( uint8(cosigner.config.Config.ThresholdModeConfig.Threshold), uint8(total), @@ -359,7 +359,7 @@ func (cosigner *LocalCosigner) GetNonces( var eg errgroup.Group - nonces := make([]CosignerNonce, total-1) + nonces := make([]Nonce, total-1) for i := 0; i < total; i++ { peerID := i + 1 @@ -433,8 +433,8 @@ func (cosigner *LocalCosigner) generateNoncesIfNecessary(uuid uuid.UUID) (*types func (cosigner *LocalCosigner) getNonce( meta *types.NoncesWithExpiration, peerID int, -) (CosignerNonce, error) { - zero := CosignerNonce{} +) (Nonce, error) { + zero := Nonce{} id := cosigner.GetIndex() @@ -448,7 +448,7 @@ func (cosigner *LocalCosigner) getNonce( } // setNonce stores a nonce provided by another cosigner -func (cosigner *LocalCosigner) setNonce(uuid uuid.UUID, nonce CosignerNonce) error { +func (cosigner *LocalCosigner) setNonce(uuid uuid.UUID, nonce Nonce) error { // Verify the source signature if nonce.Signature == nil { return errors.New("signature field is required") @@ -486,7 +486,7 @@ func (cosigner *LocalCosigner) setNonce(uuid uuid.UUID, nonce CosignerNonce) err func (cosigner *LocalCosigner) SetNoncesAndSign( _ context.Context, - req CosignerSetNoncesAndSignRequest) (*CosignerSignResponse, error) { + req CosignerSetNoncesAndSignRequest) (*SignatureResponse, error) { chainID := req.ChainID if err := cosigner.LoadSignStateIfNecessary(chainID); err != nil { @@ -510,7 +510,7 @@ func (cosigner *LocalCosigner) SetNoncesAndSign( return nil, err } - res, err := cosigner.sign(CosignerSignRequest{ + res, err := cosigner.sign(SignatureRequest{ UUID: req.Nonces.UUID, ChainID: chainID, SignBytes: req.SignBytes, diff --git a/pkg/tss/threshold-ed25519_signer_soft.go b/pkg/tss/threshold-ed25519_signer_soft.go index d05c718a..dc781d4f 100644 --- a/pkg/tss/threshold-ed25519_signer_soft.go +++ b/pkg/tss/threshold-ed25519_signer_soft.go @@ -23,7 +23,7 @@ func CreateEd25519ThresholdSignShards(pv privval.FilePVKey, threshold, shards ui out := make([]Ed25519Key, shards) for i, shard := range privShards { out[i] = Ed25519Key{ - PubKey: pv.PubKey, + PubKey: pv.PubKey.(PubKey), PrivateShard: shard, ID: i + 1, } @@ -125,7 +125,7 @@ func (ng NonceGenerator) GenerateNonces(threshold, total uint8) (types.Nonces, e Shares: make([][]byte, total), } - // The lenght of shares is equal to total + // The length of shares is equal to total shares := tsed25519.DealShares(secret, threshold, total) for i, sh := range shares { diff --git a/pkg/tss/threshold_signer.go b/pkg/tss/threshold_signer.go index 65785777..be1bbe88 100644 --- a/pkg/tss/threshold_signer.go +++ b/pkg/tss/threshold_signer.go @@ -11,7 +11,7 @@ import ( // LoadVaultKeyFromFile loads the persistent ThresholdSignerKey from file. func LoadVaultKeyFromFile(file string) (Ed25519Key, error) { - //pvKey := VaultKey{} + // pvKey := VaultKey{} var pvKey Ed25519Key keyJSONBytes, err := os.ReadFile(file) if err != nil || len(keyJSONBytes) == 0 { diff --git a/pkg/tss/types.go b/pkg/tss/types.go index df4e5355..174a6cf6 100644 --- a/pkg/tss/types.go +++ b/pkg/tss/types.go @@ -7,12 +7,23 @@ import ( cometcrypto "github.com/cometbft/cometbft/crypto" cometcryptoed25519 "github.com/cometbft/cometbft/crypto/ed25519" cometcryptoencoding "github.com/cometbft/cometbft/crypto/encoding" + cometbytes "github.com/cometbft/cometbft/libs/bytes" cometjson "github.com/cometbft/cometbft/libs/json" "github.com/cometbft/cometbft/privval" cometprotocrypto "github.com/cometbft/cometbft/proto/tendermint/crypto" "github.com/tendermint/go-amino" ) +type Address = cometbytes.HexBytes + +type PubKey interface { + Address() Address + Bytes() []byte + VerifySignature(msg []byte, sig []byte) bool + // Equals(PubKey) bool + Type() string +} + /* type ISignerKey interface { MarshalJSON() ([]byte, error) @@ -23,9 +34,9 @@ type ISignerKey interface { // Ed25519Key is a single Ed255219 key shard for an m-of-n threshold signer. // TODO: This should be renamed to SignerEd25519 and tbh Private shard should private. type PersistentEd25519Key struct { - pubKey cometcrypto.PubKey // Public key of the persistent shard. Pubkey is the same for all shards. - privateShard []byte // - index int // Shamir index of this shard + pubKey PubKey // Public key of the persistent shard. Pubkey is the same for all shards. + privateShard []byte // + index int // Shamir index of this shard } /* @@ -41,10 +52,27 @@ type PersistentEd25519Key struct { return key.Id } */ + +/* +Public Private + + type AssymetricKey struct { + PubKey cometcrypto.PubKey `json:"pubKey"` + PrivateShard []byte `json:"privateShard"` + } + + type AssymetricKeyShard struct { + AssymetricKey + ID int `json:"id"` // ID is the Shamir index or this shard. + +} + +type Ed25519Key +*/ type Ed25519Key struct { - PubKey cometcrypto.PubKey `json:"pubKey"` - PrivateShard []byte `json:"privateShard"` - ID int `json:"id"` + PubKey PubKey `json:"pubKey"` + PrivateShard []byte `json:"privateShard"` + ID int `json:"id"` } type VaultKey Ed25519Key @@ -53,7 +81,7 @@ type VaultKey Ed25519Key func (key *Ed25519Key) MarshalJSON() ([]byte, error) { type Alias Ed25519Key - protoPubkey, err := cometcryptoencoding.PubKeyToProto(key.PubKey) + protoPubkey, err := cometcryptoencoding.PubKeyToProto(key.PubKey.(cometcrypto.PubKey)) if err != nil { return nil, err } @@ -112,11 +140,11 @@ func (key *Ed25519Key) UnmarshalJSON(data []byte) error { } } - key.PubKey = pubkey + key.PubKey = pubkey.(PubKey) return nil } -// ReadCometBFTPrivValidatorFile reads in a privval.FilePVKey from a given file. +// ReadCometBFTPrivValidatorFile reads in a Comet privval.FilePVKey from a given file. func ReadCometBFTPrivValidatorFile(filename string) (out privval.FilePVKey, err error) { var bz []byte if bz, err = os.ReadFile(filename); err != nil { @@ -133,6 +161,9 @@ type VaultPrivateKey interface { } // WriteToFile writes a key structure to a given file name. +// +// It json.Marshal's the key structure and writes the result to to file with the +// 0600 permission. func WriteToFile[VPK VaultPrivateKey](privateshare VPK, file string) error { jsonBytes, err := json.Marshal(&privateshare) if err != nil { diff --git a/proto/strangelove/horcrux/cosigner.proto b/proto/strangelove/horcrux/cosigner.proto index 52475b85..c0153e8a 100644 --- a/proto/strangelove/horcrux/cosigner.proto +++ b/proto/strangelove/horcrux/cosigner.proto @@ -4,31 +4,15 @@ package strangelove.horcrux; option go_package = "github.com/strangelove-ventures/horcrux/signer/proto"; service Cosigner { - rpc SignBlock (SignBlockRequest) returns (SignBlockResponse) {} rpc SetNoncesAndSign (SetNoncesAndSignRequest) returns (SetNoncesAndSignResponse) {} rpc GetNonces (GetNoncesRequest) returns (GetNoncesResponse) {} - rpc TransferLeadership (TransferLeadershipRequest) returns (TransferLeadershipResponse) {} - rpc GetLeader (GetLeaderRequest) returns (GetLeaderResponse) {} rpc Ping(PingRequest) returns (PingResponse) {} -} -message Block { - int64 height = 1; - int64 round = 2; - int32 step = 3; - bytes signBytes = 4; - int64 timestamp = 5; + rpc SignBlock (SignBlockRequest) returns (SignBlockResponse) {} + rpc TransferLeadership (TransferLeadershipRequest) returns (TransferLeadershipResponse) {} + rpc GetLeader (GetLeaderRequest) returns (GetLeaderResponse) {} } -message SignBlockRequest { - string chainID = 1; - Block block = 2; -} - -message SignBlockResponse { - bytes signature = 1; - int64 timestamp = 2; -} message Nonce { int32 sourceID = 1; @@ -88,4 +72,22 @@ message GetLeaderResponse { } message PingRequest {} -message PingResponse {} \ No newline at end of file +message PingResponse {} + +message Block { + int64 height = 1; + int64 round = 2; + int32 step = 3; + bytes signBytes = 4; + int64 timestamp = 5; +} + +message SignBlockRequest { + string chainID = 1; + Block block = 2; +} + +message SignBlockResponse { + bytes signature = 1; + int64 timestamp = 2; +} \ No newline at end of file diff --git a/proto/strangelove/horcrux/threshold_validator.proto b/proto/strangelove/horcrux/threshold_validator.proto new file mode 100644 index 00000000..6a4baab2 --- /dev/null +++ b/proto/strangelove/horcrux/threshold_validator.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; +package strangelove.horcrux; + +option go_package = "github.com/strangelove-ventures/horcrux/signer/proto"; + +service TresholdValidator{ + rpc Validate(ValidateRequest) returns (ValidateResponse); + + rpc SignBlock (SignBlockRequest) returns (SignBlockResponse) {} + rpc TransferLeadership (TransferLeadershipRequest) returns (TransferLeadershipResponse) {} + rpc GetLeader (GetLeaderRequest) returns (GetLeaderResponse) {} +} + +message TransferLeadershipRequest { + string leaderID = 1; +} + +message TransferLeadershipResponse { + string leaderID = 1; + string leaderAddress = 2; +} + +message GetLeaderRequest {} + +message GetLeaderResponse { + int32 leader = 1; +} + +message PingRequest {} +message PingResponse {} + +message Block { + int64 height = 1; + int64 round = 2; + int32 step = 3; + bytes signBytes = 4; + int64 timestamp = 5; +} + +message SignBlockRequest { + string chainID = 1; + Block block = 2; +} + +message SignBlockResponse { + bytes signature = 1; + int64 timestamp = 2; +} \ No newline at end of file diff --git a/signer/chain-1_priv_validator_state.json b/signer/chain-1_priv_validator_state.json new file mode 100644 index 00000000..2e324556 --- /dev/null +++ b/signer/chain-1_priv_validator_state.json @@ -0,0 +1,7 @@ +{ + "height": "0", + "round": "0", + "step": 0, + "nonce_public": null, + "FilePath": "chain-1_priv_validator_state.json" +} \ No newline at end of file diff --git a/signer/cosigner_grpc_server.go b/signer/cosigner_grpc_server.go index 2c01c6a4..0759adde 100644 --- a/signer/cosigner_grpc_server.go +++ b/signer/cosigner_grpc_server.go @@ -56,7 +56,7 @@ func (rpc *NodeGRPCServer) SetNoncesAndSign( ChainID: req.ChainID, Nonces: &cosigner.CosignerUUIDNonces{ UUID: uuid.UUID(req.Uuid), - Nonces: cosigner.CosignerNoncesFromProto(req.GetNonces()), + Nonces: cosigner.FromProtoToNonces(req.GetNonces()), }, HRST: types.HRSTFromProto(req.GetHrst()), SignBytes: req.GetSignBytes(), diff --git a/signer/cosigner_health.go b/signer/cosigner_health.go index 704bc2ea..431a8af6 100644 --- a/signer/cosigner_health.go +++ b/signer/cosigner_health.go @@ -40,8 +40,8 @@ func (ch *CosignerHealth) Reconcile(ctx context.Context) { } var wg sync.WaitGroup wg.Add(len(ch.cosigners)) - for _, remote_cosigner := range ch.cosigners { - if rc, ok := remote_cosigner.(*cosigner.RemoteCosigner); ok { + for _, remotecosigner := range ch.cosigners { + if rc, ok := remotecosigner.(*cosigner.RemoteCosigner); ok { go ch.updateRTT(ctx, rc, &wg) } } diff --git a/signer/cosigner_nonce_cache.go b/signer/cosigner_nonce_cache.go index e2f07596..3faf0661 100644 --- a/signer/cosigner_nonce_cache.go +++ b/signer/cosigner_nonce_cache.go @@ -145,7 +145,7 @@ func (nc *NonceCache) PruneNonces() int { type CosignerNoncesRel struct { Cosigner ICosigner - Nonces cosigner.CosignerNonces + Nonces cosigner.Nonces } type CachedNonceSingle struct { @@ -361,7 +361,7 @@ func (cnc *CosignerNonceCache) GetNonces(fastestPeers []ICosigner) (*cosigner.Co defer cnc.cache.mu.Unlock() CheckNoncesLoop: for i, cn := range cnc.cache.cache { - var nonces cosigner.CosignerNonces + var nonces cosigner.Nonces for _, p := range fastestPeers { found := false for _, n := range cn.Nonces { diff --git a/signer/file.go b/signer/file.go index 840935bb..4e98d6c8 100644 --- a/signer/file.go +++ b/signer/file.go @@ -153,7 +153,7 @@ func GenFilePV(keyFilePath, stateFilePath string) *FilePV { } // If loadState is true, we load from the stateFilePath. Otherwise, we use an empty LastSignState. -func LoadFilePV(keyFilePath, stateFilePath string, loadState bool) (*FilePV, error) { +func LoadPVFile(keyFilePath, stateFilePath string, loadState bool) (*FilePV, error) { keyJSONBytes, err := os.ReadFile(keyFilePath) if err != nil { return nil, err diff --git a/signer/icosigner.go b/signer/icosigner.go index 46fe349d..c4eea69c 100644 --- a/signer/icosigner.go +++ b/signer/icosigner.go @@ -3,7 +3,6 @@ package signer import ( "context" - cometcrypto "github.com/cometbft/cometbft/crypto" "github.com/google/uuid" "github.com/strangelove-ventures/horcrux/pkg/cosigner" @@ -16,19 +15,20 @@ type ICosigner interface { // The index is the shamir index: 1, 2, etc... GetIndex() int - // Get the P2P URL (GRPC and Raft) + // Get the P2P URL (GRPC) GetAddress() string // Get the combined public key - GetPubKey(chainID string) (cometcrypto.PubKey, error) + // TODO: Change name to FetchPubKey + // GetPubKey(chainID string) (cometcrypto.PubKey, error) - VerifySignature(chainID string, payload, signature []byte) bool + // VerifySignature(chainID string, payload, signature []byte) bool // Get nonces for all cosigner shards GetNonces(ctx context.Context, uuids []uuid.UUID) (cosigner.CosignerUUIDNoncesMultiple, error) // Sign the requested bytes - SetNoncesAndSign(ctx context.Context, req cosigner.CosignerSetNoncesAndSignRequest) (*cosigner.CosignerSignResponse, error) + SetNoncesAndSign(ctx context.Context, req cosigner.CosignerSetNoncesAndSignRequest) (*cosigner.SignatureResponse, error) } type ICosigners []ICosigner // ICosigners is a list of ICosigner's diff --git a/signer/raft_store.go b/signer/raft_store.go index d3ee4eeb..8c6b6bad 100644 --- a/signer/raft_store.go +++ b/signer/raft_store.go @@ -92,6 +92,8 @@ func (s *RaftStore) SetThresholdValidator(thresholdValidator *ThresholdValidator s.thresholdValidator.MyCosigner = s.mycosigner // TODO: Refactor out the use of cosigner. } +// TODO: Should move away from this initilisation method +// and instead use a "service" framework. func (s *RaftStore) init() error { host := p2pURLToRaftAddress(s.RaftBind) _, port, err := net.SplitHostPort(host) diff --git a/signer/single_signer_validator.go b/signer/single_signer_validator.go index ceaaeea4..cc6646fc 100644 --- a/signer/single_signer_validator.go +++ b/signer/single_signer_validator.go @@ -84,12 +84,12 @@ func (pv *SingleSignerValidator) loadChainStateIfNecessary(chainID string) (*Sin } // The only scenario in which we want to create a new state file // on disk is when the state file does not exist. - filePV, err = LoadFilePV(keyFile, stateFile, false) + filePV, err = LoadPVFile(keyFile, stateFile, false) if err != nil { return nil, err } } else { - filePV, err = LoadFilePV(keyFile, stateFile, true) + filePV, err = LoadPVFile(keyFile, stateFile, true) if err != nil { return nil, err } diff --git a/signer/threshold_validator.go b/signer/threshold_validator.go index 4b8172bd..9e4c12a6 100644 --- a/signer/threshold_validator.go +++ b/signer/threshold_validator.go @@ -44,7 +44,7 @@ func nodecacheconfig() nodecacheconfigs { } // ThresholdValidator is the server that responds to sign requests from the "sentry" -// Implements the [connector.PrivValidator] interface. +// Implements the [connector.IPrivValidator] interface. type ThresholdValidator struct { config *config.RuntimeConfig @@ -457,7 +457,7 @@ func compareBlockSignatureAgainstHRS( func (pv *ThresholdValidator) getNoncesFallback( ctx context.Context, ) (*cosigner.CosignerUUIDNonces, []ICosigner, error) { - nonces := make(map[ICosigner]cosigner.CosignerNonces) + nonces := make(map[ICosigner]cosigner.Nonces) metrics.DrainedNonceCache.Inc() metrics.TotalDrainedNonceCache.Inc() @@ -483,7 +483,7 @@ func (pv *ThresholdValidator) getNoncesFallback( return nil, nil, errors.New("timed out waiting for ephemeral shares") } - var thresholdNonces cosigner.CosignerNonces + var thresholdNonces cosigner.Nonces thresholdCosigners := make([]ICosigner, len(nonces)) i := 0 for c, n := range nonces { @@ -518,7 +518,7 @@ func (pv *ThresholdValidator) waitForPeerNonces( u uuid.UUID, peer ICosigner, wg *sync.WaitGroup, - nonces map[ICosigner]cosigner.CosignerNonces, + nonces map[ICosigner]cosigner.Nonces, mu sync.Locker, ) { peerStartTime := time.Now() @@ -601,6 +601,9 @@ func (pv *ThresholdValidator) proxyIfNecessary( return true, signRes.Signature, stamp, nil } +// Sign returns the signature in byte for the given block and the time +// This function is called by the sentry and its responsible for in its turn calling the MPC +// to get a valid signature to return to the sentry. func (pv *ThresholdValidator) Sign(ctx context.Context, chainID string, block types.Block) ([]byte, time.Time, error) { height, round, step, stamp, signBytes := block.Height, block.Round, block.Step, block.Timestamp, block.SignBytes @@ -648,10 +651,12 @@ func (pv *ThresholdValidator) Sign(ctx context.Context, chainID string, block ty return existingSignature, existingTimestamp, nil } + // More or less everything belov here shoud be moved to a "cosigners" + // package. This is the actual MPC. + // MPC.SignBlock() is the actual function that does the MPC. numPeers := len(pv.peerCosigners) total := uint8(numPeers + 1) - // More or less everything belov here shoud be moved to a "cosigners" peerStartTime := time.Now() cosignersOrderedByFastest := pv.cosignerHealth.GetFastest() diff --git a/signer/threshold_validator_test.go.txt b/signer/threshold_validator_test.go similarity index 82% rename from signer/threshold_validator_test.go.txt rename to signer/threshold_validator_test.go index 7f679132..42271121 100644 --- a/signer/threshold_validator_test.go.txt +++ b/signer/threshold_validator_test.go @@ -5,9 +5,7 @@ import ( "context" "crypto/rand" "fmt" - mrand "math/rand" "path/filepath" - "sync" "time" "github.com/strangelove-ventures/horcrux/pkg/config" @@ -24,10 +22,10 @@ import ( cometcrypto "github.com/cometbft/cometbft/crypto" cometcryptoed25519 "github.com/cometbft/cometbft/crypto/ed25519" "github.com/cometbft/cometbft/crypto/tmhash" + "github.com/cometbft/cometbft/libs/log" cometlog "github.com/cometbft/cometbft/libs/log" cometrand "github.com/cometbft/cometbft/libs/rand" cometproto "github.com/cometbft/cometbft/proto/tendermint/types" - comet "github.com/cometbft/cometbft/types" "github.com/ethereum/go-ethereum/crypto/ecies" "github.com/ethereum/go-ethereum/crypto/secp256k1" "github.com/stretchr/testify/require" @@ -35,20 +33,87 @@ import ( "golang.org/x/sync/errgroup" ) +var ( + testConfig *config.RuntimeConfig // use this global config in tests +) + +const ( + defaultGetNoncesInterval = 3 * time.Second + defaultGetNoncesTimeout = 4 * time.Second + defaultNonceExpiration = 10 * time.Second // half of the local cosigner cache expiration + +) + +type MockValidator struct { + *signer.ThresholdValidator + nonceCache *signer.CosignerNonceCache +} + +func NewMockValidator( + logger log.Logger, + config *config.RuntimeConfig, + threshold int, + grpcTimeout time.Duration, + maxWaitForSameBlockAttempts int, + myCosigner *cosigner.LocalCosigner, + peerCosigners []signer.ICosigner, + leader signer.ILeader, +) *MockValidator { + allCosigners := make([]signer.ICosigner, len(peerCosigners)+1) + allCosigners[0] = myCosigner + copy(allCosigners[1:], peerCosigners) + + for _, peer := range peerCosigners { + logger.Debug("Peer peer", "id", peer.GetIndex()) + } + nc := signer.NewCosignerNonceCache( + logger, + allCosigners, + leader, + defaultGetNoncesInterval, + defaultGetNoncesTimeout, + defaultNonceExpiration, + uint8(threshold), + nil, + ) + return &MockValidator{ + signer.NewThresholdValidator(logger, config, threshold, grpcTimeout, maxWaitForSameBlockAttempts, allCosigners[0].(*cosigner.LocalCosigner), peerCosigners[1:], leader), + nc, + } +} + +func TestMain(m *testing.M) { + // optional alternative config via ENV VAR `CONFIG` + // if path := os.Getenv("CONFIG"); path != "" { + + // conf, err := LoadConfig(path) + // if err != nil { + // log.Fatalf("Failed to load config file %q : %v", path, err) + // } + + // testConfig = &conf + + // } else { + // testConfig = &config.RuntimeConfig{} + // } + testConfig = &config.RuntimeConfig{} + // call flag.Parse() here if TestMain uses flags + os.Exit(m.Run()) +} func TestThresholdValidator2of2(t *testing.T) { - testThresholdValidator(t, 2, 2, configuration) + testThresholdValidator(t, 2, 2, testConfig) } func TestThresholdValidator3of3(t *testing.T) { - testThresholdValidator(t, 3, 3) + testThresholdValidator(t, 3, 3, testConfig) } func TestThresholdValidator2of3(t *testing.T) { - testThresholdValidator(t, 2, 3) + testThresholdValidator(t, 2, 3, testConfig) } func TestThresholdValidator3of5(t *testing.T) { - testThresholdValidator(t, 3, 5) + testThresholdValidator(t, 3, 5, testConfig) } func loadKeyForLocalCosigner( @@ -71,7 +136,6 @@ func loadKeyForLocalCosigner( return os.WriteFile(config.KeyFilePathCosigner(chainID), keyBz, 0600) } - func testThresholdValidator(t *testing.T, threshold, total uint8, configuration *config.RuntimeConfig) { cosigners, pubKey := getTestLocalCosigners(t, threshold, total) @@ -87,7 +151,7 @@ func testThresholdValidator(t *testing.T, threshold, total uint8, configuration leader := &MockLeader{id: 1} - validator := signer.NewThresholdValidator( + validator := NewMockValidator( cometlog.NewNopLogger(), configuration, int(threshold), @@ -99,7 +163,10 @@ func testThresholdValidator(t *testing.T, threshold, total uint8, configuration ) defer validator.Stop() - leader.leader = validator + // var mockvalidator *MockValidator + // mockvalidator = mockvalidator(validator) + + leader.leader = validator.ThresholdValidator ctx := context.Background() @@ -174,7 +241,7 @@ func testThresholdValidator(t *testing.T, threshold, total uint8, configuration require.NoError(t, err) // reinitialize validator to make sure new runtime will not allow double sign - newValidator := signer.NewThresholdValidator( + newValidator := NewMockValidator( cometlog.NewNopLogger(), configuration, int(threshold), @@ -302,10 +369,10 @@ func testThresholdValidator(t *testing.T, threshold, total uint8, configuration } } -func getTestLocalCosigners(t *testing.T, threshold, total uint8) ([]*LocalCosigner, cometcrypto.PubKey) { +func getTestLocalCosigners(t *testing.T, threshold, total uint8) ([]*cosigner.LocalCosigner, cometcrypto.PubKey) { eciesKeys := make([]*ecies.PrivateKey, total) pubKeys := make([]*ecies.PublicKey, total) - cosigners := make([]*LocalCosigner, total) + cosigners := make([]*cosigner.LocalCosigner, total) for i := uint8(0); i < total; i++ { eciesKey, err := ecies.GenerateKey(rand.Reader, secp256k1.S256(), nil) @@ -322,10 +389,10 @@ func getTestLocalCosigners(t *testing.T, threshold, total uint8) ([]*LocalCosign tmpDir := t.TempDir() - cosignersConfig := make(CosignersConfig, total) + cosignersConfig := make(config.CosignersConfig, total) for i := range pubKeys { - cosignersConfig[i] = CosignerConfig{ + cosignersConfig[i] = config.CosignerConfig{ ShardID: i + 1, } } @@ -346,7 +413,7 @@ func getTestLocalCosigners(t *testing.T, threshold, total uint8) ([]*LocalCosign }, } - cosigner := NewLocalCosigner( + cosigner := cosigner.NewLocalCosigner( cometlog.NewNopLogger(), cosignerConfig, nodesecurity.NewCosignerSecurityECIES( @@ -362,49 +429,50 @@ func getTestLocalCosigners(t *testing.T, threshold, total uint8) ([]*LocalCosign cosigners[i] = cosigner - err = loadKeyForLocalCosigner(cosigner, privateKey.PubKey(), testChainID, privShards[i]) + err = loadKeyForLocalCosigner(cosigner, privateKey.PubKey(), testChainID, privShards[i], cosignerConfig) require.NoError(t, err) - err = loadKeyForLocalCosigner(cosigner, privateKey.PubKey(), testChainID2, privShards[i]) + err = loadKeyForLocalCosigner(cosigner, privateKey.PubKey(), testChainID2, privShards[i], cosignerConfig) require.NoError(t, err) } return cosigners, privateKey.PubKey() } +/* func testThresholdValidatorLeaderElection(t *testing.T, threshold, total uint8) { - cosigners, pubKey := getTestLocalCosigners(t, threshold, total) + peers, pubKey := getTestLocalCosigners(t, threshold, total) - thresholdValidators := make([]*signer.ThresholdValidator, 0, total) + thresholdValidators := make([]*MockThresholdValidator, 0, total) var leader *signer.ThresholdValidator leaders := make([]*MockLeader, total) ctx := context.Background() - for i, cosigner := range cosigners { - peers := make([]Cosigner, 0, len(cosigners)-1) - for j, otherCosigner := range cosigners { + for i, peer := range peers { + peers := make([]signer.ICosigner, 0, len(peers)-1) + for j, otherCosigner := range peers { if i != j { peers = append(peers, otherCosigner) } } - leaders[i] = &MockLeader{id: cosigner.GetIndex(), leader: leader} - tv := NewThresholdValidator( + leaders[i] = &MockLeader{id: peer.GetIndex(), leader: leader} + tv := NewMockValidator( cometlog.NewNopLogger(), - cosigner.config, + peer.config, int(threshold), time.Second, 1, - cosigner, + peer, peers, leaders[i], ) if i == 0 { - leader = tv - leaders[i].leader = tv + leader = tv.ThresholdValidator + leaders[i].leader = tv.ThresholdValidator } - thresholdValidators = append(thresholdValidators, tv) + thresholdValidators = append(thresholdValidators, tv.ThresholdValidator) defer tv.Stop() err := tv.LoadSignStateIfNecessary(testChainID) @@ -437,7 +505,7 @@ func testThresholdValidatorLeaderElection(t *testing.T, threshold, total uint8) for _, l := range leaders { l.SetLeader(newLeader) } - t.Logf("New leader: %d", newLeader.myCosigner.GetIndex()) + t.Logf("New leader: %d", newLeader.MyCosigner.GetIndex()) // time with new leader time.Sleep(time.Duration(mrand.Intn(50)+100) * time.Millisecond) //nolint:gosec @@ -584,3 +652,4 @@ func testThresholdValidatorLeaderElection(t *testing.T, threshold, total uint8) func TestThresholdValidatorLeaderElection2of3(t *testing.T) { testThresholdValidatorLeaderElection(t, 2, 3) } +*/